1use anyhow::{bail, ensure, Result};
33use serde_json::{json, Map, Value};
34
35use crate::utils::sanitize;
36use crate::utils::truncate::truncate_with_suffix;
37
38pub const MAX_JSON_DEPTH: usize = 64;
43pub const MAX_TEXT_BYTES: usize = 2048;
45pub const MAX_LONG_TEXT_BYTES: usize = 8192;
47pub const MAX_SCALAR_LIST: usize = 1000;
49pub const MAX_QUEUES: usize = 500;
51
52pub const CLUSTER_INFO_FIELDS: &[&str] = &[
53 "id",
54 "startedOn",
55 "state",
56 "haState",
57 "haZooKeeperConnectionState",
58 "rmStateStoreName",
59 "resourceManagerVersion",
60 "resourceManagerBuildVersion",
61 "resourceManagerVersionBuiltOn",
62 "hadoopVersion",
63 "hadoopBuildVersion",
64 "hadoopVersionBuiltOn",
65];
66pub const METRICS_FIELDS: &[&str] = &[
67 "appsSubmitted",
68 "appsCompleted",
69 "appsPending",
70 "appsRunning",
71 "appsFailed",
72 "appsKilled",
73 "reservedMB",
74 "availableMB",
75 "allocatedMB",
76 "pendingMB",
77 "totalMB",
78 "utilizedMB",
79 "reservedVirtualCores",
80 "availableVirtualCores",
81 "allocatedVirtualCores",
82 "pendingVirtualCores",
83 "totalVirtualCores",
84 "utilizedVirtualCores",
85 "containersAllocated",
86 "containersReserved",
87 "containersPending",
88 "totalNodes",
89 "activeNodes",
90 "lostNodes",
91 "unhealthyNodes",
92 "decommissioningNodes",
93 "decommissionedNodes",
94 "rebootedNodes",
95 "shutdownNodes",
96];
97pub const SCHEDULER_FIELDS: &[&str] = &[
98 "type",
99 "queueName",
100 "queuePath",
101 "capacity",
102 "usedCapacity",
103 "maxCapacity",
104 "qstate",
106 "minQueueMemoryCapacity",
107 "maxQueueMemoryCapacity",
108 "numNodes",
109 "usedNodeCapacity",
110 "availNodeCapacity",
111 "totalNodeCapacity",
112 "numContainers",
113];
114pub const QUEUE_FIELDS: &[&str] = &[
115 "queueName",
116 "queuePath",
117 "type",
118 "queueType",
119 "state",
120 "capacity",
121 "usedCapacity",
122 "maxCapacity",
123 "absoluteCapacity",
124 "absoluteMaxCapacity",
125 "absoluteUsedCapacity",
126 "weight",
127 "numApplications",
128 "numActiveApplications",
129 "numPendingApplications",
130 "numContainers",
131 "maxApplications",
132 "maxApplicationsPerUser",
133 "userLimit",
134 "userLimitFactor",
135 "defaultPriority",
136 "maxApps",
138 "numActiveApps",
139 "numPendingApps",
140 "schedulingPolicy",
141];
142pub const APP_FIELDS: &[&str] = &[
143 "id",
144 "user",
145 "name",
146 "queue",
147 "state",
148 "finalStatus",
149 "progress",
150 "trackingUI",
151 "trackingUrl",
152 "diagnostics",
153 "clusterId",
154 "applicationType",
155 "applicationTags",
156 "priority",
157 "startedTime",
158 "launchTime",
159 "finishedTime",
160 "elapsedTime",
161 "amContainerLogs",
162 "amHostHttpAddress",
163 "amRPCAddress",
164 "masterNodeId",
165 "allocatedMB",
166 "allocatedVCores",
167 "reservedMB",
168 "reservedVCores",
169 "runningContainers",
170 "memorySeconds",
171 "vcoreSeconds",
172 "queueUsagePercentage",
173 "clusterUsagePercentage",
174 "preemptedResourceMB",
175 "preemptedResourceVCores",
176 "numNonAMContainerPreempted",
177 "numAMContainerPreempted",
178 "logAggregationStatus",
179 "unmanagedApplication",
180 "appNodeLabelExpression",
181 "amNodeLabelExpression",
182];
183pub const ATTEMPT_FIELDS: &[&str] = &[
184 "id",
185 "appAttemptId",
186 "appAttemptState",
187 "startTime",
188 "finishedTime",
189 "containerId",
190 "nodeId",
191 "nodeHttpAddress",
192 "logsLink",
193 "blacklistedNodes",
194 "nodesBlacklistedBySystem",
195 "diagnosticsInfo",
196];
197pub const NODE_FIELDS: &[&str] = &[
198 "id",
199 "rack",
200 "state",
201 "nodeHostName",
202 "nodeHTTPAddress",
203 "version",
204 "lastHealthUpdate",
205 "healthReport",
206 "numContainers",
207 "usedMemoryMB",
208 "availMemoryMB",
209 "usedVirtualCores",
210 "availableVirtualCores",
211 "memUtilization",
212 "cpuUtilization",
213 "numRunningOpportContainers",
214 "numQueuedContainers",
215];
216
217const LONG_TEXT_FIELDS: &[&str] = &["diagnostics", "diagnosticsInfo", "healthReport"];
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum Resource {
223 ClusterInfo,
224 Metrics,
225 Scheduler,
226 Applications,
227 Attempts,
228 AppState,
229 AppQueue,
230 Nodes,
231}
232
233impl Resource {
234 pub fn of(operation: &str) -> Option<Resource> {
235 Some(match operation {
236 "get_cluster_info" => Resource::ClusterInfo,
237 "get_cluster_metrics" => Resource::Metrics,
238 "get_scheduler" => Resource::Scheduler,
239 "list_applications" | "get_application" => Resource::Applications,
240 "list_app_attempts" => Resource::Attempts,
241 "get_app_state" | "kill_application" => Resource::AppState,
242 "get_app_queue" => Resource::AppQueue,
243 "list_nodes" | "get_node" => Resource::Nodes,
244 _ => return None,
245 })
246 }
247}
248
249pub fn json_depth(bytes: &[u8]) -> usize {
252 let (mut depth, mut max, mut in_string, mut escaped) = (0usize, 0usize, false, false);
253 for &b in bytes {
254 if in_string {
255 match (escaped, b) {
256 (true, _) => escaped = false,
257 (false, b'\\') => escaped = true,
258 (false, b'"') => in_string = false,
259 _ => {}
260 }
261 continue;
262 }
263 match b {
264 b'"' => in_string = true,
265 b'[' | b'{' => {
266 depth += 1;
267 max = max.max(depth);
268 }
269 b']' | b'}' => depth = depth.saturating_sub(1),
270 _ => {}
271 }
272 }
273 max
274}
275
276pub fn parse(body: &[u8]) -> Result<Value> {
278 let depth = json_depth(body);
279 ensure!(
280 depth <= MAX_JSON_DEPTH,
281 "the response nests {depth} levels deep; at most {MAX_JSON_DEPTH} are read"
282 );
283 serde_json::from_slice(body).map_err(|e| anyhow::anyhow!("the response is not JSON: {e}"))
284}
285
286pub fn text(s: &str, long: bool) -> String {
288 let clean = if long {
289 sanitize::multiline(s)
290 } else {
291 sanitize::line_field(s)
292 };
293 truncate_with_suffix(
294 &clean,
295 if long {
296 MAX_LONG_TEXT_BYTES
297 } else {
298 MAX_TEXT_BYTES
299 },
300 "...",
301 )
302}
303
304fn scalar(key: &str, v: &Value) -> Option<Value> {
306 match v {
307 Value::Null | Value::Bool(_) | Value::Number(_) => Some(v.clone()),
308 Value::String(s) => Some(Value::String(text(s, LONG_TEXT_FIELDS.contains(&key)))),
309 Value::Array(items) => Some(Value::Array(
310 items
311 .iter()
312 .filter(|x| !matches!(x, Value::Array(_) | Value::Object(_)))
313 .take(MAX_SCALAR_LIST)
314 .map(|x| match x {
315 Value::String(s) => Value::String(text(s, false)),
316 other => other.clone(),
317 })
318 .collect(),
319 )),
320 Value::Object(_) => None,
321 }
322}
323
324pub fn project(entry: &Value, fields: &[&str]) -> Value {
326 let mut out = Map::new();
327 if let Some(obj) = entry.as_object() {
328 for key in fields {
329 if let Some(v) = obj.get(*key).and_then(|v| scalar(key, v)) {
330 out.insert((*key).to_string(), v);
331 }
332 }
333 }
334 Value::Object(out)
335}
336
337fn wrapped_list<'a>(
341 value: &'a Value,
342 outer: &str,
343 inner: &str,
344 single: Option<&str>,
345) -> Result<&'a [Value]> {
346 let obj = value
347 .as_object()
348 .ok_or_else(|| anyhow::anyhow!("the ResourceManager answers with a JSON object"))?;
349 if let Some(one) = single.and_then(|k| obj.get(k)) {
350 ensure!(
351 one.is_object(),
352 "{:?} is not an object",
353 single.unwrap_or("")
354 );
355 return Ok(std::slice::from_ref(one));
356 }
357 let wrapper = obj.get(outer).ok_or_else(|| {
358 anyhow::anyhow!(
359 "the answer carries no {outer:?}{}",
360 single.map(|s| format!(" or {s:?}")).unwrap_or_default()
361 )
362 })?;
363 let list = match wrapper {
364 Value::Null => return Ok(&[]),
365 Value::Object(w) => match w.get(inner) {
366 None | Some(Value::Null) => return Ok(&[]),
367 Some(list) => list,
368 },
369 _ => bail!("{outer:?} is neither an object nor null"),
370 };
371 match list {
372 Value::Array(items) => {
373 ensure!(
374 items.iter().all(Value::is_object),
375 "{outer}.{inner} holds something other than objects"
376 );
377 Ok(items.as_slice())
378 }
379 Value::Object(_) => Ok(std::slice::from_ref(list)),
380 _ => bail!("{outer}.{inner} is neither an array nor an object"),
381 }
382}
383
384fn take(
386 out: &mut Map<String, Value>,
387 key: &str,
388 items: &[Value],
389 max_items: usize,
390 f: impl Fn(&Value) -> Value,
391) {
392 let total = items.len();
393 let kept: Vec<Value> = items.iter().take(max_items).map(f).collect();
394 out.insert("count".into(), json!(kept.len()));
395 out.insert("total".into(), json!(total));
396 out.insert("truncated".into(), json!(total > max_items));
397 out.insert(key.into(), Value::Array(kept));
398}
399
400fn child_queues(queue: &Value) -> Vec<&Value> {
403 let mut out = Vec::new();
404 for wrapper in ["queues", "childQueues"] {
405 match queue.get(wrapper).and_then(|w| w.get("queue")) {
406 Some(Value::Array(items)) => out.extend(items.iter().filter(|q| q.is_object())),
407 Some(one @ Value::Object(_)) => out.push(one),
408 _ => {}
409 }
410 }
411 out
412}
413
414fn walk_queues(root: &Value) -> (Vec<Value>, bool) {
418 let mut out = Vec::new();
419 let mut stack: Vec<&Value> = child_queues(root).into_iter().rev().collect();
420 while let Some(queue) = stack.pop() {
421 if out.len() >= MAX_QUEUES {
422 return (out, true);
423 }
424 out.push(project(queue, QUEUE_FIELDS));
425 stack.extend(child_queues(queue).into_iter().rev());
426 }
427 (out, false)
428}
429
430pub fn decode(
432 resource: Resource,
433 operation: &str,
434 value: &Value,
435 max_items: usize,
436 context: &Map<String, Value>,
437) -> Result<Value> {
438 let mut out = context.clone();
439 out.insert("operation".into(), json!(operation));
440 match resource {
441 Resource::ClusterInfo => {
442 let info = value
443 .get("clusterInfo")
444 .filter(|v| v.is_object())
445 .ok_or_else(|| anyhow::anyhow!("the answer carries no \"clusterInfo\" object"))?;
446 ensure!(
447 info.get("resourceManagerVersion")
448 .is_some_and(Value::is_string),
449 "clusterInfo carries no resourceManagerVersion"
450 );
451 out.insert("cluster_info".into(), project(info, CLUSTER_INFO_FIELDS));
452 }
453 Resource::Metrics => {
454 let metrics = value
455 .get("clusterMetrics")
456 .filter(|v| v.is_object())
457 .ok_or_else(|| {
458 anyhow::anyhow!("the answer carries no \"clusterMetrics\" object")
459 })?;
460 out.insert("metrics".into(), project(metrics, METRICS_FIELDS));
461 }
462 Resource::Scheduler => {
463 let info = value
464 .get("scheduler")
465 .and_then(|s| s.get("schedulerInfo"))
466 .filter(|v| v.is_object())
467 .ok_or_else(|| {
468 anyhow::anyhow!("the answer carries no \"scheduler.schedulerInfo\" object")
469 })?;
470 let root = info
472 .get("rootQueue")
473 .filter(|r| r.is_object())
474 .unwrap_or(info);
475 let (queues, cut) = walk_queues(root);
476 let mut summary = project(info, SCHEDULER_FIELDS);
477 if let (Some(s), Some(r)) = (summary.as_object_mut(), root.as_object()) {
478 for key in ["queueName", "capacity", "usedCapacity", "maxCapacity"] {
479 if let (false, Some(v)) = (s.contains_key(key), r.get(key)) {
480 if let Some(v) = scalar(key, v) {
481 s.insert(key.into(), v);
482 }
483 }
484 }
485 }
486 out.insert("scheduler".into(), summary);
487 take(&mut out, "queues", &queues, max_items, Value::clone);
488 if cut {
489 out.insert("truncated".into(), json!(true));
490 }
491 }
492 Resource::Applications => {
493 let items = wrapped_list(value, "apps", "app", Some("app"))?;
494 take(&mut out, "applications", items, max_items, |x| {
495 project(x, APP_FIELDS)
496 });
497 }
498 Resource::Attempts => {
499 let items = wrapped_list(value, "appAttempts", "appAttempt", None)?;
500 take(&mut out, "attempts", items, max_items, |x| {
501 project(x, ATTEMPT_FIELDS)
502 });
503 }
504 Resource::AppState => {
505 let state = value
506 .get("state")
507 .and_then(Value::as_str)
508 .ok_or_else(|| anyhow::anyhow!("the answer carries no \"state\""))?;
509 out.insert("state".into(), json!(text(state, false)));
510 }
511 Resource::AppQueue => {
512 let queue = value
513 .get("queue")
514 .and_then(Value::as_str)
515 .ok_or_else(|| anyhow::anyhow!("the answer carries no \"queue\""))?;
516 out.insert("queue".into(), json!(text(queue, false)));
517 }
518 Resource::Nodes => {
519 let items = wrapped_list(value, "nodes", "node", Some("node"))?;
520 take(&mut out, "nodes", items, max_items, |x| {
521 project(x, NODE_FIELDS)
522 });
523 }
524 }
525 Ok(Value::Object(out))
526}
527
528pub fn remote_exception(body: &[u8]) -> Option<(String, String)> {
531 if json_depth(body) > MAX_JSON_DEPTH {
532 return None;
533 }
534 let value: Value = serde_json::from_slice(body).ok()?;
535 let e = value.get("RemoteException")?;
536 let exception = e.get("exception").and_then(Value::as_str).unwrap_or("");
537 let message = e.get("message").and_then(Value::as_str).unwrap_or("");
538 Some((text(exception, false), text(message, false)))
539}