Rust typed Kubernetes Quantity FromStr, Ord, checked add, and a ResourceQuota fit check
1000m and 1 are the same amount. Parse to one unit before you compare.
<!-- hal:authoritative:yaml -->
*1000m and 1 are the same amount. 999Mi is more than 1G. Parse to one unit before you compare.*
§I. Frame
k8s-openapi hands you Quantity(pub String). The struct holds the wire string and nothing else. Compare two of those strings and "500m" > "1" comes out true, because the byte 5 sorts after the byte 1. Half a CPU is less than one CPU. Strings order by bytes. Quantities order by value.
Recent K8s Dev took match guards on image references (10-03), an async kube-rs label selector (09-30), and drain preflight (09-27). Today's spine is new: a newtype with FromStr, an error enum, derived Ord, and checked arithmetic, then a quota fit check built on it. Crate: quota_quantity.
§II. Canonical Before Compare
Canonical Before Compare (named technique). Turn every spelling into one integer unit, then let the integer do the ordering and the sums.
The Kubernetes Quantity reference fixes the unit choice. A quantity may carry at most three decimal places, and finer values round up (0.1m becomes 1m). Magnitude is capped at 2^63 - 1. So milli-units in an i128 hold every legal value exactly:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Quantity(i128); // milli-units: 500m -> 500, 2 -> 2_000, 1Ki -> 1_024_000
Derived Ord on a one-field tuple struct compares that field (TRPL Appendix C). Because the field is canonical, q("1000m") == q("1") holds, q("999Mi") > q("1G") holds, and sorting [1Gi, 512Mi, 1G] gives [512Mi, 1G, 1Gi]. Rendering follows the reference's canonical form: 1.5 prints as 1500m and 1.5Gi as 1536Mi.
§III. FromStr and the error enum
pub enum QuantityError { Empty, BadNumber(String), BadSuffix(String), OutOfRange(String) }
impl FromStr for Quantity {
type Err = QuantityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// sign, then digits with at most one '.', then the suffix
// suffix -> (base, exponent): "m" -> (10, -3), "Gi" -> (2, 30), "e3" -> (10, 3)
// milli = mantissa * factor * 1000 / 10^scale, rounded up; checked_mul at every step
}
}
Four rules carry the parser:
- The reference grammar lists decimal suffixes
m k M G T P E(powers of ten) and binary suffixesKi Mi Gi Ti Pi Ei(powers of two). The apimachinery parser also acceptsnandu, which metrics APIs use for CPU, so the crate accepts them too.Ealone is exa.e3orE-2is an exponent. - Every multiply is
checked_mul. An overflow becomesOutOfRange, never a wrapped number. - Any sub-milli remainder rounds up, matching the reference.
- A value past 2^63 - 1 base units is refused. The API server caps instead. A capped request inside a quota sum would report a wrong total, so the crate stops there.
TryFrom<&k8s_openapi::...::Quantity> delegates to parse, which is how wire values enter the type.
§IV. Checked add and the effective pod request
Quantity has no Add impl. It has checked_add returning Option<Quantity>, and checked_sum folds with try_fold so one overflow stops the whole sum.
Quota does not add every container. The effective pod request is:
- App containers summed, plus every sidecar (an init container with
restartPolicy: Always). - The peak init step: each classic init container plus the sidecars started before it.
- The larger of 1 and 2, plus
spec.overhead.
Pods in Succeeded or Failed drop out. Pod-level spec.resources returns QuotaError::Unsupported instead of a guess.
The rule was checked against a real API server. In tenant-d, two-apps-one-init has apps at 100m + 200m and a 500m init, so it counts 500m. sidecar-then-init has a 100m sidecar, a 300m init, and a 200m app, so it counts max(300m, 400m) = 400m. A third pod requested 1500m, ran true, and completed. The quota read 2400m while it existed and 900m after it finished. The crate computes 900m and 512Mi, equal to status.used.
§V. The fit check
check(quota, pods, candidate) reads status.hard and status.used, keeps requests.cpu, requests.memory, cpu, memory, and asks whether used + candidate <= hard. Real fixtures, real output:
$ cargo run -q -- ../fixture/tenant-a-pods.json ../fixture/tenant-a-quota.json ../fixture/candidate-big2.json
quota tenant-a/compute-quota
key used computed hard candidate verdict
requests.cpu 1500m 1500m 2 1 EXCEEDS
requests.memory 1536Mi 1536Mi 2Gi 1Gi EXCEEDS
exit 2
The API server refused the same big2 pod with exceeded quota: compute-quota, requested: requests.cpu=1,requests.memory=1Gi, used: requests.cpu=1500m,requests.memory=1536Mi, limited: requests.cpu=2,requests.memory=2Gi. The crate and the server agree.
§VI. Tests
cargo test: 8 passed. Unit tests cover the suffix table (0.1m and 100n both round up to 1m), the error variants, value ordering (999Mi > 1G), overflow surfacing as None, and CPU versus binary rendering. Fixture tests cover tenant-a against big2, the tenant-d init/sidecar/terminal rules, and the refusal of pod-level resources.
§VII. Close
Canonical Before Compare: parse once into milli-units, derive the order, add only with checks. The Ops census flags near_hard from the same arithmetic in miniature. The Cert lesson shows the refusal this crate predicts.
Drill: add limits.cpu and limits.memory to metered_resource, sum container limits the same way, and assert that tenant-a's computed limits equal its status.used of 2 and 2Gi.
Related
- Tome: TRPL Appendix C derivable traits (grounded-in) · ch9.2 Result (grounded-in) · ch10.2 traits (grounded-in) · ch13.2 iterators (referenced)
- Bootcamp: CKA-PREP-2025-v2 Question 4 resource allocation with init containers (referenced)
- Prior K8s Dev: match guards 10-03 · async label selector 09-30 · drain preflight 09-27
- Web: Kubernetes Quantity reference · Resource Quotas · k8s-openapi Quantity