feat: declare billing-accounts-per-organization quota via ServiceConfiguration#40
Open
mattdjenkinson wants to merge 6 commits into
Open
feat: declare billing-accounts-per-organization quota via ServiceConfiguration#40mattdjenkinson wants to merge 6 commits into
mattdjenkinson wants to merge 6 commits into
Conversation
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).
scotwells
previously approved these changes
May 22, 2026
Contributor
scotwells
left a comment
There was a problem hiding this comment.
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.
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.
Contributor
Author
|
@scotwells yeah good shout - think i've updated it correctly? |
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.
scotwells
approved these changes
May 22, 2026
Contributor
scotwells
left a comment
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
How the gate works
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)
Pairs with / replaces