Skip to content

feat: declare billing-accounts-per-organization quota via ServiceConfiguration#40

Open
mattdjenkinson wants to merge 6 commits into
mainfrom
feat/billing-account-quota
Open

feat: declare billing-accounts-per-organization quota via ServiceConfiguration#40
mattdjenkinson wants to merge 6 commits into
mainfrom
feat/billing-account-quota

Conversation

@mattdjenkinson
Copy link
Copy Markdown
Contributor

@mattdjenkinson mattdjenkinson commented May 21, 2026

Summary

Declares the BillingAccount-per-Organization ceiling on a new `ServiceConfiguration` for the billing service. The service-catalog controller fans this out to the underlying `ResourceRegistration` + `ClaimCreationPolicy` + default `ResourceGrant` — no hand-rolled quota CRDs and no billing-side admission code.

  • `config/milo/service.yaml` — registers `billing-miloapis-com` as a `Service` (canonical `serviceName: billing.miloapis.com`, displayName "Billing").
  • `config/milo/service-configuration.yaml` — declares one metric (`billing.miloapis.com/billingaccount/count`, `kind: Delta`, `unit: 1`), one `quota.limits` entry (consumer Organization, `defaultLimit: 1`, unit `1/{organization}`), and one `quota.metricRules` entry mapping `BillingAccount` creation → cost of 1 on that metric.

How the gate works

  1. New Organization → service-catalog fans the `defaultLimit` out to a default `ResourceGrant` of 1 BillingAccount.
  2. Org creates 1st BillingAccount → `ClaimCreationPolicy` (also fanned out from `metricRules`) records a claim of 1 against the Organization's billingaccount-count capacity. Within quota.
  3. Org tries to create a 2nd → admission plugin sees the claim would exceed the grant and rejects.
  4. Operator wants to enable multi-BA for that org → issues an additional `ResourceGrant` on the target Organization with positive capacity on `billing.miloapis.com/billingaccount/count`.

Consumer Organization resolution at claim time is the service-catalog admission plugin's job — derived from the request's URL parent context (`/apis/.../organizations/{id}/control-plane/...`), not from a namespace name. That was the bug in #39.

Why this PR (vs. the previous shape)

The first attempt at #40 declared the same three CRDs hand-rolled in `config/components/feature-flags/`. Review feedback from @scotwells flagged that the recently-merged milo-os/service-catalog#14 introduced first-class `spec.quota.limits[]` + `spec.quota.metricRules[]` on `ServiceConfiguration` for exactly this — providers declare the quota once on their ServiceConfiguration and the controller fans it out. Migrating one quota over keeps billing's quota declaration architecturally aligned with how every future service (compute, storage, …) will work.

Out of scope (narrow)

  • Migrating existing legacy registrations (`cloud-portal-usage-metering-dashboard`) into the new ServiceConfiguration. They keep working as-is; folding them into the same document is a follow-up.
  • The infra-side FluxCD Kustomization that applies `config/milo/` to milo-system. Mirrors `activity-system/milo-configuration.yaml`; lands in a separate PR on `datum/infra` once this billing PR ships.

Pairs with / replaces

Gate the number of BillingAccounts an Organization can hold via
milo's quota system rather than a billing-side feature flag. Day-one
design intent (one BA per org by default; multi-BA opt-in for higher
tiers) is preserved while delegating consumer resolution to the
quota admission plugin — which derives the Organization from the
parent context (see milo/docs/architecture/discovery-contexts.md),
so billing's own webhooks never need to interpret namespaces as org
identifiers.

- ResourceRegistration billing-accounts-per-organization:
  type Entity, consumer Organization, resourceType
  billing.miloapis.com/billingaccounts, baseUnit "account",
  claimingResources BillingAccount.
- ClaimCreationPolicy billing-account-quota-enforcement: every
  BillingAccount creation claims 1 unit of the org's billingaccounts
  capacity. consumerRef is auto-filled by the admission plugin from
  the request's parent context.
- GrantCreationPolicy billing-accounts-default-grant: when an
  Organization reaches Active phase, auto-grant 1 unit of
  billingaccounts capacity. Preserves the design promise that every
  org can have a BillingAccount out of the box.

Operators enable multi-BA for an org by issuing an additional
ResourceGrant on the target org (or a tier-specific
GrantCreationPolicy that does so automatically). The billing service
needs no code changes — quota enforcement happens entirely in milo's
admission plugin.

Replaces the closed #39 (which tried to enforce the same gate
billing-side but incorrectly assumed BillingAccount.metadata.namespace
was the org name).
@mattdjenkinson mattdjenkinson marked this pull request as ready for review May 21, 2026 16:26
scotwells
scotwells previously approved these changes May 22, 2026
Copy link
Copy Markdown
Contributor

@scotwells scotwells left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you look at using the service catalog for this? I added support for registering quota configurations in milo-os/service-catalog#14.

Addresses review feedback on #40 (scotwells): use the service-catalog
quota pattern introduced in milo-os/service-catalog#14 instead of
emitting ResourceRegistration + ClaimCreationPolicy + GrantCreationPolicy
manifests directly.

The previous approach worked but duplicated the fan-out logic the
service-catalog controller already provides. Migrating the
billing-accounts gate to a ServiceConfiguration is the architecturally
aligned move and matches how compute / storage / etc. will declare
their own quotas going forward.

Removed:
- config/components/feature-flags/registrations/billing-accounts.yaml
- config/components/feature-flags/policies/* (whole subdir)
- The corresponding kustomization entries.

Added (new config/milo/ tree mirroring activity's layout):
- config/milo/service.yaml — registers billing-miloapis-com as a
  Service (serviceName billing.miloapis.com, displayName "Billing").
- config/milo/service-configuration.yaml — declares one metric
  (billing.miloapis.com/billingaccount/count, kind Delta, unit 1),
  one quota.limits entry (consumer Organization, defaultLimit 1,
  unit 1/{organization}), and one quota.metricRules entry mapping
  BillingAccount creation to a cost of 1 on that metric. The
  service-catalog controller fans these out to the underlying
  ResourceRegistration + ClaimCreationPolicy + default
  ResourceGrant.
- config/milo/kustomization.yaml.

Out of scope (narrow PR — Matt confirmed):
- Migrating the existing legacy registrations (cloud-portal-usage-
  metering-dashboard) into the new ServiceConfiguration. They keep
  working as-is; can be folded into the same ServiceConfiguration as
  a follow-up.
- The infra-side FluxCD Kustomization that applies config/milo/.
  Mirrors activity-system/milo-configuration.yaml; lands in a
  separate PR on datum/infra once this billing PR ships.
@mattdjenkinson mattdjenkinson changed the title feat: register BillingAccount as a quota Entity per Organization feat: declare billing-accounts-per-organization quota via ServiceConfiguration May 22, 2026
Adds an explicit, named feature flag on top of the
billing-accounts-per-organization quota declared via the
ServiceConfiguration. Operators get a single discoverable toggle per
org; flipping it on opens the quota cap automatically — no separate
"raise the count override" step.

How the two layers cooperate:

- ServiceConfiguration declares the default cap (1 BA / org) and the
  claim cost per BillingAccount creation. The quota admission plugin
  enforces it.
- The new feature-multi-billing-accounts ResourceRegistration is a
  quota Feature (boolean entitlement, no admission machinery of its
  own). Operators apply / remove a ResourceGrant on this Feature for
  the target Organization to express intent.
- A GrantCreationPolicy watches ResourceGrants of the Feature and
  emits a count-override grant of 99 against the same Organization on
  billing.miloapis.com/billingaccount/count, raising the effective
  cap to 100. Constraint filters out the policy's own output so it
  does not recurse on grants it produces.

The override grant is named deterministically
(multi-billing-accounts-count-override-<org>) so re-applying the
Feature grant does not pile up duplicates.

Files:
- config/components/feature-flags/registrations/multi-billing-accounts.yaml
- config/components/feature-flags/policies/multi-billing-accounts-feature-grant.yaml
- config/components/feature-flags/policies/kustomization.yaml
- Parent kustomizations updated to include policies/ and the new
  registration.
@mattdjenkinson
Copy link
Copy Markdown
Contributor Author

@scotwells yeah good shout - think i've updated it correctly?

@mattdjenkinson mattdjenkinson requested a review from scotwells May 22, 2026 08:01
Brings the multi-billing-accounts Feature registration in line with
the cloud-portal-usage-metering-dashboard Feature already in the
repo, so the cloud-portal MiloFeatureFlagProvider (which checks
AllowanceBucket.status.available > 0 keyed on spec.resourceType) can
resolve it the same way.

- Flatten resourceType:
    billing.miloapis.com/feature/multi-billing-accounts
  → billing.miloapis.com/multi-billing-accounts
  Existing convention is billing.miloapis.com/<flag-name>; the
  /feature/ segment was inconsistent and would have required a
  bespoke FeatureFlag enum entry on the portal side.
- Declare BillingAccount as a claimingResource so an AllowanceBucket
  is materialised per Organization. Without it the provider never
  sees a bucket and falls back to defaultValue (flag off) even after
  a Feature grant is issued.
- Update the GrantCreationPolicy CEL constraint to match the new
  flat resourceType string so the +99 count-override grant still
  fires on the Feature grant.
@mattdjenkinson mattdjenkinson requested a review from JoseSzycho May 22, 2026 14:03
Copy link
Copy Markdown
Contributor

@scotwells scotwells left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. We should make an issue in the service catalog backlog to consider how we represent the quota features we're configuring manually here. Ideally everything is pushed through the service catalog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants