# Node Management API

Module: `citadel_operator.k3s.node`

See also: `PolicyMatch` from `citadel_operator.k3s.lifecycle_models` (documented below).

## NodeInfo

Frozen dataclass representing a node's current state.

```python
@dataclass(frozen=True)
class NodeInfo:
    name: str
    creation_timestamp: datetime
    age_hours: float
    ready: bool
    schedulable: bool
    conditions: dict[str, str] = {}       # e.g., {"DiskPressure": "False", "Ready": "True"}
    labels: dict[str, str] = {}
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `marked_for_termination` | `bool` | True if `citadel.dev/terminate-reason` label exists |
| `terminate_reason` | `str \| None` | Value of `citadel.dev/terminate-reason` label |

## DrainResult

Frozen dataclass representing the outcome of a drain operation.

```python
@dataclass(frozen=True)
class DrainResult:
    node_name: str
    success: bool
    pods_evicted: int
    pods_failed: list[str] = []
    pods_force_deleted: list[str] = []
    error: str | None = None
```

## NodeManager

Manages node lifecycle operations using the Kubernetes API.

### Constructor

```python
NodeManager(kubeconfig: str | None = None)
```

Loads kubeconfig from the given path, or falls back to in-cluster config / default kubeconfig.

### Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `list_nodes` | `() -> list[NodeInfo]` | List all cluster nodes |
| `get_node` | `(name: str) -> NodeInfo` | Get a single node |
| `get_node_age_hours` | `(name: str) -> float` | Get node age in hours |
| `cordon` | `(name: str) -> None` | Mark node unschedulable |
| `uncordon` | `(name: str) -> None` | Mark node schedulable |
| `label_for_termination` | `(name: str, reason: str, policy_name: str) -> None` | Set audit labels |
| `clear_termination_labels` | `(name: str) -> None` | Remove termination labels |
| `drain` | `(name, timeout_seconds=300, delete_emptydir_data=True, force_after_timeout=None) -> DrainResult` | Drain node via Eviction API |

### Drain Details

- Uses Kubernetes Eviction API (respects PodDisruptionBudgets)
- Skips DaemonSet pods and mirror pods
- Retries evictions blocked by PDBs with backoff
- If `force_after_timeout` is set, force-deletes remaining pods after the additional timeout

### Node Labels

| Label | Value |
|-------|-------|
| `citadel.dev/terminate-reason` | Reason string from policy |
| `citadel.dev/terminate-after` | ISO 8601 timestamp |
| `citadel.dev/policy-name` | Policy name that triggered termination |
| `citadel.dev/managed-by` | `citadel-lifecycle` |

### Usage

```python
from citadel_operator import NodeManager

nm = NodeManager()

# List and inspect nodes
nodes = nm.list_nodes()
for node in nodes:
    print(f"{node.name}: age={node.age_hours:.1f}h ready={node.ready}")

# Cordon and drain
nm.cordon("worker-1")
result = nm.drain("worker-1", timeout_seconds=300, force_after_timeout=60)
if result.success:
    print(f"Drained: {result.pods_evicted} pods evicted")
else:
    print(f"Failed: {result.error}, stuck pods: {result.pods_failed}")

# Audit-label a node
nm.label_for_termination("worker-1", reason="max-age", policy_name="max-age-48h")
```

## PolicyMatch

Module: `citadel_operator.k3s.lifecycle_models`

Frozen dataclass representing a match between a node and a termination policy. Produced by `LifecyclePolicyEvaluator.evaluate()`.

```python
@dataclass(frozen=True)
class PolicyMatch:
    node_name: str
    policy_name: str
    reason: str
    priority: int
    age_hours: float
    dry_run: bool
    drain_timeout: int
    delete_emptydir_data: bool = True
    force_after_timeout: int | None = None
```

```python
from citadel_operator import PolicyMatch
```
