Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion isvctl/configs/providers/my-isv/config/k8s.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ commands:
phase: test
continue_on_failure: true
command: "python ../scripts/breakfix/cordon_node.py"
args: ["--region", "{{region}}"]
args: ["--region={{region}}"]
timeout: 600

tests:
Expand Down
327 changes: 308 additions & 19 deletions isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Copy link
Copy Markdown

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:

#!/bin/bash
set -eu
file='isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py'
printf '%s\n' '--- outline ---'
ast-grep outline "$file" --view expanded || true
printf '%s\n' '--- target file ---'
cat -n "$file"
printf '%s\n' '--- related subprocess and kubectl usage ---'
rg -n -C 3 'subprocess\.run|request-timeout|def _run|finally|uncordon|kubectl' isvctl/configs/providers/my-isv
printf '%s\n' '--- tests and docs mentioning cordon_node ---'
rg -n -C 3 'cordon_node|CordonTestError|request-timeout|TimeoutExpired' . --glob '!node_modules' --glob '!build' --glob '!dist'

Repository: NVIDIA/ai-cloud-validation

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- focused cordon tests ---'
cat -n isvctl/tests/test_my_isv_cordon_node.py
printf '%s\n' '--- executor timeout context ---'
sed -n '370,445p' isvctl/src/isvctl/orchestrator/step_executor.py
printf '%s\n' '--- configuration for the cordon step ---'
rg -n -C 8 'cordon|timeout|breakfix' isvctl/configs/providers/my-isv/config/k8s.yaml isvctl/configs/suites/k8s.yaml isvctl/configs/suites/bare_metal.yaml
printf '%s\n' '--- direct references to the test timeout ---'
rg -n -C 3 'step.*timeout|timeout.*step|subprocess\.run\(' isvctl/src/isvctl isvctl/tests --glob '*.py' | head -240

Repository: NVIDIA/ai-cloud-validation

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import subprocess
import sys
import time

script = (
    "import time\n"
    "time.sleep(0.25)\n"
)
started = time.monotonic()
completed = subprocess.run(
    [sys.executable, "-c", script],
    capture_output=True,
    text=True,
    check=False,
)
elapsed_without_timeout = time.monotonic() - started

started = time.monotonic()
try:
    subprocess.run(
        [sys.executable, "-c", script],
        capture_output=True,
        text=True,
        check=False,
        timeout=0.05,
    )
except subprocess.TimeoutExpired as exc:
    elapsed_with_timeout = time.monotonic() - started
    print({
        "without_timeout_returncode": completed.returncode,
        "without_timeout_elapsed_at_least_0_2": elapsed_without_timeout >= 0.2,
        "with_timeout_raises_timeout_expired": True,
        "with_timeout_elapsed_below_0_2": elapsed_with_timeout < 0.2,
        "timeout_value": exc.timeout,
    })
else:
    print({
        "without_timeout_returncode": completed.returncode,
        "without_timeout_elapsed_at_least_0_2": elapsed_without_timeout >= 0.2,
        "with_timeout_raises_timeout_expired": False,
    })
PY

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 kubectl process.

Pass a finite timeout to subprocess.run and a nonzero --request-timeout to kubectl. Translate subprocess.TimeoutExpired to CordonTestError, including in _cleanup, so a hung command cannot leave the node cordoned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py` around lines
46 - 52, Update the kubectl execution helper around subprocess.run to pass a
finite subprocess timeout and add a nonzero --request-timeout argument to every
kubectl invocation. Catch subprocess.TimeoutExpired and translate it to
CordonTestError, including cleanup handling in _cleanup, so timed-out commands
cannot leave the node cordoned.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 -240

Repository: 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.py

Repository: NVIDIA/ai-cloud-validation

Length of output: 17357


🌐 Web query:

Kubernetes kubectl cordon already cordoned node exit code successful behavior source

💡 Result:

When you run kubectl cordon on a node that is already cordoned, the command exits successfully with a return code of 0 [1]. The behavior is intentional and idempotent [1]. In the kubectl source code, the CordonHelper utility checks whether the node.Spec.Unschedulable field is already set to the desired state (in this case, true) [2]. If the node is already cordoned, the command determines that no update is required and performs no patch or update operation on the node [2][3]. It then typically prints a message to standard output confirming the node status (e.g., "node/name already cordoned") and exits successfully [4][3]. Because the command encounters no actual error or failure when the node is already in the target state, it does not trigger an error exit code [3]. This behavior ensures that scripts or automation pipelines using kubectl cordon do not break if the command is executed repeatedly or on nodes that were cordoned in previous steps [1].

Citations:


🌐 Web query:

Kubernetes API optimistic concurrency conditional update resourceVersion JSON patch test spec.unschedulable node

💡 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.py

Repository: 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.py

Repository: 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.py

Repository: NVIDIA/ai-cloud-validation

Length of output: 2610


Make cordon ownership atomic.

kubectl cordon succeeds when the node is already cordoned. Use a conditional update with the node's metadata.resourceVersion that requires spec.unschedulable to be false before setting it to true. Record cleanup ownership only after that update succeeds. Uncordon conditionally so a later actor's cordon is preserved. Add tests for concurrent cordon attempts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py` at line 255,
Update the node cordon flow around _select_node and the subsequent kubectl
operations to atomically claim ownership: conditionally update the node using
its metadata.resourceVersion, requiring spec.unschedulable to be false before
setting it true, and record cleanup ownership only after that update succeeds.
Make uncordon conditional on the ownership established by that update so a later
actor’s cordon remains intact, and add coverage for concurrent cordon attempts.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Restore the required my-isv execution gate.

When ISVCTL_DEMO_MODE=1, this script calls kubectl instead of returning dummy success. When demo mode is disabled, it performs a real operation instead of reporting not-implemented status. This makes local demo execution require a cluster.

Restore the required gate, or move this live validation to a non-template provider script.

As per coding guidelines, my-isv scripts must retain a DEMO_MODE gate, return dummy success in demo mode, and report not-implemented status for real runs.

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 319-319: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@isvctl/configs/providers/my-isv/scripts/breakfix/cordon_node.py` around lines
238 - 321, Update main to restore the required DEMO_MODE gate before any kubectl
or live validation work: when ISVCTL_DEMO_MODE=1, return the provider-neutral
dummy success result immediately, and when demo mode is disabled, return the
required not-implemented status instead of executing the cordon operation. Keep
the existing live validation logic out of the my-isv template path or otherwise
prevent it from being reached.

Source: Coding guidelines



if __name__ == "__main__":
sys.exit(main())
Loading