|
| 1 | +# Node status (why a node did or did not converge) |
| 2 | + |
| 3 | +The provider runs one bounded reconcile pass on each boot. Because that pass runs |
| 4 | +as a fire-and-forget boot-time stage, its exit code does not flow back to |
| 5 | +`kairos-agent` - so the provider publishes its outcome through two channels you can |
| 6 | +inspect directly, instead of making you read journald. Both are **best-effort**: a |
| 7 | +status write never blocks the boot, never retries forever, and never changes or |
| 8 | +masks the real reconcile result. |
| 9 | + |
| 10 | +## Layer 1 - the local status file (always written) |
| 11 | + |
| 12 | +Every reconcile pass and every reset writes a small YAML status document to: |
| 13 | + |
| 14 | +- `/run/provider-kubernetes/status.yaml` - current boot, on tmpfs (authoritative). |
| 15 | +- `/var/log/provider-kubernetes/status.yaml` - a persistent mirror that survives |
| 16 | + reboot, for post-mortem after a failed boot. |
| 17 | + |
| 18 | +Both are written atomically (temp file + rename, so a reader never sees a partial |
| 19 | +doc) with mode **0640, owner root, group adm** (group-readable by an `adm`-group |
| 20 | +monitoring agent; world-unreadable). This is the only channel that works when a |
| 21 | +node never joined the cluster - the very failure you most need to debug. |
| 22 | + |
| 23 | +The document carries **no secrets by construction**: every field except `message` |
| 24 | +is a closed enum, and `message` is sanitized (bootstrap tokens, PEM blocks, and |
| 25 | +long secret-like runs are redacted; it is truncated to one short line). |
| 26 | + |
| 27 | +### Example |
| 28 | + |
| 29 | +```yaml |
| 30 | +apiVersion: provider-kubernetes.kairos.io/v1 |
| 31 | +phase: Failed |
| 32 | +role: worker |
| 33 | +membership: uninitialized |
| 34 | +outcome: failure |
| 35 | +reason: ControlPlaneUnreachable |
| 36 | +terminal: false |
| 37 | +lastAction: wait-for-control-plane |
| 38 | +message: "control-plane endpoint not reachable within budget" |
| 39 | +budget: |
| 40 | + attempts: 3 |
| 41 | + maxAttempts: 3 |
| 42 | +updatedAt: "2026-06-03T12:00:00Z" |
| 43 | +bootID: 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
| 44 | +version: v0.2.0 |
| 45 | +``` |
| 46 | +
|
| 47 | +### Fields |
| 48 | +
|
| 49 | +| Field | Meaning | |
| 50 | +|-------|---------| |
| 51 | +| `phase` | `Reconciling` (in progress), `Converged` (success), `Failed`, or `Reset`. | |
| 52 | +| `role` | The node's declared role: `init`, `controlplane`, or `worker`. | |
| 53 | +| `membership` | `uninitialized`, `initialized`, or `joined` (probed actual state). | |
| 54 | +| `outcome` | `success` or `failure`. | |
| 55 | +| `reason` | Closed enum, empty on success. See table below. | |
| 56 | +| `terminal` | `true` if the failure will not be retried on the next boot (fail-fast); `false` if a later boot may converge. | |
| 57 | +| `lastAction` | The last kubeadm action attempted (e.g. `run-join`, `upgrade-apply`). | |
| 58 | +| `message` | One short, sanitized, operator-facing line. | |
| 59 | +| `budget` | `attempts` used out of `maxAttempts`. | |
| 60 | +| `updatedAt` | RFC3339 timestamp of this status. | |
| 61 | +| `bootID` | The kernel boot ID, so you can tell which boot produced it. | |
| 62 | +| `version` | The provider build version. | |
| 63 | + |
| 64 | +### Reason codes |
| 65 | + |
| 66 | +| `reason` | What happened | |
| 67 | +|----------|---------------| |
| 68 | +| `ControlPlaneUnreachable` | The control-plane endpoint was not reachable within the budget. | |
| 69 | +| `JoinTimeout` | A join did not complete within the budget. | |
| 70 | +| `InitRefused` | A second `role: init` was refused to avoid clobbering an existing cluster (#4099-5). | |
| 71 | +| `UpgradeRefused` | A downgrade, skip-level, or out-of-window upgrade pin was refused. | |
| 72 | +| `BudgetExhausted` | The bounded retry budget ran out. | |
| 73 | +| `KubeadmError` | A kubeadm action failed. | |
| 74 | +| `ConfigInvalid` | The supplied cluster config was invalid (e.g. empty/short `cluster_token`). | |
| 75 | +| `ResetFailed` / `ResetOK` | The outcome of an `EventClusterReset`. | |
| 76 | + |
| 77 | +## Layer 2 - Node annotations (when the node is a cluster member) |
| 78 | + |
| 79 | +When - and only when - the node is already a member and a local kubeconfig exists, |
| 80 | +the provider **also** records the same outcome as annotations on its own Node |
| 81 | +object, so you can see it cluster-wide without SSH: |
| 82 | + |
| 83 | +```sh |
| 84 | +kubectl get node <name> -o jsonpath='{.metadata.annotations}' | tr ',' '\n' | grep provider-kubernetes.kairos.io |
| 85 | +``` |
| 86 | + |
| 87 | +Keys published (all under `provider-kubernetes.kairos.io/`): |
| 88 | +`phase`, `outcome`, `reason`, `terminal`, `last-action`, `updated-at`, `version`. |
| 89 | + |
| 90 | +Notes: |
| 91 | + |
| 92 | +- The free-text `message` is **not** published as an annotation - only the closed |
| 93 | + enum fields are, so an annotation can never carry a secret. |
| 94 | +- These annotations are readable by any principal with `get node`. They are coarse |
| 95 | + lifecycle metadata and intentionally carry no sensitive data. |
| 96 | +- The write uses the **least-privilege** credential already on the node |
| 97 | + (`kubelet.conf`, the `system:node:<name>` identity, preferred over `admin.conf`). |
| 98 | + The provider creates **no** new RBAC, ServiceAccount, or token. |
| 99 | +- If the node is not a member, has no kubeconfig, or the API is unreachable, this |
| 100 | + layer is silently skipped - Layer 1 still has the truth. |
| 101 | + |
| 102 | +## How to use it |
| 103 | + |
| 104 | +- A node didn't come up? Read `/run/provider-kubernetes/status.yaml` (current boot) |
| 105 | + or, after a reboot, the `/var/log` mirror. `phase: Failed` + `reason` + `message` |
| 106 | + tells you what to fix; `terminal: true` means the next boot won't retry on its |
| 107 | + own. |
| 108 | +- Watching a fleet? Scrape the Node annotations with `kubectl`. |
| 109 | + |
| 110 | +See also [Lifecycle and reset](./lifecycle.md) and |
| 111 | +[Troubleshooting](./troubleshooting.md). |
0 commit comments