Skip to content

Commit 7576be9

Browse files
sajjadghfSajjad Ghafarian
authored andcommitted
feat(utils): add semantic ResourceQuota comparison utility
Adds kubernetes/utils/resource_quota.py with get_resource_list_diff(), resource_quotas_equal(), and compare_resource_quotas() backed by the existing parse_quantity() helper. Mirrors the behaviour of equality.Semantic.DeepEqual and resource.Quantity.Cmp() from k8s.io/apimachinery so that semantically equal quantities expressed in different units (e.g. '1' vs '1000m', '1Gi' vs '1073741824') compare correctly. resource_quotas_equal compares the full spec (hard, scopes, scope_selector) to avoid false positives when only hard limits match. get_resource_list_diff accepts Optional[Mapping[str, str]] to handle None values returned by the API client without caller null-checks.
1 parent f8c3ba2 commit 7576be9

4 files changed

Lines changed: 893 additions & 0 deletions

File tree

examples/resource_quota_compare.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env python
2+
# Copyright 2024 The Kubernetes Authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
Example: Semantic ResourceQuota comparison using the Python client.
18+
19+
This example mirrors a kubebuilder operator pattern in Go where two
20+
ResourceQuota objects are fetched from etcd and compared using
21+
``equality.Semantic.DeepEqual`` / ``resource.Quantity.Cmp()``:
22+
23+
https://github.com/kubernetes/apimachinery/blob/master/pkg/api/equality/semantic.go
24+
https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go
25+
26+
The Python implementation delegates all unit normalisation to
27+
``kubernetes.utils.quantity.parse_quantity`` so that semantically equal
28+
quantities expressed in different units (e.g. ``"1Gi"`` vs
29+
``"1073741824"``, or ``"1000m"`` vs ``"1"``) compare correctly without
30+
any manual conversion.
31+
32+
Prerequisites:
33+
- A running Kubernetes cluster accessible via kubeconfig or in-cluster
34+
service account.
35+
- At least one ResourceQuota in each namespace referenced below.
36+
- The ``kubernetes`` Python package installed.
37+
38+
Usage::
39+
40+
python examples/resource_quota_compare.py
41+
"""
42+
43+
from kubernetes import client, config
44+
from kubernetes.utils.resource_quota import (
45+
ResourceChangeKind,
46+
compare_resource_quotas,
47+
get_resource_list_diff,
48+
resource_quotas_equal,
49+
)
50+
51+
52+
# ---------------------------------------------------------------------------
53+
# Configuration — adjust to match your cluster setup
54+
# ---------------------------------------------------------------------------
55+
56+
BASELINE_QUOTA_NAME = "baseline-quota"
57+
BASELINE_NAMESPACE = "default"
58+
59+
TARGET_QUOTA_NAME = "team-quota"
60+
TARGET_NAMESPACE = "team-ns"
61+
62+
63+
# ---------------------------------------------------------------------------
64+
# Helper printers
65+
# ---------------------------------------------------------------------------
66+
67+
_KIND_SYMBOLS = {
68+
ResourceChangeKind.ADDED: "➕",
69+
ResourceChangeKind.REMOVED: "➖",
70+
ResourceChangeKind.MODIFIED: "✏️ ",
71+
ResourceChangeKind.UNCHANGED: "✅",
72+
}
73+
74+
75+
def print_section(title: str) -> None:
76+
print("\n" + "=" * 60)
77+
print(title)
78+
print("=" * 60)
79+
80+
81+
def print_diffs(diffs) -> None:
82+
if not diffs:
83+
print(" (no differences)")
84+
return
85+
for diff in diffs:
86+
symbol = _KIND_SYMBOLS.get(diff.kind, "?")
87+
print(f" {symbol} {diff}")
88+
89+
90+
# ---------------------------------------------------------------------------
91+
# Demo 1 — Compare two quotas fetched from the cluster
92+
# ---------------------------------------------------------------------------
93+
94+
def demo_cluster_comparison(api_client: client.ApiClient) -> None:
95+
"""Fetch two ResourceQuotas from the cluster and print their differences."""
96+
print_section("Demo 1 — Cluster ResourceQuota comparison")
97+
98+
try:
99+
diffs = compare_resource_quotas(
100+
api_client,
101+
name_a=BASELINE_QUOTA_NAME,
102+
namespace_a=BASELINE_NAMESPACE,
103+
name_b=TARGET_QUOTA_NAME,
104+
namespace_b=TARGET_NAMESPACE,
105+
)
106+
if diffs:
107+
print(
108+
f" '{BASELINE_QUOTA_NAME}' ({BASELINE_NAMESPACE}) differs from "
109+
f"'{TARGET_QUOTA_NAME}' ({TARGET_NAMESPACE}):")
110+
print_diffs(diffs)
111+
else:
112+
print(
113+
f" '{BASELINE_QUOTA_NAME}' and '{TARGET_QUOTA_NAME}' "
114+
"are semantically identical."
115+
)
116+
except client.exceptions.ApiException as exc:
117+
print(f" API error — make sure both quotas exist: {exc}")
118+
119+
120+
# ---------------------------------------------------------------------------
121+
# Demo 2 — Static ResourceList comparison (no cluster required)
122+
# ---------------------------------------------------------------------------
123+
124+
def demo_static_comparison() -> None:
125+
"""Compare two ResourceLists offline to illustrate unit normalisation."""
126+
print_section(
127+
"Demo 2 — Offline ResourceList comparison (unit normalisation)")
128+
129+
hard_baseline = {
130+
"cpu": "1", # 1 core
131+
"memory": "1Gi", # 1 gibibyte
132+
"pods": "10",
133+
}
134+
hard_target = {
135+
"cpu": "1000m", # semantically equal to "1"
136+
"memory": "1073741824", # semantically equal to "1Gi"
137+
"pods": "20", # genuinely different
138+
}
139+
140+
print(" Baseline hard limits:", hard_baseline)
141+
print(" Target hard limits:", hard_target)
142+
143+
diffs = get_resource_list_diff(hard_baseline, hard_target)
144+
print("\n Differences (unit-normalised):")
145+
print_diffs(diffs)
146+
147+
# Show all fields including unchanged ones
148+
diffs_all = get_resource_list_diff(
149+
hard_baseline, hard_target, include_unchanged=True
150+
)
151+
print("\n All resources (include_unchanged=True):")
152+
print_diffs(diffs_all)
153+
154+
155+
# ---------------------------------------------------------------------------
156+
# Demo 3 — Boolean equality guard (reconciler pattern)
157+
# ---------------------------------------------------------------------------
158+
159+
def demo_equality_guard() -> None:
160+
"""Show the boolean helper — mirrors the Go reconciler guard pattern."""
161+
print_section("Demo 3 — Boolean equality guard (reconciler pattern)")
162+
163+
# In Go:
164+
# if !equality.Semantic.DeepEqual(current.Spec, stored.Spec) {
165+
# // update needed
166+
# }
167+
168+
spec_current = client.V1ResourceQuotaSpec(
169+
hard={"cpu": "1000m", "memory": "1073741824"}
170+
)
171+
spec_stored = client.V1ResourceQuotaSpec(
172+
hard={"cpu": "1", "memory": "1Gi"}
173+
)
174+
175+
quota_current = client.V1ResourceQuota(spec=spec_current)
176+
quota_stored = client.V1ResourceQuota(spec=spec_stored)
177+
178+
if resource_quotas_equal(quota_current, quota_stored):
179+
print(" ✅ Specs are semantically equal — no update needed.")
180+
else:
181+
print(" ✏️ Specs differ — would trigger an update.")
182+
183+
# Now change memory on the stored quota
184+
quota_stored.spec.hard["memory"] = "2Gi"
185+
if not resource_quotas_equal(quota_current, quota_stored):
186+
diffs = get_resource_list_diff(
187+
quota_current.spec.hard, quota_stored.spec.hard
188+
)
189+
print("\n After changing stored memory to 2Gi:")
190+
print_diffs(diffs)
191+
192+
193+
# ---------------------------------------------------------------------------
194+
# Entry point
195+
# ---------------------------------------------------------------------------
196+
197+
def main() -> None:
198+
try:
199+
config.load_incluster_config()
200+
except config.ConfigException:
201+
config.load_kube_config()
202+
203+
api_client = client.ApiClient()
204+
205+
demo_static_comparison()
206+
demo_equality_guard()
207+
demo_cluster_comparison(api_client)
208+
209+
210+
if __name__ == "__main__":
211+
main()

0 commit comments

Comments
 (0)