# Probe Discovery API

Module: `citadel_operator.k3s.probe_discovery`

Discovers and checks health probes from pods annotated with Apollo health annotations. Aggregates results per entity for service-level health assessment.

## Annotations

Pods must have these annotations to be discovered:

| Annotation | Required | Default | Description |
|------------|----------|---------|-------------|
| `apollo.citadel.io/health-port` | Yes | — | Port exposing the health endpoint |
| `apollo.citadel.io/entity-name` | Yes | — | Logical entity this pod belongs to |
| `apollo.citadel.io/health-path` | No | `/health` | Path to probe |
| `apollo.citadel.io/health-scheme` | No | `http` | URL scheme (http or https) |

### Example Pod Annotation

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-service-abc123
  annotations:
    apollo.citadel.io/health-port: "8081"
    apollo.citadel.io/health-path: "/ready"
    apollo.citadel.io/health-scheme: "http"
    apollo.citadel.io/entity-name: "my-service"
```

## ProbeAnnotation

Frozen dataclass parsed from pod annotations.

```python
@dataclass(frozen=True)
class ProbeAnnotation:
    port: int
    path: str
    scheme: str
    entity_name: str

    @classmethod
    def from_annotations(cls, annotations: dict[str, str]) -> ProbeAnnotation | None: ...
```

Returns `None` if required annotations are missing.

## DiscoveredProbe

Frozen dataclass representing a probe endpoint found on a pod.

```python
@dataclass(frozen=True)
class DiscoveredProbe:
    pod_name: str
    pod_ip: str
    namespace: str
    entity_name: str
    port: int
    path: str
    scheme: str = "http"

    @property
    def url(self) -> str: ...  # e.g., "http://10.42.0.5:8081/ready"
```

## ProbeResult

Frozen dataclass representing a health check result. Also exported from the public API as `DiscoveryProbeResult` (alias).

```python
@dataclass(frozen=True)
class ProbeResult:
    probe: DiscoveredProbe
    healthy: bool
    status_code: int | None
    response_time_ms: float
    error: str | None
    checked_at: str              # ISO 8601 timestamp
```

```python
# Public API alias
from citadel_operator import DiscoveryProbeResult  # same as probe_discovery.ProbeResult
```

## EntityHealth

Frozen dataclass aggregating health across all pods of an entity.

```python
@dataclass(frozen=True)
class EntityHealth:
    entity_name: str
    total_pods: int
    healthy_pods: int
    unhealthy_pods: int
    health_ratio: float              # 0.0 to 1.0
    probe_results: tuple[ProbeResult, ...]

    @property
    def fully_healthy(self) -> bool: ...    # All pods healthy
    @property
    def fully_unhealthy(self) -> bool: ...  # No pods healthy
```

## ProbeDiscoverer

Discovers annotated pods and checks their health endpoints.

### Constructor

```python
ProbeDiscoverer(
    namespace: str = "default",
    poll_interval: int = 30,
    health_check_timeout: int = 5,
)
```

### Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `discover_probes` | `() -> list[DiscoveredProbe]` | Find annotated pods via kubectl |
| `check_probe` | `(probe) -> ProbeResult` | Single HTTP health check |
| `check_all` | `() -> list[ProbeResult]` | Discover + check all probes |
| `aggregate_health` | `() -> list[EntityHealth]` | Discover + check + aggregate by entity |

### Usage

```python
from citadel_operator import ProbeDiscoverer

pd = ProbeDiscoverer(namespace="production")

# Full pipeline: discover → check → aggregate
entities = pd.aggregate_health()

for entity in entities:
    status = "OK" if entity.fully_healthy else f"{entity.health_ratio:.0%}"
    print(f"{entity.entity_name}: {status} ({entity.healthy_pods}/{entity.total_pods} pods)")
```

### Implementation Notes

- Uses `kubectl get pods --output json` for discovery (minimal dependencies)
- Uses `urllib.request` for HTTP health checks (no extra deps)
- Groups results by `entity_name` annotation value
