-
Notifications
You must be signed in to change notification settings - Fork 29
test(breakfix): implement cordon node validation #572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,35 +2,324 @@ | |
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Cordon a Kubernetes node (BFX01-04) - my-isv template.""" | ||
| """Exercise Kubernetes node cordoning semantics for BFX01-04.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import os | ||
| import shlex | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| import time | ||
| import uuid | ||
| from typing import Any | ||
|
|
||
| # Allow importing provider-local helpers from scripts/common/. | ||
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | ||
| from common.stub import emit_stub | ||
| DEFAULT_IMAGE = "registry.k8s.io/pause:3.10" | ||
|
|
||
|
|
||
| def main() -> int: | ||
| """Emit the cordon-node template result (BFX01-04).""" | ||
| parser = argparse.ArgumentParser(description="Cordon node (template)") | ||
| parser.add_argument("--region", default="", help="Cloud region") | ||
| _ = parser.parse_args() | ||
|
|
||
| return emit_stub( | ||
| "cordon_node", | ||
| hint="cordon node breakfix API", | ||
| operation={ | ||
| "cordoned": True, | ||
| "new_workloads_blocked": True, | ||
| "existing_workloads_running": True, | ||
| }, | ||
| class CordonTestError(RuntimeError): | ||
| """Raised when the cordon workflow cannot prove the required behavior.""" | ||
|
|
||
|
|
||
| def _kubectl_command() -> list[str]: | ||
| """Return the configured kubectl-compatible command prefix.""" | ||
| configured = os.environ.get("KUBECTL", "kubectl") | ||
| try: | ||
| command = shlex.split(configured) | ||
| except ValueError as exc: | ||
| raise CordonTestError(f"Invalid KUBECTL value: {exc}") from exc | ||
| if not command: | ||
| raise CordonTestError("KUBECTL must not be blank") | ||
| return command | ||
|
|
||
|
|
||
| def _run( | ||
| kubectl: list[str], | ||
| *args: str, | ||
| input_text: str | None = None, | ||
| check: bool = True, | ||
| ) -> subprocess.CompletedProcess[str]: | ||
| """Run kubectl and raise a concise error when the command fails.""" | ||
| try: | ||
| completed = subprocess.run( | ||
| [*kubectl, *args], | ||
| input=input_text, | ||
| capture_output=True, | ||
| text=True, | ||
| check=False, | ||
| ) | ||
| except OSError as exc: | ||
| raise CordonTestError(f"Unable to run {' '.join(kubectl)}: {exc}") from exc | ||
| if check and completed.returncode != 0: | ||
| detail = (completed.stderr or completed.stdout).strip() | ||
| detail = detail[-500:] if detail else "command failed without output" | ||
| raise CordonTestError(f"kubectl {' '.join(args)} failed: {detail}") | ||
| return completed | ||
|
|
||
|
|
||
| def _json_output(completed: subprocess.CompletedProcess[str], resource: str) -> dict[str, Any]: | ||
| """Parse one kubectl JSON object.""" | ||
| try: | ||
| payload = json.loads(completed.stdout) | ||
| except json.JSONDecodeError as exc: | ||
| raise CordonTestError(f"kubectl returned invalid JSON for {resource}") from exc | ||
| if not isinstance(payload, dict): | ||
| raise CordonTestError(f"kubectl returned a non-object for {resource}") | ||
| return payload | ||
|
|
||
|
|
||
| def _node_is_ready(node: dict[str, Any]) -> bool: | ||
| """Return whether a node is Ready, schedulable, and available for the test.""" | ||
| conditions = node.get("status", {}).get("conditions", []) | ||
| ready = any(item.get("type") == "Ready" and item.get("status") == "True" for item in conditions) | ||
| return ready and not node.get("spec", {}).get("unschedulable", False) | ||
|
|
||
|
|
||
| def _select_node(kubectl: list[str], requested_node: str | None) -> tuple[str, str, list[dict[str, str]]]: | ||
| """Select a schedulable Ready node and return its identity and required tolerations.""" | ||
| payload = _json_output(_run(kubectl, "get", "nodes", "-o", "json"), "node list") | ||
| items = payload.get("items") | ||
| if not isinstance(items, list): | ||
| raise CordonTestError("kubectl node list is missing items") | ||
|
|
||
| candidates = [node for node in items if isinstance(node, dict) and _node_is_ready(node)] | ||
| if requested_node: | ||
| candidates = [node for node in candidates if node.get("metadata", {}).get("name") == requested_node] | ||
| if not candidates: | ||
| raise CordonTestError(f"Requested node {requested_node!r} is not Ready and schedulable") | ||
| if not candidates: | ||
| raise CordonTestError("No Ready, schedulable node is available for the cordon test") | ||
|
|
||
| node = candidates[0] | ||
| metadata = node.get("metadata", {}) | ||
| name = metadata.get("name") | ||
| hostname = metadata.get("labels", {}).get("kubernetes.io/hostname") | ||
| if not isinstance(name, str) or not name: | ||
| raise CordonTestError("Selected node is missing metadata.name") | ||
| if not isinstance(hostname, str) or not hostname: | ||
| raise CordonTestError(f"Node {name!r} is missing the kubernetes.io/hostname label") | ||
| tolerations = [] | ||
| for taint in node.get("spec", {}).get("taints", []): | ||
| key = taint.get("key") | ||
| effect = taint.get("effect") | ||
| if not isinstance(key, str) or effect not in {"NoSchedule", "NoExecute"}: | ||
| continue | ||
| if key == "node.kubernetes.io/unschedulable": | ||
| continue | ||
| tolerations.append( | ||
| { | ||
| "key": key, | ||
| "operator": "Equal", | ||
| "value": str(taint.get("value", "")), | ||
| "effect": effect, | ||
| } | ||
| ) | ||
| return name, hostname, tolerations | ||
|
|
||
|
|
||
| def _pod_manifest( | ||
| name: str, | ||
| namespace: str, | ||
| hostname: str, | ||
| image: str, | ||
| tolerations: list[dict[str, str]], | ||
| ) -> str: | ||
| """Build a minimal long-running pod constrained to one node hostname.""" | ||
| return json.dumps( | ||
| { | ||
| "apiVersion": "v1", | ||
| "kind": "Pod", | ||
| "metadata": { | ||
| "name": name, | ||
| "namespace": namespace, | ||
| "labels": { | ||
| "app.kubernetes.io/managed-by": "isvtest", | ||
| "isvtest.nvidia.com/purpose": "bfx01-04", | ||
| }, | ||
| }, | ||
| "spec": { | ||
| "restartPolicy": "Never", | ||
| "nodeSelector": {"kubernetes.io/hostname": hostname}, | ||
| "tolerations": tolerations, | ||
| "containers": [{"name": "probe", "image": image}], | ||
| }, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _get_pod(kubectl: list[str], namespace: str, name: str) -> dict[str, Any]: | ||
| """Return one pod as a JSON object.""" | ||
| completed = _run(kubectl, "get", "pod", name, "-n", namespace, "-o", "json") | ||
| return _json_output(completed, f"pod {namespace}/{name}") | ||
|
|
||
|
|
||
| def _pod_is_ready_on_node(pod: dict[str, Any], node_name: str) -> bool: | ||
| """Return whether a pod is still Ready and bound to the expected node.""" | ||
| if pod.get("spec", {}).get("nodeName") != node_name or pod.get("status", {}).get("phase") != "Running": | ||
| return False | ||
| conditions = pod.get("status", {}).get("conditions", []) | ||
| return any(item.get("type") == "Ready" and item.get("status") == "True" for item in conditions) | ||
|
|
||
|
|
||
| def _pod_is_unschedulable(pod: dict[str, Any]) -> bool: | ||
| """Return whether the scheduler explicitly reported the pod unschedulable.""" | ||
| if pod.get("spec", {}).get("nodeName"): | ||
| return False | ||
| conditions = pod.get("status", {}).get("conditions", []) | ||
| return any( | ||
| item.get("type") == "PodScheduled" and item.get("status") == "False" and item.get("reason") == "Unschedulable" | ||
| for item in conditions | ||
| ) | ||
|
|
||
|
|
||
| def _wait_for_unschedulable( | ||
| kubectl: list[str], | ||
| namespace: str, | ||
| name: str, | ||
| timeout_seconds: float, | ||
| poll_interval_seconds: float, | ||
| ) -> bool: | ||
| """Poll until Kubernetes reports that the new probe cannot be scheduled.""" | ||
| deadline = time.monotonic() + timeout_seconds | ||
| while True: | ||
| if _pod_is_unschedulable(_get_pod(kubectl, namespace, name)): | ||
| return True | ||
| if time.monotonic() >= deadline: | ||
| return False | ||
| time.sleep(poll_interval_seconds) | ||
|
|
||
|
|
||
| def _cleanup(kubectl: list[str], namespace: str, pod_names: list[str], node_name: str | None) -> list[str]: | ||
| """Delete probe pods and restore node schedulability, returning any errors.""" | ||
| errors: list[str] = [] | ||
| for pod_name in pod_names: | ||
| try: | ||
| completed = _run( | ||
| kubectl, | ||
| "delete", | ||
| "pod", | ||
| pod_name, | ||
| "-n", | ||
| namespace, | ||
| "--ignore-not-found=true", | ||
| "--wait=false", | ||
| check=False, | ||
| ) | ||
| except CordonTestError as exc: | ||
| errors.append(f"delete pod {namespace}/{pod_name}: {exc}") | ||
| continue | ||
| if completed.returncode != 0: | ||
| errors.append(f"delete pod {namespace}/{pod_name}: {(completed.stderr or completed.stdout).strip()}") | ||
| if node_name: | ||
| try: | ||
| completed = _run(kubectl, "uncordon", node_name, check=False) | ||
| except CordonTestError as exc: | ||
| errors.append(f"uncordon node {node_name}: {exc}") | ||
| else: | ||
| if completed.returncode != 0: | ||
| errors.append(f"uncordon node {node_name}: {(completed.stderr or completed.stdout).strip()}") | ||
| return errors | ||
|
|
||
|
|
||
| def _parser() -> argparse.ArgumentParser: | ||
| """Build the command-line parser.""" | ||
| parser = argparse.ArgumentParser(description="Cordon a node and verify Kubernetes scheduling behavior") | ||
| parser.add_argument("--region", default="", help="Accepted for provider config compatibility") | ||
| parser.add_argument("--node", help="Specific Ready, schedulable node to test") | ||
| parser.add_argument("--namespace", default="default", help="Namespace for temporary probe pods") | ||
| parser.add_argument("--image", default=DEFAULT_IMAGE, help="Container image for temporary probe pods") | ||
| parser.add_argument("--timeout-seconds", type=float, default=120, help="Timeout for each scheduling assertion") | ||
| parser.add_argument("--poll-interval-seconds", type=float, default=2, help="Pending-pod polling interval") | ||
| return parser | ||
|
|
||
|
|
||
| def main() -> int: | ||
| """Run the reversible cordon test and emit its provider-neutral JSON result.""" | ||
| args = _parser().parse_args() | ||
| operation: dict[str, Any] = { | ||
| "cordoned": False, | ||
| "new_workloads_blocked": False, | ||
| "existing_workloads_running": False, | ||
| } | ||
| result: dict[str, Any] = {"success": False, "platform": "my-isv", "test_name": "cordon_node"} | ||
| kubectl: list[str] = [] | ||
| created_pods: list[str] = [] | ||
| cordoned_node: str | None = None | ||
|
|
||
| try: | ||
| if args.timeout_seconds <= 0 or args.poll_interval_seconds <= 0: | ||
| raise CordonTestError("Timeout and poll interval must be greater than zero") | ||
| kubectl = _kubectl_command() | ||
| node_name, hostname, tolerations = _select_node(kubectl, args.node) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -eu
file=$(git ls-files 'isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py')
printf '%s\n' "$file"
wc -l "$file"
ast-grep outline "$file" || true
printf '\n--- target sections ---\n'
sed -n '200,320p' "$file"
printf '\n--- related symbols and tests ---\n'
rg -n --glob '!dist' --glob '!build' 'cordoned_node|_select_node|cordon_node|cordon' isvctl/configs/providers/my-isv isvctl | head -240Repository: NVIDIA/ai-cloud-validation Length of output: 16261 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- cordon_node.py: imports through selection and cleanup ---'
sed -n '1,225p' isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py
printf '%s\n' '--- test_my_isv_cordon_node.py ---'
sed -n '1,245p' isvctl/tests/test_my_isv_cordon_node.pyRepository: NVIDIA/ai-cloud-validation Length of output: 17357 🌐 Web query:
💡 Result: When you run Citations:
🌐 Web query:
💡 Result: Kubernetes uses optimistic concurrency control via the resourceVersion field in an object's metadata to prevent lost updates [1][2][3]. By including the current resourceVersion in an update or patch request, a client can ensure the operation only succeeds if the object has not been modified since it was last read [2][4][3]. If the provided resourceVersion does not match the server-side version, the API server rejects the request with a 409 Conflict error [1][2][4]. To perform a conditional update on a node's unschedulable status, you can follow this pattern: 1. Retrieve the latest node object, including its current metadata.resourceVersion [2][3]. 2. Construct your patch payload. While a standard patch might omit the version, you can include the resourceVersion within the metadata field to enforce concurrency control [3]. For example, using a strategic merge patch to set a node to unschedulable: { "metadata": { "resourceVersion": "CURRENT_VERSION_STRING" }, "spec": { "unschedulable": true } } When using JSON Patch (RFC 6902), which uses an array of operations, you cannot directly include the resourceVersion in the same way as a merge patch because the format is strictly defined by the operations [5]. However, the Kubernetes API supports conditional PATCH operations where you can effectively use the resourceVersion to ensure the update remains atomic [6]. Note that standard practice often favors using the replace operation (PUT) when full object concurrency control is required, or ensuring the patch itself is applied to the intended version [5][2]. If you are using kubectl, the command to set a node as unschedulable is: kubectl patch node -p '{"spec":{"unschedulable":true}}' [7][8] To make this conditional, you would typically fetch the resource, verify the version, and use the strategic merge patch approach shown above [3]. The resourceVersion must be treated as an opaque string by clients and passed back exactly as received from the server [2][4]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import ast
from pathlib import Path
source_path = Path("isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py")
tree = ast.parse(source_path.read_text())
main = next(
node for node in tree.body
if isinstance(node, ast.FunctionDef) and node.name == "main"
)
events = []
for node in ast.walk(main):
if isinstance(node, ast.Assign):
value = node.value
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
if value.func.id == "_select_node":
events.append(("select", node.lineno))
elif value.func.id == "_run" and len(value.args) >= 2:
command = value.args[1]
if isinstance(command, ast.Constant) and command.value == "cordon":
events.append(("cordon", node.lineno))
if (
isinstance(value, ast.Name)
and value.id == "node_name"
and any(isinstance(target, ast.Name) and target.id == "cordoned_node" for target in node.targets)
):
events.append(("record_ownership", node.lineno))
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Call):
call = node.value
if isinstance(call.func, ast.Name) and call.func.id == "_cleanup":
events.append(("cleanup", node.lineno))
ordered = sorted(events, key=lambda item: item[1])
print("main event order:", ordered)
expected = ["select", "cordon", "record_ownership", "cleanup"]
actual = [name for name, _ in ordered]
assert actual == expected, (actual, expected)
# Standalone model of the documented idempotent cordon behavior.
node = {"unschedulable": False, "resource_version": "v1"}
selected_version = node["resource_version"]
node["unschedulable"] = True
node["resource_version"] = "v2" # another actor cordons after selection
cordon_returncode = 0 # kubectl cordon treats an already-cordoned node as success
ownership_recorded = cordon_returncode == 0
assert ownership_recorded
assert node["unschedulable"] is True
if ownership_recorded:
node["unschedulable"] = False # _cleanup invokes uncordon unconditionally
print("race result:", {
"selected_resource_version": selected_version,
"current_resource_version": node["resource_version"],
"final_unschedulable": node["unschedulable"],
})
assert node["unschedulable"] is False
PY
printf '\n--- tests covering ownership races ---\n'
rg -n 'concurrent|race|already cordon|precord|cordon' isvctl/tests/test_my_isv_cordon_node.pyRepository: NVIDIA/ai-cloud-validation Length of output: 403 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import ast
from pathlib import Path
source_path = Path("isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py")
tree = ast.parse(source_path.read_text())
main = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "main")
events = []
for node in ast.walk(main):
call = None
if isinstance(node, ast.Expr):
call = node.value
elif isinstance(node, ast.Assign):
call = node.value
if not isinstance(call, ast.Call) or not isinstance(call.func, ast.Name):
continue
if call.func.id == "_select_node":
events.append(("select", node.lineno))
elif call.func.id == "_run" and len(call.args) >= 2:
command = call.args[1]
if isinstance(command, ast.Constant) and command.value == "cordon":
events.append(("cordon", node.lineno))
elif call.func.id == "_cleanup":
events.append(("cleanup", node.lineno))
if (
isinstance(node, ast.Assign)
and isinstance(call, ast.Name)
and call.id == "node_name"
and any(isinstance(target, ast.Name) and target.id == "cordoned_node" for target in node.targets)
):
events.append(("record_ownership", node.lineno))
ordered = sorted(events, key=lambda item: item[1])
print("main event order:", ordered)
expected = ["select", "cordon", "record_ownership", "cleanup"]
actual = [name for name, _ in ordered]
assert actual == expected, (actual, expected)
node = {"unschedulable": False, "resource_version": "v1"}
selected_version = node["resource_version"]
node["unschedulable"] = True
node["resource_version"] = "v2"
cordon_returncode = 0
ownership_recorded = cordon_returncode == 0
assert ownership_recorded
if ownership_recorded:
node["unschedulable"] = False
print("race result:", {
"selected_resource_version": selected_version,
"current_resource_version": node["resource_version"],
"final_unschedulable": node["unschedulable"],
})
assert node["unschedulable"] is False
PY
printf '\n--- ownership-related tests ---\n'
rg -n 'concurrent|race|precord|cordon|uncordon' isvctl/tests/test_my_isv_cordon_node.pyRepository: NVIDIA/ai-cloud-validation Length of output: 383 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import ast
from pathlib import Path
source_path = Path("isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py")
tree = ast.parse(source_path.read_text())
main = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "main")
events = []
for node in ast.walk(main):
if isinstance(node, ast.Assign):
if (
isinstance(node.value, ast.Name)
and node.value.id == "node_name"
and any(isinstance(target, ast.Name) and target.id == "cordoned_node" for target in node.targets)
):
events.append(("record_ownership", node.lineno))
call_nodes = []
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
call_nodes.append(node.value)
elif isinstance(node, ast.Assign):
call_nodes.extend(
child for child in ast.walk(node.value)
if isinstance(child, ast.Call)
)
for call in call_nodes:
if not isinstance(call.func, ast.Name):
continue
if call.func.id == "_select_node":
events.append(("select", call.lineno))
elif call.func.id == "_cleanup":
events.append(("cleanup", call.lineno))
elif call.func.id == "_run" and len(call.args) >= 2:
command = call.args[1]
if isinstance(command, ast.Constant) and command.value == "cordon":
events.append(("cordon", call.lineno))
ordered = sorted(set(events), key=lambda item: item[1])
print("main event order:", ordered)
expected = ["select", "cordon", "record_ownership", "cleanup"]
actual = [name for name, _ in ordered]
assert actual == expected, (actual, expected)
node = {"unschedulable": False, "resource_version": "v1"}
selected_version = node["resource_version"]
node["unschedulable"] = True
node["resource_version"] = "v2"
cordon_returncode = 0
ownership_recorded = cordon_returncode == 0
assert ownership_recorded
if ownership_recorded:
node["unschedulable"] = False
print("race result:", {
"selected_resource_version": selected_version,
"current_resource_version": node["resource_version"],
"final_unschedulable": node["unschedulable"],
})
assert node["unschedulable"] is False
PY
printf '\n--- ownership-related tests ---\n'
rg -n 'concurrent|race|precord|cordon|uncordon' isvctl/tests/test_my_isv_cordon_node.pyRepository: NVIDIA/ai-cloud-validation Length of output: 2610 Make cordon ownership atomic.
🤖 Prompt for AI Agents |
||
| operation["node_id"] = node_name | ||
| suffix = uuid.uuid4().hex[:8] | ||
| existing_pod = f"isvtest-bfx-existing-{suffix}" | ||
| blocked_pod = f"isvtest-bfx-blocked-{suffix}" | ||
|
|
||
| _run( | ||
| kubectl, | ||
| "create", | ||
| "-f", | ||
| "-", | ||
| input_text=_pod_manifest(existing_pod, args.namespace, hostname, args.image, tolerations), | ||
| ) | ||
| created_pods.append(existing_pod) | ||
| _run( | ||
| kubectl, | ||
| "wait", | ||
| "--for=condition=Ready", | ||
| f"pod/{existing_pod}", | ||
| "-n", | ||
| args.namespace, | ||
| f"--timeout={args.timeout_seconds:g}s", | ||
| ) | ||
|
|
||
| _run(kubectl, "cordon", node_name) | ||
| cordoned_node = node_name | ||
| node = _json_output(_run(kubectl, "get", "node", node_name, "-o", "json"), f"node {node_name}") | ||
| operation["cordoned"] = node.get("spec", {}).get("unschedulable") is True | ||
| if not operation["cordoned"]: | ||
| raise CordonTestError(f"Node {node_name!r} was not marked unschedulable") | ||
|
|
||
| operation["existing_workloads_running"] = _pod_is_ready_on_node( | ||
| _get_pod(kubectl, args.namespace, existing_pod), node_name | ||
| ) | ||
| if not operation["existing_workloads_running"]: | ||
| raise CordonTestError("Existing probe pod did not remain Ready on the cordoned node") | ||
|
|
||
| _run( | ||
| kubectl, | ||
| "create", | ||
| "-f", | ||
| "-", | ||
| input_text=_pod_manifest(blocked_pod, args.namespace, hostname, args.image, tolerations), | ||
| ) | ||
| created_pods.append(blocked_pod) | ||
| operation["new_workloads_blocked"] = _wait_for_unschedulable( | ||
| kubectl, | ||
| args.namespace, | ||
| blocked_pod, | ||
| args.timeout_seconds, | ||
| args.poll_interval_seconds, | ||
| ) | ||
| if not operation["new_workloads_blocked"]: | ||
| raise CordonTestError("New probe pod was not confirmed unschedulable on the cordoned node") | ||
| result["success"] = True | ||
| except CordonTestError as exc: | ||
| result["error"] = str(exc) | ||
| finally: | ||
| cleanup_errors = _cleanup(kubectl, args.namespace, created_pods, cordoned_node) if kubectl else [] | ||
| if cleanup_errors: | ||
| result["success"] = False | ||
| result["cleanup_errors"] = cleanup_errors | ||
| result.setdefault("error", "Cordon test cleanup failed") | ||
|
|
||
| result["operation"] = operation | ||
| print(json.dumps(result, indent=2)) | ||
| return 0 if result["success"] else 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Restore the required When Restore the required gate, or move this live validation to a non-template provider script. As per coding guidelines, 🧰 Tools🪛 ast-grep (0.45.0)[info] 319-319: use jsonify instead of json.dumps for JSON output (use-jsonify) 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA/ai-cloud-validation
Length of output: 50382
🏁 Script executed:
Repository: NVIDIA/ai-cloud-validation
Length of output: 50382
🏁 Script executed:
Repository: NVIDIA/ai-cloud-validation
Length of output: 352
🌐 Web query:
kubectl generated command cordon --request-timeout default 0 request-timeout documentation💡 Result:
The --request-timeout flag is a valid, inherited option available for the kubectl cordon command [1][2]. According to official Kubernetes documentation, the --request-timeout flag specifies the length of time to wait before giving up on a single server request [1][3]. The default value for this flag is "0," which signifies that there is no timeout for requests [1][4]. If a non-zero value is specified, it must include a corresponding time unit (e.g., 1s, 2m, or 3h) [1][5]. Because this is a global flag inherited by kubectl commands, it allows users to override the default client-side timeout behavior when performing operations like marking a node as unschedulable [3][6].
Citations:
Bound every
kubectlprocess.Pass a finite
timeouttosubprocess.runand a nonzero--request-timeouttokubectl. Translatesubprocess.TimeoutExpiredtoCordonTestError, including in_cleanup, so a hung command cannot leave the node cordoned.🤖 Prompt for AI Agents