-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCrdSchemaUtils.java
More file actions
274 lines (237 loc) · 10.8 KB
/
CrdSchemaUtils.java
File metadata and controls
274 lines (237 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import io.fabric8.kubernetes.api.model.Quantity;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Shared utilities for CRD schema introspection and resource path resolution.
*
* <p>Used by both {@code VerifyResourceLimits} and {@code VerifyDocumentedResources}
* to discover {@code ResourceRequirements} fields in CRD OpenAPI v3 schemas
* and resolve those paths against CR instances.
*/
public class CrdSchemaUtils {
private CrdSchemaUtils() { }
/**
* Extract the kind name from a CRD document.
*/
static String extractCrdKind(Map<String, Object> crd) {
Map<String, Object> names = getNestedMap(crd, "spec", "names");
return names != null ? (String) names.get("kind") : null;
}
/**
* Extract the OpenAPI v3 schema from the first version of a CRD.
*/
@SuppressWarnings("unchecked")
static Map<String, Object> extractCrdSchema(Map<String, Object> crd) {
Map<String, Object> spec = getMap(crd, "spec");
if (spec == null) return null;
Object versionsObj = spec.get("versions");
if (!(versionsObj instanceof List)) return null;
List<?> versions = (List<?>) versionsObj;
if (versions.isEmpty()) return null;
Object firstVersion = versions.get(0);
if (!(firstVersion instanceof Map)) return null;
return getNestedMap((Map<String, Object>) firstVersion, "schema", "openAPIV3Schema");
}
/**
* Recursively walk a CRD schema to find all ResourceRequirements fields.
* Records the JSON path for each field found.
*
* <p>This method finds ALL ResourceRequirements fields without filtering.
* Callers that need to skip certain paths (e.g., pod-level overhead fields
* inside embedded PodTemplateSpecs) should filter the results using
* {@link #isPodSpecOverheadPath(String)}.
*/
@SuppressWarnings("unchecked")
static void walkSchema(Map<String, Object> schemaNode, String currentPath, List<String> result) {
if (schemaNode == null) return;
Map<String, Object> properties = getMap(schemaNode, "properties");
if (properties == null) return;
if (isResourceRequirements(properties)) {
result.add(currentPath);
return;
}
for (Map.Entry<String, Object> entry : properties.entrySet()) {
if (!(entry.getValue() instanceof Map)) continue;
Map<String, Object> childSchema = (Map<String, Object>) entry.getValue();
String childPath = currentPath + "." + entry.getKey();
String type = (String) childSchema.get("type");
if ("array".equals(type)) {
Map<String, Object> items = getMap(childSchema, "items");
if (items != null) {
walkSchema(items, childPath + "[]", result);
}
} else {
walkSchema(childSchema, childPath, result);
}
}
}
/**
* Detect a ResourceRequirements field by its OpenAPI schema signature.
* Must have "limits" and "requests" properties where both have
* additionalProperties with x-kubernetes-int-or-string: true.
*/
static boolean isResourceRequirements(Map<String, Object> properties) {
if (!properties.containsKey("limits") || !properties.containsKey("requests")) {
return false;
}
return hasIntOrStringAdditionalProperties(properties.get("limits"))
&& hasIntOrStringAdditionalProperties(properties.get("requests"));
}
@SuppressWarnings("unchecked")
private static boolean hasIntOrStringAdditionalProperties(Object fieldObj) {
if (!(fieldObj instanceof Map)) return false;
Map<String, Object> field = (Map<String, Object>) fieldObj;
Object addProps = field.get("additionalProperties");
if (!(addProps instanceof Map)) return false;
return Boolean.TRUE.equals(((Map<String, Object>) addProps).get("x-kubernetes-int-or-string"));
}
/**
* Check whether a ResourceRequirements path represents a pod-level
* overhead field embedded in a PodTemplateSpec, rather than a
* component-level resource requirement.
*
* <p>Pod-level resources (added in k8s 1.30) appear as siblings of
* {@code containers} inside embedded PodSpec structures like
* {@code template.spec} or {@code podTemplateSpec.spec}. These are
* infrastructure overhead and should not be required in CR configs.
*
* <p>CRD-level resources (e.g., Prometheus {@code spec.resources}) that
* happen to be siblings of {@code containers} are NOT filtered by this
* method — they appear at the CRD spec level, not inside an embedded
* PodTemplateSpec.
*
* @param path the dot-separated path (e.g., ".spec.app.podTemplateSpec.spec.resources")
* @return true if this is a pod-level overhead path that should be skipped
*/
static boolean isPodSpecOverheadPath(String path) {
return path.matches(".*\\.template\\.spec\\.resources$")
|| path.matches(".*\\.podTemplateSpec\\.spec\\.resources$");
}
/**
* Resolve a path through a document, handling array segments (ending with []).
* Returns all leaf values reached along with their resolved paths.
*/
@SuppressWarnings("unchecked")
static List<ResolvedNode> resolvePath(Object current, String[] segments, int index, String pathSoFar) {
if (index >= segments.length) {
return List.of(new ResolvedNode(pathSoFar, current));
}
String segment = segments[index];
if (segment.endsWith("[]")) {
String key = segment.substring(0, segment.length() - 2);
if (!(current instanceof Map)) return List.of();
Object listObj = ((Map<String, Object>) current).get(key);
if (!(listObj instanceof List)) return List.of();
List<?> list = (List<?>) listObj;
List<ResolvedNode> results = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
results.addAll(resolvePath(list.get(i), segments, index + 1,
pathSoFar + "." + key + "[" + i + "]"));
}
return results;
} else {
if (!(current instanceof Map)) return List.of();
Object child = ((Map<String, Object>) current).get(segment);
if (child == null) return List.of();
return resolvePath(child, segments, index + 1, pathSoFar + "." + segment);
}
}
// --- Kubernetes quantity parsing (delegates to Fabric8 Quantity) ---
private static final BigDecimal MILLIS_PER_CORE = BigDecimal.valueOf(1000);
private static final BigDecimal BYTES_PER_MIB = BigDecimal.valueOf(1_048_576);
/**
* Parse a Kubernetes CPU quantity to millicores.
*
* <p>Handles all Kubernetes quantity formats via Fabric8 {@link Quantity},
* including millicore suffixes ({@code "500m"}), whole/fractional cores
* ({@code "1"}, {@code "0.5"}), and values parsed by SnakeYAML as
* {@link Integer} or {@link Double}.
*
* @param value the CPU quantity (String, Integer, or Double)
* @return the value in millicores
*/
static long parseCpuMillis(Object value) {
Quantity q = Quantity.parse(String.valueOf(value));
return q.getNumericalAmount().multiply(MILLIS_PER_CORE).longValue();
}
/**
* Parse a Kubernetes memory quantity to MiB.
*
* <p>Handles all Kubernetes quantity formats via Fabric8 {@link Quantity},
* including binary suffixes ({@code Ki}, {@code Mi}, {@code Gi}, {@code Ti},
* {@code Pi}, {@code Ei}), decimal suffixes ({@code k}, {@code M}, {@code G},
* {@code T}, {@code P}, {@code E}), exponent notation, and plain byte counts.
*
* @param value the memory quantity (String, Integer, or Double)
* @return the value in MiB (rounded half-up)
*/
static long parseMemoryMiB(Object value) {
Quantity q = Quantity.parse(String.valueOf(value));
BigDecimal bytes = Quantity.getAmountInBytes(q);
return bytes.divide(BYTES_PER_MIB, 0, RoundingMode.HALF_UP).longValue();
}
/**
* Check that {@code resources.requests} does not exceed {@code resources.limits}
* for both CPU and memory.
*
* <p>Uses numeric comparison via Fabric8 {@link Quantity} so semantically
* equal values in different formats (e.g., {@code "1"} vs {@code "1000m"})
* are treated as equal.
*
* @param resources the resources map (with "requests" and "limits" sub-maps)
* @param prefix a human-readable prefix for error messages
* @return list of invariant violation messages (empty if requests <= limits)
*/
@SuppressWarnings("unchecked")
static List<String> checkRequestsNotExceedLimits(Map<String, Object> resources, String prefix) {
List<String> errors = new ArrayList<>();
if (resources == null) return errors;
Object requestsObj = resources.get("requests");
Object limitsObj = resources.get("limits");
if (!(requestsObj instanceof Map) || !(limitsObj instanceof Map)) return errors;
Map<String, Object> requests = (Map<String, Object>) requestsObj;
Map<String, Object> limits = (Map<String, Object>) limitsObj;
if (requests.containsKey("cpu") && limits.containsKey("cpu")) {
long reqCpu = parseCpuMillis(requests.get("cpu"));
long limCpu = parseCpuMillis(limits.get("cpu"));
if (reqCpu > limCpu) {
errors.add(prefix + " requests.cpu (" + reqCpu
+ "m) > limits.cpu (" + limCpu + "m)");
}
}
if (requests.containsKey("memory") && limits.containsKey("memory")) {
long reqMem = parseMemoryMiB(requests.get("memory"));
long limMem = parseMemoryMiB(limits.get("memory"));
if (reqMem > limMem) {
errors.add(prefix + " requests.memory (" + reqMem
+ "Mi) > limits.memory (" + limMem + "Mi)");
}
}
return errors;
}
// --- Utilities ---
@SuppressWarnings("unchecked")
static Map<String, Object> getMap(Map<String, Object> parent, String key) {
Object value = parent.get(key);
return value instanceof Map ? (Map<String, Object>) value : null;
}
static Map<String, Object> getNestedMap(Map<String, Object> root, String... keys) {
Map<String, Object> current = root;
for (String key : keys) {
current = getMap(current, key);
if (current == null) return null;
}
return current;
}
static class ResolvedNode {
final String path;
final Object value;
ResolvedNode(String path, Object value) {
this.path = path;
this.value = value;
}
}
}