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
- func AssignPlan(discovery Installed, store dynamic.Interface, logger klog.Logger) func(ctx context.Context, org, plan string) error
- func EntitlementPlan(discovery Installed, store dynamic.Interface, logger klog.Logger) func(ctx context.Context, org string) (plan string, ok bool, err error)
- func IsPlanArchived(plan *unstructured.Unstructured) bool
- func MeteringClient(base *rest.Config, workspace string) (dynamic.Interface, error)
- func PlanExists(discovery Installed, store dynamic.Interface) func(ctx context.Context, name string) (bool, error)
- func PlanIsArchived(store dynamic.Interface) func(ctx context.Context, name string) (bool, error)
- func ResetPlan(discovery Installed, store dynamic.Interface, defaultPlan string, ...) func(ctx context.Context, org string) error
- func SetupOrganizationReconciler(mgr manager.Manager, opts OrganizationOptions) error
- func SetupProviderReconciler(mgr mcmanager.Manager, opts ProviderOptions) error
- func ValidateDefaultPlan(store dynamic.Interface, defaultPlan string) func(ctx context.Context) error
- func WorkspaceClients(base *rest.Config) func(path string) (dynamic.Interface, error)
- type Billing
- func (b *Billing) Checkout(ctx context.Context, org, plan, successURL, cancelURL string) (string, error)
- func (b *Billing) CheckoutQuoted(ctx context.Context, org, plan, expectedPrice, successURL, cancelURL string) (string, error)
- func (b *Billing) ForwardWebhook(w http.ResponseWriter, r *http.Request)
- func (b *Billing) Portal(ctx context.Context, org, returnURL string) (string, error)
- func (b *Billing) Summary(ctx context.Context, org string) (OrgBilling, error)
- type BillingError
- type Discovery
- type Endpoints
- type EntitlementUpdate
- type Handler
- type Installed
- type Limit
- type OrgBilling
- type OrgSubscription
- type OrganizationOptions
- type OrganizationReconciler
- type PlanOffer
- type PlanUpdate
- type ProviderGetter
- type ProviderOptions
- type ProviderReconciler
- type SubscriptionPrice
- type UsageWindow
- type WorkspaceNotifier
- type Writer
- func (wr *Writer) ArchivePlan(ctx context.Context, name string) error
- func (wr *Writer) PutPlan(ctx context.Context, name string, body PlanUpdate) (*unstructured.Unstructured, bool, error)
- func (wr *Writer) UpdateEntitlement(ctx context.Context, name string, body EntitlementUpdate) (*unstructured.Unstructured, error)
Constants ¶
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" )
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" )
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 ¶
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
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.
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
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.
type BillingError ¶ added in v0.2.34
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 ¶
NewDiscovery builds a Discovery from the hub's kcp config (any cluster path; it is re-scoped to workspace).
func (*Discovery) Current ¶
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
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.
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 ¶
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 (*Handler) WithWriter ¶
WithWriter enables the write routes on h.
type Installed ¶
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 ¶
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.
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 ¶
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 ¶
func (r *ProviderReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error)
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
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
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.