Skip to content

design-proposal: tenant quotas as reservation limits - #48

Open
mattia-eleuteri wants to merge 3 commits into
cozystack:mainfrom
mattia-eleuteri:proposal/tenant-quota-reservation
Open

design-proposal: tenant quotas as reservation limits#48
mattia-eleuteri wants to merge 3 commits into
cozystack:mainfrom
mattia-eleuteri:proposal/tenant-quota-reservation

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Aug 3, 2026

Copy link
Copy Markdown

What this PR does

Adds a design proposal under design-proposals/tenant-quota-reservation/ proposing that tenant quotas become reservation limits: consumption is the sum of the sizes declared in a tenant's apps.cozystack.io resources, evaluated at admission, rather than the requests and limits of the pods those resources produce.

Verified against main at 10554f9dc.

The problem, in one number

A tenant quota is declared in instance-type units (memory: 16Gi, what the tenant buys and the dashboard shows) but enforced in pod units, because it is ultimately compared against ResourceQuota.status.used. A virt-launcher pod requests the guest memory plus KubeVirt's virtualization overhead, which is additive per VM rather than proportional to the amount reserved. So a tenant whose quota is fully allocated to declared VMs cannot start the last one.

--tenant-quota-buffer-percent exists to absorb that, and cannot be set correctly, because the required buffer depends on VM granularity rather than volume. For a 16Gi quota, at roughly 468Mi of overhead per launcher:

Tenant shape Actual launcher demand Buffer required
2 VMs of 8Gi 17320Mi +6%
16 VMs of 1Gi 23872Mi +46%
64 VMs of 256Mi 46336Mi +183%

Any single value is simultaneously too tight for tenants running many small VMs and too generous for tenants running few large ones. The knob is not mistuned, it is the wrong shape for the error it corrects.

There is a second symptom with the same root cause: ResourceQuota.status.used drifts permanently and is never recomputed. Since v1.6.0 reads it from the admission path (parentPoolUsage), a stale counter in any pool-member namespace can now forbid the creation of a legitimate sub-tenant, with an error blaming the parent's quota. A platform bug in a leaf namespace has become an onboarding failure.

What it proposes

The hierarchical pool machinery in internal/controller/tenantquota is kept as-is, and only its usage oracle changes. The declaration gate already speaks the reservation vocabulary — parseDeclaredQuotas says the flatten expansion is "intentionally not applied here" — so the arithmetic is already right; pod units enter at exactly two call sites.

  • A declarative spec.reservation block on ApplicationDefinition, so no per-kind Go lives in the aggregated apiserver.
  • parentPoolUsage and snapshot sum declared reservations instead of status.used; renderedLimitKey is deleted. This also takes uncached reads off the admission path, since HelmReleases already have an informer where ResourceQuotas deliberately do not.
  • The declaration gate generalizes from Tenant to every kind, on Create and Update, charging only the delta on update.
  • The chart-rendered ResourceQuota is kept, deliberately loose, as a capacity guard. It has to stay: the LimitRange sits under the same if .Values.resourceQuotas guard, and AutoResourceLimitsGate only sets limits on virt-launcher when the namespace has a quota constraining limits.*.
  • --tenant-quota-buffer-percent becomes unnecessary and is deprecated in the last rollout phase.

What it deliberately does not do

It does not fix the kube-controller-manager counter staleness, introduce usage-based quotas, or evict already-admitted workloads when a quota is lowered. It takes the stale counter out of the tenant contract and off the admission path; fixing it remains separate work.

Ordering against in-flight work

The proposal is written to compose with what is already in review, and two of those change it materially:

Where reviewer input is most useful

  • Reserving maxReplicas for an autoscaled pool charges idle tenants for headroom. Reserving minReplicas cannot work, though: the cluster-autoscaler scales the MachineDeployment without touching the CR, so no admission gate ever sees it. Is charging the maximum the right trade?
  • Should charts be required to materialize sized defaults into their values, so the reservation contract stays a pure function of stored values? That would prevent the class of hole rather than the md0 instance, and it would equally help the versioning proposal.
  • The cozy-lib preset table is Helm-only. Port it to Go with a parity test that parses _resourcepresets.tpl, or export it as a ConfigMap from the platform chart?
  • Is a deliberately loose guard quota acceptable, or should the LimitRange and AutoResourceLimitsGate be wired independently of resourceQuotas?

Summary by CodeRabbit

  • Documentation
    • Added a proposal for reservation-based tenant quota accounting using declared application resources.
    • Documented declarative reservation settings, resource and storage evaluation, instance-type and preset resolution, and quota handling during application updates.
    • Outlined admission checks, capacity reporting, overcommit visibility, compatibility behavior, security considerations, testing, and rollout phases.

mattia-eleuteri and others added 3 commits August 3, 2026 10:57
Tenant quotas are declared in instance-type units but enforced in pod
units, and the gap between the two is KubeVirt's per-VM virtualization
overhead: additive, not proportional. That is why
--tenant-quota-buffer-percent cannot be set correctly, the required
buffer ranging from +6% to +183% with VM granularity alone.

The proposal makes reservation the accounting authority: the sum of the
sizes declared in a tenant's apps.cozystack.io resources, evaluated at
admission via a declarative spec.reservation block on
ApplicationDefinition. The v1.6.0 hierarchical pool machinery is kept;
only its usage oracle changes, which also takes
ResourceQuota.status.used off the admission path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…es split

Rechecked against upstream/main (10554f9dc). The implicit md0 default is
still live: #2936 made it removable and migration 47 pins it, but the
kubernetes.nodeGroups helper still emits it when no pool is declared.

What changes the design is that kubernetes-nodes-split phase 1 has already
landed: KubernetesNodes is a registered kind whose sizing sits at the top
level of its own values, so worker-pool reservation is a flat block with no
iteration and no chart-computed default. Phase 2 (#3315) removes
spec.nodeGroups and the implicit md0 with it.

The proposal therefore no longer introduces a helper-resolution escape
hatch. The evaluator stays a pure function of stored values, md0 is a
transitional exception, and phase 4 of the rollout is preferably ordered
after #3315. Also notes that reserving minReplicas cannot work for the
cluster-autoscaler, which scales the MachineDeployment without touching the
CR: the strongest argument for reserving maxReplicas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
Per design-proposals/template.md the status transitions Draft -> Review
when the PR is opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The proposal describes reservation-based tenant quota accounting. It defines the reservation contract, resource evaluation and aggregation APIs, admission checks, controller reporting, operational semantics, rollout phases, and validation plans.

Tenant quota reservation

Layer / File(s) Summary
Accounting model and quota boundaries
design-proposals/tenant-quota-reservation/README.md
Documents current pod-based accounting and defines reservation-based accounting with separate reservation and operational limits.
Reservation contract and shipped configurations
design-proposals/tenant-quota-reservation/README.md
Defines ApplicationDefinition reservation fields and configurations for VM, storage, database, node, and Kubernetes resources.
Evaluation, aggregation, and admission flow
design-proposals/tenant-quota-reservation/README.md
Defines resolver, evaluator, and aggregator APIs. Admission checks reservation deltas for all application kinds. The tenant-quota controller reports reserved capacity and overcommit status.
Operational semantics and rollout validation
design-proposals/tenant-quota-reservation/README.md
Specifies failure handling, compatibility behavior, security rules, rollout phases, tests, open questions, and rejected alternatives.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ApplicationDefinition
  participant Admission
  participant Evaluate
  participant Resolver
  participant Aggregator
  participant TenantQuotaController
  ApplicationDefinition->>Admission: submit declared reservation
  Admission->>Evaluate: evaluate spec and values
  Evaluate->>Resolver: resolve instance type or preset
  Resolver-->>Evaluate: return resource values
  Evaluate-->>Admission: return reservation
  Admission->>Admission: check update reservation delta
  Aggregator->>TenantQuotaController: report namespace reservations
  TenantQuotaController->>TenantQuotaController: report reserved capacity and overcommit status
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the proposal to change tenant quotas from pod-resource limits to reservation limits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@design-proposals/tenant-quota-reservation/README.md`:
- Line 442: Update the concurrent-admission behavior in the tenant quota
reservation proposal to avoid claiming the overshoot is limited to one
application. Specify an atomic reservation mechanism with per-pool serialization
or optimistic concurrency, or explicitly document best-effort admission
semantics and define the resulting billing, reporting, and tenant-guarantee
treatment.
- Around line 309-315: Delay enabling kubernetes reservation enforcement until
the implicit md0 default is covered: either materialize the default in
evaluation, resolve the kubernetes.nodeGroups chart helper, or exclude
kubernetes from phase-3 gating. Update the rollout and reservation gate
references so the kubernetes block cannot become enforceable while empty
nodeGroups still produce an uncharged ten-worker pool.
- Around line 152-168: Make the reservation contract versioned or immutable
after an application first uses it: preserve the selected spec.reservation shape
and referenced presets/instance types for existing applications, or persist a
reservation version/snapshot with each application and migrate changes
explicitly. Update the ApplicationDefinitionSpec reservation handling and
related accounting evaluation so later definition or preset changes cannot
silently alter an existing application’s charge.
- Around line 383-391: Clarify the coexistence behavior so the controller
disables EnforcedHard and the tenant-quota-allocated object whenever the
reservation oracle is active, while retaining both only for the legacy path.
Update the surrounding rollout description to make this gating explicit and
prevent reservation-unit values from being applied as pod-unit ResourceQuota
clamps.
- Around line 355-364: The proposed admission accounting must not use cached
HelmRelease objects or evaluated values as its authoritative reservation source.
Update parentPoolUsage and snapshot to read the committed apps.cozystack.io
custom resources—or an atomic reservation record—the admission gate evaluates,
using the same stored representation and preserving alignment between
reservation semantics and accounting.
- Line 445: Update the autoscaler behavior statement in the quota reservation
proposal to distinguish cluster-autoscaler from DHA: describe cluster-autoscaler
scale-ups as covered by the pre-reserved maxReplicas capacity if that is the
intended design, or document the admission/controller enforcement path for live
MachineDeployment scaling instead of claiming the Update gate rejects it.
- Around line 331-352: Expand the reservation contract around Aggregator and
Resolver to define how each application maps to its ApplicationDefinition and
kind, which ApplicationDefinition storage version Evaluate consumes, and how
definitions are resolved for out-of-tree kinds. Specify that ForNamespaces
aggregates only applications while excluding namespace-scoped HelmRelease
objects, so evaluation is deterministic across definition versions and resource
types.
- Around line 426-432: The reservation admission design must fail closed for
existing unevaluable or missing reservations, including out-of-tree
ApplicationDefinitions. Update the reservation evaluation and admission behavior
so missing reservations require an explicit reviewed zero-reservation exemption,
while unevaluable existing applications either block pending reservations or
retain their last known reservation until evaluation succeeds; do not rely
solely on pool conditions or in-tree completeness tests.
- Around line 393-404: Update the proposal’s capacity-guard design around
ResourceQuota, LimitRange, and AutoResourceLimitsGate so the loose operational
guard is explicitly separate from the exact tenant reservation contract. Ensure
the documented admission behavior does not claim that a tenant charged exactly
to its quota can always start workloads when Kubernetes ResourceQuota may reject
pods based on status.used; either retain the guard solely as an independent
operational limit or describe independently wired LimitRange and
AutoResourceLimitsGate behavior.
- Around line 434-440: Align the evaluator and vm-instance rendering for
applications with both instanceType and explicit resources. Update the relevant
vm-instance chart logic to omit the instance-type reference when resources take
precedence, or reject the conflicting configuration before reservation
enforcement; ensure admitted VMs do not render both fields while reservation
sizing remains based on explicit resources.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e47530bb-86d0-41a1-8c4c-0475d5344d77

📥 Commits

Reviewing files that changed from the base of the PR and between c484063 and ca91cf5.

📒 Files selected for processing (1)
  • design-proposals/tenant-quota-reservation/README.md

Comment on lines +152 to +168
### 2. `spec.reservation` on `ApplicationDefinition`

Each application declares how to read its own size, next to the `openAPISchema` the definition already carries. The apiserver contains no per-kind knowledge.

This is the same move [PR #39](https://github.com/cozystack/community/pull/39) makes for visibility, cardinality and sharing: behavior that varies per kind becomes data on the definition rather than a branch in Go. It is also what [PR #43](https://github.com/cozystack/community/pull/43) forces, since an out-of-tree catalog cannot ship a patch to the aggregated apiserver.

```go
// api/v1alpha1/applicationdefinitions_types.go

type ApplicationDefinitionSpec struct {
Application ApplicationDefinitionApplication `json:"application"`
Release ApplicationDefinitionRelease `json:"release"`
// Reservation declares how much this application charges against its
// tenant's quota, read from the application's own values. Absent means the
// kind reserves nothing.
// +optional
Reservation *ApplicationDefinitionReservation `json:"reservation,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
sed -n '130,200p' design-proposals/tenant-quota-reservation/README.md

echo
echo "== reservation mentions =="
rg -n "Reservation|reservation|ApplicationDefinitionReservation|re-evaluated|re-?evaluate|snapshot|version" design-proposals/tenant-quota-reservation/README.md

Repository: cozystack/community

Length of output: 19516


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== related proposal files =="
git ls-files | rg 'proposal/|application-definition-versioning|tenant-quota-reservation'

echo
echo "== application-definition-versioning mentions in repo =="
rg -n "application-definition-versioning|ApplicationSchema|storage version|spec\.reservation|reservation" . | head -200

Repository: cozystack/community

Length of output: 17606


Version the reservation contract for existing applications.

Update ApplicationDefinition currently changes each application’s accounting shape. If spec.reservation is removed, changed, or its referenced presets change, existing applications can be re-evaluated with a different charge without the tenant changing app.spec. Treat the reservation contract as immutable after first use, or persist a reservation version/snapshot with each application and migrate it explicitly. Apply the same rule to referenced presets and instance types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 152 - 168,
Make the reservation contract versioned or immutable after an application first
uses it: preserve the selected spec.reservation shape and referenced
presets/instance types for existing applications, or persist a reservation
version/snapshot with each application and migrate changes explicitly. Update
the ApplicationDefinitionSpec reservation handling and related accounting
evaluation so later definition or preset changes cannot silently alter an
existing application’s charge.

Comment on lines +309 to +315
**Where a values path is not enough, and why that is temporary.** The evaluator reads values, so a workload a chart synthesizes without mentioning it in the values is invisible to it. Exactly one such case exists in tree, and it is on its way out.

On `main` today, `kubernetes.nodeGroups` (`packages/apps/kubernetes/templates/_helpers.tpl`) emits a default `md0` pool with `maxReplicas: 10` whenever `.Values.nodeGroups` is empty. [cozystack/cozystack#2936](https://github.com/cozystack/cozystack/pull/2936) made that default *removable*, so it now applies only when no pool is declared and migration 47 pins it explicitly on existing clusters, but it is still emitted for a cluster that declares none. A literal read of `nodeGroups` therefore charges nothing for a cluster that can autoscale to ten workers, which is an under-charge and so a quota hole.

Two ways to close it. The narrow one is to resolve the named chart helper rather than the raw path for this single kind, which works but puts a slice of chart logic into the reservation contract. The better one is to let [`kubernetes-nodes-split`](#scope-and-related-proposals) close it: [#3315](https://github.com/cozystack/cozystack/pull/3315) removes `spec.nodeGroups` and the implicit `md0` with it, and worker pools become `KubernetesNodes` resources whose sizing sits at the top level of their own values, with nothing implicit left to resolve.

This proposal therefore does **not** introduce a general escape hatch. It keeps the evaluator a pure function of stored values, treats the `md0` default as a transitional exception covered by the per-kind unit test, and prefers ordering phase 4 of the [rollout](#rollout) after #3315 so the exception is never written. If #3315 slips, the narrow helper resolution is the fallback, scoped to one kind and one path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not activate the kubernetes reservation before the md0 default is covered.

The exception becomes active in phase 3, not phase 4. Phase 2 already ships a reservation block for kubernetes (Line 465), and phase 3 enables the gate for kinds with a block. With empty nodeGroups, the chart creates md0 with ten workers while the evaluator charges zero.

Materialize the default, resolve the helper, or exclude kubernetes from reservation enforcement until kubernetes-nodes-split completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 309 - 315,
Delay enabling kubernetes reservation enforcement until the implicit md0 default
is covered: either materialize the default in evaluation, resolve the
kubernetes.nodeGroups chart helper, or exclude kubernetes from phase-3 gating.
Update the rollout and reservation gate references so the kubernetes block
cannot become enforceable while empty nodeGroups still produce an uncharged
ten-worker pool.

Comment on lines +331 to +352
`pkg/reservation` exposes three pieces, split so that the pure arithmetic is testable without a cluster:

```go
// Resolver turns a size name into a resource list.
type Resolver interface {
InstanceType(ctx context.Context, name string) (corev1.ResourceList, error)
Preset(name string) (corev1.ResourceList, error)
}

// Evaluate applies a kind's reservation spec to one application's values.
// Pure apart from Resolver; no client, no cluster state.
func Evaluate(
ctx context.Context,
spec *v1alpha1.ApplicationDefinitionReservation,
values map[string]any,
r Resolver,
) (corev1.ResourceList, error)

// Aggregator sums the reservations of every application in a set of namespaces.
type Aggregator interface {
ForNamespaces(ctx context.Context, namespaces []string) (map[string]corev1.ResourceList, error)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,200p'

echo "== target context =="
if [ -f design-proposals/tenant-quota-reservation/README.md ]; then
  wc -l design-proposals/tenant-quota-reservation/README.md
  sed -n '300,370p' design-proposals/tenant-quota-reservation/README.md
fi

echo "== related references =="
rg -n "ApplicationDefinitionReservation|ForNamespaces|Resolver|Evaluate\\(" design-proposals/tenant-quota-reservation/README.md pkg/reservation -S || true

echo "== search relevant types =="
rg -n "type .*ApplicationDefinitionReservation|ApplicationDefinitionReservation|HelmRelease|Application|storage version|storageVersion|values|valuesPath|ApplicationDefinition" -S --glob '!vendor/**' --glob '!node_modules/**' | head -n 200

Repository: cozystack/community

Length of output: 7953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact proposal section and nearby definitions if present.
[ -f design-proposals/tenant-quota-reservation/README.md ] && {
  echo "== lines 1-80 =="
  sed -n '1,80p' design-proposals/tenant-quota-reservation/README.md
  echo "== lines 200-320 =="
  sed -n '200,320p' design-proposals/tenant-quota-reservation/README.md
}

# Programmatic check: ensure the described interfaces exist and no mapping fields are present in the snippet.
python3 - <<'PY'
from pathlib import Path
import re
p = Path("design-proposals/tenant-quota-reservation/README.md")
if not p.exists():
    raise SystemExit("README missing")
text = p.read_text()
start = text.find("func Evaluate(")
end = text.find("###")
if start < 0:
    raise SystemExit("Evaluate signature missing")
snippet = text[max(0, start-1200): end if end > 0 else start+1200]
print("has Evaluate signature:", "func Evaluate(" in snippet)
print("has Aggregator signature:", "type Aggregator interface" in snippet)
print("has Resolver signature:", "type Resolver interface" in snippet)
for name in ["ApplicationDefinition", "HelmRelease", "storageVersion", "values", "kind", "set"]:
    print(f"{name}: {text.count(name)}")
PY

Repository: cozystack/community

Length of output: 16677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target section =="
sed -n '130,190p' design-proposals/tenant-quota-reservation/README.md
sed -n '328,380p' design-proposals/tenant-quota-reservation/README.md

echo "== deterministic contract parse =="
python3 - <<'PY'
from pathlib import Path
import re

text = Path("design-proposals/tenant-quota-reservation/README.md").read_text()

interfaces = {}
for m in re.finditer(r'(?m)^type\s+(\w+)\s+interface\s*\{(.*?)\n\}', text, re.S):
    name = m.group(1)
    body = m.group(2)
    interfaces[name] = {
        "methods": re.findall(r'(?m)^\s*(\w+)\([^)]*\)', body),
        "args_strings": " ".join(body.split()),
    }

print("interfaces:", list(interfaces.keys()))
for name, data in interfaces.items():
    if name in {"Resolver", "Evaluate", "Aggregator"}:
        print(f"== {name} ==")
        for line in data["args_strings"].split()[:100]:
            print(line)

section = text[text.find("# 4. Reservation as the usage oracle"):text.find("### 5. Generalizing")]
required_terms = {
    "storage version": [],
    "ApplicationSchema": [],
    "ApplicationDefinition": [],
    "version lookup": [],
    "values mapping": [],
    "valuesPath": [],
    "HelmRelease kind": [],
    "exclude HelmRelease": [],
    "out-of-tree": [],
}
for term, found in required_terms.items():
    hit = any(term.lower() in section.lower() or (term in text) for _ in [1])
    print(f"{term}: {hit}")
PY

Repository: cozystack/community

Length of output: 7414


Complete the Aggregator contract before implementation.

Evaluate needs an ApplicationDefinitionReservation, values, and a resolver. Aggregator.ForNamespaces receives only namespace names, and Resolver does not define how application/kind definitions are looked up. Define the application-to-definition mapping, which ApplicationDefinition storage version is used for reservation, and how the aggregator excludes only applications and keeps namespace-scoped HelmRelease objects out of the summation.

Without that mapping, out-of-tree kinds and application-definition versioning cannot be evaluated deterministically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 331 - 352,
Expand the reservation contract around Aggregator and Resolver to define how
each application maps to its ApplicationDefinition and kind, which
ApplicationDefinition storage version Evaluate consumes, and how definitions are
resolved for out-of-tree kinds. Specify that ForNamespaces aggregates only
applications while excluding namespace-scoped HelmRelease objects, so evaluation
is deterministic across definition versions and resource types.

Comment on lines +355 to +364
The two call sites change source, not shape:

| Call site | Today | After |
|---|---|---|
| `quota.go` `parentPoolUsage` | lists `ResourceQuota` per member namespace, sums `status.used`, keys via `renderedLimitKey` | lists HelmReleases per member namespace, evaluates each, sums in shorthand keys |
| `reconciler.go` `snapshot` | lists all `ResourceQuota`, builds `usedByNS` from `status.used` | builds `usedByNS` from the aggregator |

`renderedLimitKey` and its `rawQuotaKeys` companion are deleted: with both sides in shorthand there is nothing to bridge.

This also removes uncached reads from the admission path. `parentPoolUsage` today deliberately uses the direct watch client `r.w` for ResourceQuotas, with the comment that the aggregated apiserver "must not spin up a cluster-wide ResourceQuota informer just for admission". HelmReleases already have an informer, since `siblingDeclaredQuotas` uses the cached client `r.c` for them, so the new oracle reads from cache where the old one could not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching README:"
fd -a 'README.md' . | sed 's#^\./##' | rg 'design-proposals/tenant-quota-reservation/README.md|tenant-quota-reservation' || true

echo
echo "Relevant proposal section:"
sed -n '1,420p' design-proposals/tenant-quota-reservation/README.md | nl -ba | sed -n '300,375p'

echo
echo "Search reservation goal/source terms:"
rg -n "reserved|deallocate|reservation|HelmRelease|ResourceQuota|snapshot|parentPoolUsage|declared" design-proposals/tenant-quota-reservation/README.md

Repository: cozystack/community

Length of output: 324


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant proposal section:"
sed -n '300,375p' design-proposals/tenant-quota-reservation/README.md | awk '{printf "%7d  %s\n", NR+299, $0}'

echo
echo "Search reservation goal/source terms:"
rg -n "reserved|deallocate|reservation|HelmRelease|ResourceQuota|snapshot|parentPoolUsage|declared|uses|source|api server|inform" design-proposals/tenant-quota-reservation/README.md

Repository: cozystack/community

Length of output: 34916


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "HelmRelease-related references in repository:"
rg -n "HelmRelease|helm release|helm releases|applications|apps\." -S . --glob '!**/.git/**' | head -200

Repository: cozystack/community

Length of output: 38962


Use an authoritative reservation source instead of caching HelmReleases.

The proposal makes apps.cozystack.io custom resources the admission source, but the call-site change reads cached HelmRelease objects and evaluates their values. HelmRelease objects are not the committed source record; Flux reconciliation or conversion can lag admission, and cached reads can hide a missing or stale values projection. Use the same stored source the gate evaluates, or an atomic reservation record, so admission accounting and reservation semantics stay aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 355 - 364,
The proposed admission accounting must not use cached HelmRelease objects or
evaluated values as its authoritative reservation source. Update parentPoolUsage
and snapshot to read the committed apps.cozystack.io custom resources—or an
atomic reservation record—the admission gate evaluates, using the same stored
representation and preserving alignment between reservation semantics and
accounting.

Comment on lines +383 to +391
### 6. What the controller becomes

Feeding `usedByNS` in instance-type units while `EnforcedHard` still writes a `ResourceQuota` enforced against pods would reintroduce the same unit mismatch one level up: the clamp would be computed from reservations and applied to launcher requests. So the controller must stop being an enforcement point.

It can. Once the gate covers every kind, pool sharing between unbounded siblings is already enforced at admission, because every application create in every member namespace is checked against the pool's reservation. `EnforcedHard`, `upsertAllocatedQuota`, `gcAllocatedQuotas` and the `tenant-quota-allocated` object become redundant and are removed.

The controller becomes an observer. It publishes `reserved` against `budget` per pool, and keeps reporting `Overcommitted`, the one case no admission check can prevent, since it arises when a parent lowers its quota after children have already carved out slices.

During the flag-gated coexistence period `EnforcedHard` stays in place so the legacy path is not left without a runtime net; its removal is the last rollout step.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching README:"
git ls-files | rg '(^|/)design-proposals/tenant-quota-reservation/README\.md$|tenant-quota|quota|EnforcedHard|tenant-quota-allocated' || true

echo
echo "Section around proposed lines:"
sed -n '350,410p' design-proposals/tenant-quota-reservation/README.md

echo
echo "Search for EnforcedHard and allocated quota:"
rg -n "EnforcedHard|tenant-quota-allocated|overcommitted|reservation|usedByNS|ResourceQuota|pod[s]?" design-proposals/tenant-quota-reservation/README.md . -S --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200

Repository: cozystack/community

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find candidate implementations:"
git ls-files | rg '(^|/)(reconciler|quota|quot)' | head -n 100 || true

echo
echo "Search EnforcedHard in tracked Go/controller files only:"
rg -n "EnforcedHard|tenant-quota-allocated|upsertAllocatedQuota|gcAllocatedQuotas|usedByNS|resourceQuotas|Enforced|Hard" internal pkg --glob '*.go' -S || true

echo
echo "Search tests mentioning legacy/flag/allocated/enforced:"
rg -n "legacy|EnforcedHard|tenant-quota-allocated|flag|reservation" internal pkg --glob '*_test.go' * --glob '*.go' -S | head -n 200 || true

Repository: cozystack/community

Length of output: 506


Keep EnforcedHard on the legacy path only.

The proposal says EnforcedHard stays during the flag-gated coexistence period so the legacy path keeps a runtime net. Make that explicit: turn off EnforcedHard and tenant-quota-allocated when the reservation oracle is active. Otherwise reservation units become pod-unit ResourceQuota clamps and the mismatch reappears.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 383 - 391,
Clarify the coexistence behavior so the controller disables EnforcedHard and the
tenant-quota-allocated object whenever the reservation oracle is active, while
retaining both only for the legacy path. Update the surrounding rollout
description to make this gating explicit and prevent reservation-unit values
from being applied as pod-unit ResourceQuota clamps.

Comment on lines +393 to +404
### 7. The `ResourceQuota` becomes a capacity guard

The chart-rendered `tenant-quota` is kept, deliberately loose, for two reasons that have nothing to do with the tenant contract:

- The `LimitRange` providing default container requests is rendered under the same `if .Values.resourceQuotas` guard. Dropping the quota would drop the defaults.
- `AutoResourceLimitsGate` only sets limits on virt-launcher pods when the namespace has a quota constraining `limits.*`. Dropping the quota would silently change the QoS class of every VM on the platform.

It cannot be made exact. An exact overhead allowance requires knowing how many VMs the tenant runs, and the quota is rendered by a Helm chart that does not know. A second quota object cannot compensate either, because Kubernetes applies the **most restrictive** quota in a namespace: an additional object can only tighten, never loosen.

So the guard is rendered from `resourceQuotas` multiplied by a wide, platform-configurable factor, and documented as a guard rather than a contract. Its imprecision is the point: it exists to stop an unbounded runaway and to keep `AutoResourceLimitsGate` armed, not to decide what a tenant may claim.

The inflation factor therefore changes role rather than disappearing. It moves from "the tenant contract must be falsified", where no value is correct, to "the guard is deliberately slack", where imprecision is the desired property. That requalification is what makes `--tenant-quota-buffer-percent` obsolete, not its literal deletion.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="design-proposals/tenant-quota-reservation/README.md"
if [ ! -f "$file" ]; then
  echo "file not found: $file"
  git ls-files | rg 'tenant-quota-reservation|README\.md|quota|ResourceQuota|AutoResourceLimitsGate' || true
else
  echo "--- file exists: $file ($(wc -l < "$file") lines) ---"
  echo "--- around lines 80-120 ---"
  sed -n '80,120p' "$file" | nl -ba -v80
  echo "--- around lines 360-420 ---"
  sed -n '360,420p' "$file" | nl -ba -v360
  echo "--- key terms "---
  rg -n "ResourceQuota|tenant-quota|AutoResourceLimitsGate|--tenant-quota-buffer-percent|reservation contract|exact|status.used|quota" "$file" -C 2
fi

Repository: cozystack/community

Length of output: 311


🌐 Web query:

Kubernetes ResourceQuota admission status.used rejects pod after update quota exceeded

💡 Result:

In Kubernetes, the ResourceQuota admission controller prevents resource consumption from exceeding defined namespace limits by rejecting requests that would violate these constraints [1][2]. If a creation or update request would cause a namespace to exceed its ResourceQuota, the Kubernetes API server will reject that request with an HTTP 403 Forbidden error and a message detailing the violated constraint [1]. The following key points summarize how ResourceQuota interacts with pod lifecycle events and updates: Admission and Rejection: The ResourceQuota plugin introspects incoming requests at the API server level [2]. If accepting a new pod or an update to an existing pod (such as a resource resize) would cause the total usage to exceed the hard limit set in the ResourceQuota, the admission controller blocks the request before it is committed to etcd [2][3]. Updates and Resizing: With the support for in-place pod resource updates, the ResourceQuota admission controller is designed to validate that the requested change (e.g., increasing CPU/memory requests) does not violate the namespace's current quota [3][4]. If an update request exceeds the available quota, the admission controller will reject the update [4]. Existing Resources and Quota Changes: Changes to ResourceQuota limits or the deletion of pods are handled differently. If you reduce the quota limits such that existing pods already exceed the new, lower limit, the existing pods are not affected or evicted; ResourceQuota admission control only enforces limits on new creations or updates to resources [5]. Consistency: ResourceQuota is designed to be eventually consistent [6]. The API server performs atomic quota reservation during the admission phase, while a background ResourceQuota controller periodically synchronizes the status field to reflect actual usage, which is necessary for reconciling changes after deletions [2][7]. Because of this design, there can be brief periods where observed usage and quota status might slightly diverge, but the admission controller remains the primary mechanism for preventing new violations [6][7]. Top results: [1][2][3][4]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="design-proposals/tenant-quota-reservation/README.md"
sed -n '100,115p' "$file" > /tmp/section_guarantees.txt
sed -n '386,404p' "$file" > /tmp/section_guard.txt
echo "--- lines 100-115 ---"
awk '{printf "%4d  %s\n", NR+100, $0}' /tmp/section_guarantees.txt
echo "--- lines 386-404 ---"
awk '{printf "%4d  %s\n", NR+386, $0}' /tmp/section_guard.txt
echo "--- terms around quota admission/status/guard/contract ---"
grep -nEi "status\.used|ResourceQuota|quota|AutoResourceLimitsGate|reservation contract|exact|garant|reject|rejects|deny|block|contract" "$file"

Repository: cozystack/community

Length of output: 33433


🌐 Web query:

Kubernetes ResourceQuota does not support in place update request changes update resource limit quota check on status.used

💡 Result:

In Kubernetes, ResourceQuota status.used is managed as an eventually consistent system [1][2], and it does not always update instantaneously in response to resource changes [1][3]. The system relies on a combination of admission controllers and a background reconciliation loop to maintain these values [2][4]. 1. Admission Control vs. Controller Reconciliation: When a resource (like a Pod) is created or updated, the ResourceQuota admission controller attempts to intercept the request and increment the used quota values immediately [1][4]. However, because this is an eventually consistent system, there are scenarios where the status.used may not perfectly reflect the actual state of the cluster at every microsecond [1][2]. 2. Reconciliation Loop: The ResourceQuota controller periodically performs a full re-calculation (reconciliation) of usage across the namespace to correct any mismatches that occur [1][2]. If a change happens that the admission controller fails to account for—or if a transient error causes a discrepancy—the background controller eventually syncs the status.used to match the actual resource usage in the cluster [2][3]. 3. In-Place Updates: When features like In-Place Pod Vertical Scaling are used, the quota controller is designed to monitor pod status updates (specifically when container resource status is populated or changed) to ensure that the ResourceQuota.status.used remains accurate [5]. Without this tracking, there could be delays or inaccuracies in reporting usage as pods resize [5]. 4. Performance Considerations: While users might desire perfectly synchronous updates, the design of the quota system avoids complex distributed locking or atomic synchronous updates to every involved component to maintain performance [1]. Consequently, if you observe a temporary mismatch between your actual resources and the status.used field, it is typically resolved by the controller during its next sync period [1][2]. If you are seeing persistent inaccuracies that do not resolve after a reasonable amount of time, it is generally considered a drift in the controller's observed state, which the background synchronization loop is explicitly designed to rectify [2][3].

Citations:


Make the capacity guard explicit and avoid contradicting the tenant contract.

The current goal states a tenant charged exactly to its quota can start all workloads, but tenant-quota is still a Kubernetes ResourceQuota that can reject pods via status.used. Keep the loose guard as a separate operational limit, or wire LimitRange and AutoResourceLimitsGate independently, so the exact reservation admission result is not mixed with Kubernetes pod quota admission.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 393 - 404,
Update the proposal’s capacity-guard design around ResourceQuota, LimitRange,
and AutoResourceLimitsGate so the loose operational guard is explicitly separate
from the exact tenant reservation contract. Ensure the documented admission
behavior does not claim that a tenant charged exactly to its quota can always
start workloads when Kubernetes ResourceQuota may reject pods based on
status.used; either retain the guard solely as an independent operational limit
or describe independently wired LimitRange and AutoResourceLimitsGate behavior.

Comment on lines +426 to +432
## Security

- **No new tenant-supplied input.** The reservation is computed from values that already pass the kind's OpenAPI schema. Tenants gain no new field.
- **`spec.reservation` is platform-authored.** It lives on a cluster-scoped `ApplicationDefinition`, which tenants cannot write.
- **A missing or wrong reservation block under-charges a tenant**, which is a quota-escalation vector: an application kind that reserves nothing is free. This is the main new risk. It is mitigated by a completeness test asserting that every in-tree application kind has a `reservation` block, gating each rollout step, and by the fact that an absent block is a visible `0` in the pool's `reserved` report rather than a silent pass.
- **The gate must fail closed on the object being written.** Today `siblingDeclaredQuotas` deliberately skips siblings whose values do not parse, with a warning, so that "a malformed sibling must not block an unrelated tenant write". That fail-open choice is right for a sibling and wrong for the object under admission: applied to reservation it would make an unparseable custom resource free. The proposed behavior is to reject when the object being written cannot be evaluated, and to warn plus set a pool condition when another object cannot be, so the under-count is surfaced instead of hidden.
- **RBAC surface is unchanged.** The gate reads HelmReleases with the apiserver's existing service account, as it already does for siblings and pool usage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate README and related proposal files =="
git ls-files | rg 'design-proposals/tenant-quota-reservation|tenant-quota-reservation|quota.*reservation|reservation'

echo
echo "== relevant README excerpt =="
sed -n '380,450p' design-proposals/tenant-quota-reservation/README.md 2>/dev/null || true

echo
echo "== search for reservation handling / siblingDeclaredQuotas / in-tree tests =="
rg -n "spec\.reservation|reservation|siblingDeclaredQuotas|completeness|in-tree|in tree|out-of-tree|unevaluable|skip|warning|pool condition|reserved" design-proposals/tenant-quota-reservation -S || true

echo
echo "== list all files under proposal =="
git ls-files design-proposals/tenant-quota-reservation 2>/dev/null | sort

Repository: cozystack/community

Length of output: 31737


🌐 Web query:

tenant quota reservation out-of-tree application kinds spec.reservation in-tree completeness test proposal

💡 Result:

In the context of Kubernetes and broader distributed system architecture, there is no single unified standard named spec.reservation for tenant quota reservations, nor is there a widely documented "completeness test" specifically tied to this phrase. The terminology likely originates from one of two contexts: 1. Distributed Systems Design Patterns: In complex systems (such as agent loops or OpenStack Neutron), "reservations" are frequently used to handle pre-commit accounting—where a system reserves resources (e.g., tokens, CPU, or memory) against a tenant quota before an operation is dispatched [1][2]. A "completeness test" in this context typically refers to the validation logic required to ensure that a reservation request, once reconciled or fulfilled, does not result in a state that violates total tenant capacity or exceeds the requested "worst-case" budget [1][2]. 2. Kubernetes Out-of-Tree Integrations: The Kubernetes ecosystem (notably via KEP-3521 and projects like Kueue) has long grappled with the limitations of the in-tree ResourceQuota system [3][4][5]. Efforts to support "out-of-tree" quota solutions involve creating extensible, generic quota APIs that allow external controllers (like batch schedulers) to manage resources that the core Kubernetes scheduler cannot natively track (e.g., licenses or external cluster-scoped assets) [4][6][5]. In these designs, "reservation" concepts are often introduced to bridge the gap between namespace-scoped in-tree ResourceQuotas and external, tenant-wide policy enforcement [4]. If you are encountering these terms in a specific proposal or codebase, it is likely an internal architectural requirement for ensuring that an out-of-tree admission controller or quota manager correctly accounts for resource claims across both standard in-tree resources and custom application kinds [4][7]. Top results: [1], [2], [4], [6], [5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== outline for all files =="
git ls-files | sort

echo
echo "== reservation references across repo =="
rg -n "siblingDeclaredQuotas|parseDeclaredQuotas|spec\.reservation|reservation|unparseable|malformed|pool condition|complete|completeness" -S \
  --glob '!design-proposals/tenant-quota-reservation/README.md' \
  --glob '!**/vendor/**' \
  --glob '!**/.git/**' . \
  | head -n 200

echo
echo "== relevant docs for deployment/package model =="
git ls-files | rg 'packages|internal|pkg|README|manifest|openapi|application-definition|ApplicationDefinition' | head -n 200

Repository: cozystack/community

Length of output: 3439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== full relevant reservation safety section =="
sed -n '152,312p' design-proposals/tenant-quota-reservation/README.md

echo
echo "== full rollout, failing and edge cases, testing sections =="
sed -n '428,456p' design-proposals/tenant-quota-reservation/README.md
sed -n '338,400p' design-proposals/tenant-quota-reservation/README.md

Repository: cozystack/community

Length of output: 17035


Fail closed for missing or unevaluable existing reservations.

The completeness test pins only in-tree kinds. An out-of-tree ApplicationDefinition can omit spec.reservation and reserve zero, and a pool condition does not prevent another write from consuming falsely available headroom. Require an explicit, reviewed zero-reservation exemption. For unevaluable existing applications, block pending reservations or retain the last known reservation until evaluation succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 426 - 432,
The reservation admission design must fail closed for existing unevaluable or
missing reservations, including out-of-tree ApplicationDefinitions. Update the
reservation evaluation and admission behavior so missing reservations require an
explicit reviewed zero-reservation exemption, while unevaluable existing
applications either block pending reservations or retain their last known
reservation until evaluation succeeds; do not rely solely on pool conditions or
in-tree completeness tests.

Comment on lines +434 to +440
## Failure and edge cases

- Application kind with no `reservation` block → reserves nothing; the completeness test prevents this shipping for in-tree kinds.
- `instanceType` names a `VirtualMachineClusterInstancetype` that does not exist → rejected at admission with the resolver's error, instead of later by the chart's `lookup` failure in `templates/vm.yaml`.
- Both `instanceType` and `resources` set → the evaluator charges `resources`, matching the precedence the `kubernetes` chart documents on `nodeGroups[].resources`. Note that `packages/apps/vm-instance` does not currently omit the instancetype in that case the way the `kubernetes` chart does, so the rendered `VirtualMachine` carries both and KubeVirt cannot reconcile them; tightening that chart is out of scope here but worth a separate fix, and the reservation charges the same number either way.
- Explicit `resources` with no `instanceType` → charged from `resources`, matching `virtual-machine.domainResources` in `packages/apps/vm-instance/templates/_helpers.tpl`.
- **`nodeGroups: {}` on a `kubernetes` application, while the parent chart still owns pools** → the chart emits a default `md0` group with `maxReplicas: 10`, so a literal read charges nothing for a cluster that can autoscale to ten workers. Covered by the per-kind unit test and resolved for good by [#3315](https://github.com/cozystack/cozystack/pull/3315); see [§2](#2-specreservation-on-applicationdefinition).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Repository files matching vm/instance/quota names =="
git ls-files | rg -n 'design-proposals/tenant-quota-reservation/README.md|vm-instance|VirtualMachine|instancetype|reservation|nodeGroups|resources' | head -200

echo
echo "== Proposal relevant lines =="
sed -n '410,455p' design-proposals/tenant-quota-reservation/README.md

echo
echo "== Search evaluator/chart precedence handling =="
rg -n "resources|instanceType|nodeGroups|VirtualMachineClusterInstancetype|virtual-machine.domainResources|kubernetes chart|omit the instancetype|reservation|charges|precedence" design-proposals/tenant-quota-reservation README.md packages/apps -S | head -250

Repository: cozystack/community

Length of output: 31792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Remote package files =="
curl -fsSL 'https://raw.githubusercontent.com/cozystack/cozystack/main/packages/apps/vm-instance/templates/vm.yaml' -o /tmp/vm.yaml
curl -fsSL 'https://raw.githubusercontent.com/cozystack/cozystack/main/packages/apps/vm-instance/templates/_helpers.tpl' -o /tmp/helpers.tpl
curl -fsSL 'https://raw.githubusercontent.com/cozystack/cozystack/main/packages/apps/vm-instance/values.yaml' -o /tmp/defaults.yaml

echo "== vm.yaml vm block =="
python3 - <<'PY'
from pathlib import Path
p=Path('/tmp/vm.yaml')
for i,line in enumerate(p.read_text().splitlines(),1):
    if 'VirtualMachine' in line or 'instanceType' in line or 'domain' in line:
        lo=max(1,i-8); hi=min(i+16,p.read_text().count('\n')+1)
        print(f'--- {p}:{lo}-{hi}')
        for j,l in enumerate(p.read_text().splitlines()[lo-1:hi],lo):
            print(f'{j}: {l}')
PY

echo "== helpers relevant vm/resources sections =="
python3 - <<'PY'
from pathlib import Path
text=Path('/tmp/helpers.tpl').read_text()
for term in ['virtual-machine.domainResources', 'virtual-machine.instanceType', 'virtual-machine.resources', 'VmResources', 'domainResources', 'instanceType', 'nodeGroups']:
    idx=text.find(term)
    if idx!=-1:
        start=max(0,text.rfind('\n',0,idx-200)); end=min(len(text),text.find('\n',idx+500))
        print(f'--- around {term}')
        print(text[start:end])
PY

echo "== vm defaults =="
python3 - <<'PY'
from pathlib import Path
import yaml, re
text=Path('/tmp/defaults.yaml').read_text()
lines=text.splitlines()
for i,line in enumerate(lines,1):
    if re.search(r'instanceType:|resources:|namePreset:|resourcesPreset:', line):
        print(f'{i}: {line}')
PY

echo
echo "== Check if KubeVirt source docs this specific precedence/rejection in repo =="
tmpdir=$(mktemp -d)
git clone --depth 1 --no-single-branch https://github.com/kubevirt/kubevirt.git "$tmpdir" >/tmp/clone.log 2>&1
rg -n "instanceType|VirtualMachine.*instanceType|cannot reconcile|rejects|instanceType.*resources|domain resources|domainResources|resourcesPreset|resourcesPresetMap" "$tmpdir" -S \
  --glob '*.go' --glob '*.md' | head -300 || true

Repository: cozystack/community

Length of output: 16135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Fetch KubeVirt source snippets =="
curl -fsSL 'https://raw.githubusercontent.com/kubevirt/kubevirt/main/pkg/virt-api/webhooks/mutating-webhook/webhooks/virtualmachine/virtualmachine_create_admit.go' -o /tmp/vm_create_admit.go
curl -fsSL 'https://raw.githubusercontent.com/kubevirt/kubevirt/main/pkg/virt-controller/services/renderresources/renderresources.go' -o /tmp/renderresources.go

echo
echo "== Search KubeVirt instanceType/resources precedence/rejection =="
rg -n "instanceType|resourcesPreset|resourcesPresetMap|domainResources|explicit resources|resources.*memory|spec\.domain\.resources|instancetype" /tmp/vm_create_admit.go /tmp/renderresources.go -S

echo
echo "== Fetch renderresources around relevant lines =="
python3 - <<'PY'
from pathlib import Path
for name in ['/tmp/vm_create_admit.go', '/tmp/renderresources.go']:
    text=Path(name).read_text()
    lines=text.splitlines()
    matches=[]
    q='resourcesPreset'
    for i,l in enumerate(lines,1):
        if q in l:
            lo=max(1,i-18); hi=min(len(lines),i+35)
            print(f'--- {name}:{lo}-{hi}')
            for j,line in enumerate(lines[lo-1:hi],lo):
                print(f'{j}: {line}')
PY

Repository: cozystack/community

Length of output: 245


🌐 Web query:

KubeVirt VirtualMachine instanceType resourcesPreset explicit domain resources precedence

💡 Result:

In KubeVirt, the precedence and interaction of resources are determined by whether you are using the modern InstanceType/Preference framework or the older, deprecated VirtualMachineInstancePreset mechanism [1]. InstanceType Precedence: When using VirtualMachineInstancetype (or VirtualMachineClusterInstancetype), the resources defined within the instance type are mandatory [2][3]. Anything defined in the instance type cannot be overridden within the VirtualMachine spec [2][1]. Specifically, because CPU and memory are required attributes of an instance type, if you attempt to define these resources within the underlying VirtualMachine, a conflict will occur, and the creation of the VM will be rejected [2][4]. VirtualMachinePreference Precedence: In contrast, VirtualMachinePreference (or VirtualMachineClusterPreference) defines preferred values for attributes [1][3]. Unlike instance types, preferences only provide defaults and can be explicitly overridden by values provided by the user in the VirtualMachine spec [1][3]. Deprecated VirtualMachineInstancePreset: The VirtualMachineInstancePreset CRD is deprecated [1]. It operated using a conflict resolution strategy similar to Kubernetes PodPresets [5]. If a setting (such as CPU or memory) was present in both the VirtualMachineInstance and the Preset, KubeVirt would check for identity [5]. If the values differed, it would result in a conflict error, and the VirtualMachineInstance would fail to enter a running state [5]. General Resource Precedence: When defining resources directly in the VirtualMachine spec (outside of the InstanceType framework), if both spec.domain.memory.guest and spec.domain.resources.requests.memory are set, the memory requests take precedence [6].

Citations:


🌐 Web query:

VirtualMachine resourcesPreset CPU memory conflicts with custom spec.domain.resources instanceType KubeVirt

💡 Result:

In KubeVirt, conflicts between resource definitions arise primarily when using Instance Types, which are the recommended mechanism for defining VM resources [1][2]. If you reference an Instance Type (via VirtualMachineInstancetype or VirtualMachineClusterInstancetype), any CPU or Memory resources defined within that Instance Type are mandatory [3][2]. Consequently, if you attempt to define conflicting CPU or Memory requests within the VirtualMachine's own spec.domain.resources, the request will be rejected by the KubeVirt API [3][1][4]. You cannot override resources encapsulated by an Instance Type [3][2]. Regarding older mechanisms: - VirtualMachineInstancePresets: These are deprecated in favor of Instance Types [1]. When used, if a Preset and the VirtualMachine define different settings for the same domain spec elements, it results in a merge conflict [5][6]. KubeVirt will typically generate a warning event, and the VirtualMachine may enter a Failed state rather than applying the conflicting configuration [5][7]. For standard resource management without Instance Types or Presets: - KubeVirt requires memory to be specified in either spec.domain.memory.guest or spec.domain.resources.requests.memory (with requests taking precedence) [8]. - For CPU, you should not specify both spec.domain.cpu and spec.domain.resources.[requests/limits].cpu; consistent definitions are required, or the configuration will be rejected [9]. To resolve conflicts when using Instance Types, remove the manual resource definitions from your VirtualMachine's spec.domain.resources, as the Instance Type is intended to be the single source of truth for these attributes [3][2]. If you require different resource profiles, you should reference a different Instance Type [2].

Citations:


Align evaluator and vm-instance precedence for conflicting fields.

When instanceType and explicit resources are both set, the evaluator charges explicit resources; vm-instance still renders both the instance-type ref and explicit domain.resources. KubeVirt treats explicit resources as conflicting against an instance type, so the VM is rejected after admission and the workload does not run. Reconcile reservation sizing with chart rendering by rejecting the conflict first or omitting instance type when explicit resources take precedence before enabling reservation enforcement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` around lines 434 - 440,
Align the evaluator and vm-instance rendering for applications with both
instanceType and explicit resources. Update the relevant vm-instance chart logic
to omit the instance-type reference when resources take precedence, or reject
the conflicting configuration before reservation enforcement; ensure admitted
VMs do not render both fields while reservation sizing remains based on explicit
resources.

- Explicit `resources` with no `instanceType` → charged from `resources`, matching `virtual-machine.domainResources` in `packages/apps/vm-instance/templates/_helpers.tpl`.
- **`nodeGroups: {}` on a `kubernetes` application, while the parent chart still owns pools** → the chart emits a default `md0` group with `maxReplicas: 10`, so a literal read charges nothing for a cluster that can autoscale to ten workers. Covered by the per-kind unit test and resolved for good by [#3315](https://github.com/cozystack/cozystack/pull/3315); see [§2](#2-specreservation-on-applicationdefinition).
- **A `KubernetesNodes` pool whose name collides with a `nodeGroup` still owned by the parent** → the render already fails on ownership conflict, as its own schema documents. Reservation would have charged the pool twice, once per owner, so failing early is the correct outcome and no special case is needed.
- Concurrent creates in different member namespaces of one pool → both may pass, overshooting by at most one application. The controller reports the overshoot; nothing is evicted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Correct the concurrent-admission overshoot bound.

Without per-pool serialization or optimistic concurrency, any number of requests can observe the same reservation snapshot and pass. Overshoot can equal the sum of all accepted reservations, not one application. Updates can also introduce large deltas.

Use an atomic reservation mechanism, or document best-effort semantics and define how overshoot affects billing, reporting, and tenant guarantees.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` at line 442, Update the
concurrent-admission behavior in the tenant quota reservation proposal to avoid
claiming the overshoot is limited to one application. Specify an atomic
reservation mechanism with per-pool serialization or optimistic concurrency, or
explicitly document best-effort admission semantics and define the resulting
billing, reporting, and tenant-guarantee treatment.

- Concurrent creates in different member namespaces of one pool → both may pass, overshooting by at most one application. The controller reports the overshoot; nothing is evicted.
- Parent lowers its quota below existing carve-outs → `Overcommitted` reports it, as today. No retroactive enforcement.
- `vm-disk` resized upward → the `Update` path charges the delta; a downward resize releases it.
- Autoscaler (DHA or cluster-autoscaler) raises `replicas` past the pool budget → the `Update` is rejected and the autoscaler surfaces the failure, rather than the scale-up silently failing at pod admission.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate README =="
fd -a 'README.md' . | sed 's#^\./##' | rg 'tenant|quota|reservation|design-proposals' || true

echo "== Relevant lines =="
if [ -f design-proposals/tenant-quota-reservation/README.md ]; then
  sed -n '420,485p' design-proposals/tenant-quota-reservation/README.md | nl -ba -v420
fi

echo "== Search for cluster-autoscaler, autoscaler, MachineDeployment text =="
rg -n "cluster-autoscaler|autoscaler|MachineDeployment|maxReplicas|pre-reserved|Update|admission" design-proposals/tenant-q* tenant* -S 2>/dev/null | head -200

Repository: cozystack/community

Length of output: 843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant lines =="
sed -n '420,485p' design-proposals/tenant-quota-reservation/README.md

echo "== Search for autoscaler/MachineDeployment wording =="
rg -n "cluster-autoscaler|autoscaler|MachineDeployment|maxReplicas|pre-reserved|Update|admission" design-proposals/tenant-quota-reservation/README.md design-proposals -S | head -240

Repository: cozystack/community

Length of output: 50375


🌐 Web query:

CAPI cluster-autoscaler MachineDeployment replicas spec minMaxReplicas changes live MachineDeployment without updating Application CR

💡 Result:

When using the Kubernetes Cluster Autoscaler with Cluster API (CAPI) MachineDeployments, how the replicas field is managed depends on whether you are using ClusterClass and Managed Topologies [1][2]. If you are using ClusterClass/Managed Topologies, you must leave the replicas field unset in the Cluster definition (specifically under spec.topology.workers.machineDeployments[].replicas) to allow the Cluster Autoscaler to take control [1][2]. If this field is set in the Cluster definition, the CAPI topology controller will enforce that value on the MachineDeployment, which can interfere with the autoscaler and lead to unpredictable scaling behavior [3][4][5]. When the replicas field is left unset in the Cluster topology: 1. The topology controller does not enforce a value on the MachineDeployment replicas field [5]. 2. The Cluster Autoscaler is then permitted to modify the MachineDeployment replicas field directly [5]. 3. The MachineDeployment uses specific defaulting logic for its replicas field when it is created or updated, which considers the autoscaler's min/max size annotations [6][7]. For example, if the autoscaler annotations (e.g., cluster-autoscaler.kubernetes.io/cluster-api-machine-deployment-name-min-size and max-size) are present, the system attempts to maintain the replica count within that range during creation or updates [6][3][7]. In summary, you should not update the Application CR (Cluster object) replicas field to scale the cluster; instead, you should leave it unset in the Cluster definition and ensure the required autoscaler annotations are applied to the MachineDeployment [1][2][5]. The Cluster Autoscaler will then manage the live MachineDeployment replicas field automatically [5].

Citations:


Split the cluster-autoscaler scale-up behavior from DHA.

Line 445 says cluster-autoscaler scale-ups are rejected by the Update gate, but line 474 says cluster-autoscaler scales the live MachineDeployment without updating the application CR. If maxReplicas is pre-reserved, state that coverage instead of claiming Update rejection. Otherwise, document the required admission/controller path for live MachineDeployment scaling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-proposals/tenant-quota-reservation/README.md` at line 445, Update the
autoscaler behavior statement in the quota reservation proposal to distinguish
cluster-autoscaler from DHA: describe cluster-autoscaler scale-ups as covered by
the pre-reserved maxReplicas capacity if that is the intended design, or
document the admission/controller enforcement path for live MachineDeployment
scaling instead of claiming the Update gate rejects it.

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.

1 participant