Skip to content

Commit 588baa7

Browse files
wrkodeclaude
andauthored
docs: reflect cluster upgrades as implemented (#10)
PR #9 landed the upgrade lifecycle, so update the docs/README to match: - New docs/upgrades.md: operator runbook (model + explicit-pin trigger, supported edges, single-node and one-at-a-time HA upgrade, the automatic kubelet-config repair, best-effort encrypted etcd snapshots, rollback, common failures). - docs/lifecycle.md: Upgrades section now describes the implemented flow and links the runbook (was 'not yet implemented'). - docs index, configuration (kubernetesVersion bump triggers an upgrade) and troubleshooting (upgrade-didn't-run / refuse-upgrade / snapshot-skipped) updated. - README: 'What works today' adds the upgrade bullet; Status table flips Cluster upgrades to implemented/validated; full init/join/upgrade/reset lifecycle noted; Documentation table links the new page. Docs only; no code change. Signed-off-by: William Rizzo <william.rizzo@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent ff2bd58 commit 588baa7

6 files changed

Lines changed: 137 additions & 9 deletions

File tree

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ built and verified in CI):
5656
bounded-TTL join material (for control planes, re-uploading the cluster certs
5757
under a fresh certificate key), computes the cluster CA SPKI pin, derives the
5858
API endpoint from `admin.conf`, and prints a ready-to-paste join cloud-config.
59+
- **Cluster upgrades (`kubeadm upgrade`).** Pin a newer `kubernetesVersion` and
60+
boot the matching image; the provider converges the cluster one minor at a time
61+
(control plane via `upgrade apply`, followers/workers via `upgrade node`),
62+
refusing downgrades / skip-level / out-of-window. It auto-repairs the kubelet
63+
config when an image swap leaves the new kubelet unable to start, and takes a
64+
best-effort etcd snapshot (only onto encrypted storage). Validated live for
65+
1.34 -> 1.35. See [`docs/upgrades.md`](./docs/upgrades.md).
5966
- **CNI is the operator's choice.** The provider installs no CNI;
6067
[`samples/cni-calico/`](./samples/cni-calico/) shows two ways to add Calico
6168
(apply after the cluster is up, or bundle it in the control-plane
@@ -64,10 +71,9 @@ built and verified in CI):
6471
image build across the supported Kubernetes window; tagged releases publish
6572
per-minor images to ghcr (see "Released images").
6673

67-
Cluster bootstrap, credential handling, multi-control-plane HA, the reset
68-
handler, the image build, CI, and release publishing are implemented. The next
69-
major capability is cluster upgrades (`kubeadm upgrade`). See the roadmap
70-
discussion in the issue above.
74+
The full lifecycle - bootstrap, join, **upgrade**, and reset - plus credential
75+
handling, multi-control-plane HA, the image build, CI, and release publishing are
76+
implemented and validated on VMs. See the roadmap discussion in the issue above.
7177

7278
## Status
7379

@@ -81,7 +87,7 @@ discussion in the issue above.
8187
| Kairos image build (pinned, checksum-verified) | implemented |
8288
| CI (build/vet/test/lint + image build across 1.34/1.35/1.36) | implemented |
8389
| Release automation (per-minor images to ghcr + binary) | implemented |
84-
| Cluster upgrades (`kubeadm upgrade`) | not yet (next) |
90+
| Cluster upgrades (`kubeadm upgrade`) | implemented, validated on libvirt (1.34 -> 1.35) |
8591
| Field readiness | not ready |
8692

8793
## Building
@@ -171,6 +177,7 @@ Usage documentation lives in [`docs/`](./docs/):
171177
| [Creating a cluster](./docs/creating-a-cluster.md) | Single control plane plus workers. |
172178
| [High availability](./docs/high-availability.md) | Multi-control-plane (stacked etcd). |
173179
| [mint-join](./docs/mint-join.md) | The join-material CLI. |
180+
| [Upgrades](./docs/upgrades.md) | Upgrading between Kubernetes minors with `kubeadm upgrade`. |
174181
| [CNI](./docs/cni.md) | Installing a CNI. |
175182
| [Security model](./docs/security.md) | Tokens, the cert-key blast radius, CA pinning, at-rest encryption. |
176183
| [Lifecycle and reset](./docs/lifecycle.md) | Reconcile, reset, the version window, upgrades. |

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Usage documentation for **provider-kubernetes**, the Go-native
2121
| [Creating a cluster](./creating-a-cluster.md) | Single control plane, adding workers, the join-material flow. |
2222
| [High availability](./high-availability.md) | Multi-control-plane (stacked etcd): stable endpoint, one-at-a-time bring-up, failover. |
2323
| [mint-join reference](./mint-join.md) | The `agent-provider-kubernetes mint-join` subcommand. |
24+
| [Upgrades](./upgrades.md) | Upgrading between Kubernetes minors (`kubeadm upgrade`): pin + image, single-node and HA, kubelet-config repair, etcd snapshots, rollback. |
2425
| [CNI](./cni.md) | Installing a CNI (the provider installs none). |
2526
| [Security model](./security.md) | `cluster_token`, certificate-key blast radius, CA pinning, at-rest encryption, the trust boundary. |
2627
| [Lifecycle and reset](./lifecycle.md) | Reboot idempotency, reset / `EventClusterReset`, the supported version window, upgrades. |

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Common fields:
6464
```yaml
6565
config: |
6666
clusterConfiguration:
67-
kubernetesVersion: v1.34.0 # must be within the supported window
67+
kubernetesVersion: v1.34.0 # within the window; must match the image; bumping it triggers an upgrade (see docs/upgrades.md)
6868
controlPlaneEndpoint: "vip:6443" # stable endpoint; REQUIRED for HA
6969
imageRepository: registry.k8s.io
7070
networking:

docs/lifecycle.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Each released image bundles one Kubernetes minor; pick the matching image tag (s
5555

5656
## Upgrades
5757

58-
Cluster **upgrades** (`kubeadm upgrade`) are **not yet implemented** - this is the
59-
next planned capability. For now the lifecycle covers bootstrap, join, and reset.
60-
Track the roadmap in the project's issues.
58+
Cluster **upgrades** (`kubeadm upgrade`) are **implemented**: pin a newer
59+
`kubernetesVersion`, boot the matching image, and the provider converges the
60+
cluster on the next reconcile (control plane via `kubeadm upgrade apply`, followers
61+
and workers via `kubeadm upgrade node`, one minor at a time). A newer image without
62+
a pin bump does not auto-upgrade, and downgrade / skip-level / out-of-window
63+
targets are refused. See [Upgrades](./upgrades.md) for the full operator runbook
64+
(single-node and HA), the automatic kubelet-config repair, etcd snapshots, and
65+
rollback.

docs/troubleshooting.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ match the `kubeadm` binary in the image. A mismatch fails fast by design. Use th
5151
image tag for the minor you want, or adjust the pin. See
5252
[Lifecycle](./lifecycle.md#supported-version-window).
5353

54+
### Upgrade didn't run, or was refused
55+
56+
- **Nothing happened after booting a newer image.** An upgrade runs only when you
57+
bump `clusterConfiguration.kubernetesVersion` to the new minor; a newer binary
58+
alone is a no-op by design.
59+
- **Reconcile logs `refuse-upgrade`.** The pin is a downgrade, skip-level
60+
(e.g. 1.34 -> 1.36), or out-of-window. Upgrade one minor at a time within the
61+
window.
62+
- **Snapshot skipped warning.** The provider refuses to write a plaintext etcd
63+
snapshot when it can't confirm the persistent partition is encrypted - take a
64+
manual etcd backup before upgrading. See [Upgrades](./upgrades.md).
65+
5466
### A reset left a stale etcd member (HA)
5567

5668
If a control plane was reset while the cluster was unreachable, its etcd member is

docs/upgrades.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Upgrades
2+
3+
The provider upgrades a running cluster between supported Kubernetes minors by
4+
driving `kubeadm upgrade`. Upgrades are **image-driven and explicitly pinned**:
5+
you boot a newer provider-kubernetes image and bump the version pin; on the next
6+
boot the provider detects the delta and converges the cluster.
7+
8+
> Validated live for 1.34 -> 1.35 on a single control plane. Multi-node/HA upgrade
9+
> ordering is implemented (see below) but exercise it carefully. As always, this
10+
> provider is under active development and not field-ready.
11+
12+
## The model
13+
14+
- The **target version is the bundled `kubeadm` binary** in the image you boot.
15+
By ADR-3 an operator `kubernetesVersion` pin must equal the bundled minor, so the
16+
pin and the binary always agree.
17+
- An upgrade runs **only when you pin** `clusterConfiguration.kubernetesVersion` to
18+
the new minor. A newer image **without** a matching pin bump does **not**
19+
auto-upgrade - it is a no-op (so a routine image refresh never silently
20+
re-versions your cluster).
21+
- Only **+1 minor** at a time (kubeadm's rule). Skipping a minor (1.34 -> 1.36),
22+
downgrades, and out-of-window targets are **refused loudly** before anything
23+
destructive runs.
24+
25+
## Supported edges
26+
27+
Within the current window: **1.34 -> 1.35** and **1.35 -> 1.36**. To go 1.34 ->
28+
1.36, step through 1.35 (boot the 1.35 image first, let it converge, then the 1.36
29+
image).
30+
31+
## Upgrading a single control plane
32+
33+
1. Make sure the cluster is healthy (`kubectl get nodes`, control-plane pods
34+
Running). Take an **etcd backup** first - see "etcd snapshots" below.
35+
2. Bump the pin in the node's cloud-config (`/oem` on an installed node) to the new
36+
minor, e.g. `kubernetesVersion: v1.35.5`.
37+
3. Upgrade the OS image to the matching minor and reboot. Either:
38+
- `kairos-agent upgrade --source oci:<registry>/provider-kubernetes:<tag>-k8s1.35`
39+
(then reboot), or
40+
- boot the new image via your provisioning flow.
41+
4. On reboot the provider's reconcile detects that the bundled binary is a minor
42+
ahead of the cluster and runs `kubeadm upgrade apply <version>`. When the new
43+
kubelet cannot start against the old node config (a flag removed across the
44+
minor), the provider first repairs the kubelet config (see below), brings the
45+
control plane back, then applies. The kubelet is restarted onto the new version.
46+
5. Verify: `kubectl version` (server), `kubectl get nodes` (node at the new
47+
version, Ready).
48+
49+
## Upgrading multiple control planes / workers (HA)
50+
51+
Upgrade **one node at a time**, control planes first, then workers:
52+
53+
1. Upgrade the first control plane as above; wait until the cluster version has
54+
flipped and it is Ready.
55+
2. Upgrade each remaining control plane one at a time. A follower runs
56+
`kubeadm upgrade node` (the provider detects the cluster already advanced and
57+
does not re-apply); it waits, bounded, for the control plane to be healthy first.
58+
3. Upgrade workers last, one at a time; each runs `kubeadm upgrade node`.
59+
60+
This sequencing is the operator's responsibility (the same one-at-a-time delivery
61+
contract as HA join); the provider adds bounded health gates but builds no
62+
cross-node lock.
63+
64+
## The kubelet-config repair (why an upgrade "just works" after an image swap)
65+
66+
Kubernetes removes/renames kubelet flags and config keys across minors. Because a
67+
Kairos A/B image swap replaces the kubelet binary *before* any upgrade runs, the
68+
new kubelet can crashloop on a flag the old kubeadm wrote (for example
69+
`--pod-infra-container-image`, removed in 1.35), which would leave the control
70+
plane down. The provider handles this automatically: when an upgrade is due and the
71+
local API is unreachable, it runs `kubeadm init phase kubelet-start` to regenerate
72+
the kubelet config with the new kubeadm (no API, no secrets), which lets the
73+
kubelet start and the existing control plane return before `kubeadm upgrade apply`
74+
runs. You do not need to do anything for this.
75+
76+
## etcd snapshots
77+
78+
`kubeadm upgrade apply` mutates etcd and is largely forward-only. The provider
79+
takes a **best-effort** etcd snapshot before applying, but **only** onto an
80+
encrypted persistent partition (an etcd snapshot is a full plaintext dump of every
81+
cluster secret). If it cannot confirm the partition is encrypted, it **refuses to
82+
write a plaintext snapshot** and logs a warning - so on an unencrypted node you
83+
must take your own snapshot before upgrading. The provider never copies the
84+
snapshot off the node; backup custody is yours.
85+
86+
## Rollback
87+
88+
kubeadm upgrades (especially etcd) are forward-only; there is no automatic
89+
rollback. To recover, restore your etcd snapshot and boot the previous image. A
90+
node wedged mid-upgrade can be recovered with the [reset](./lifecycle.md) flow and
91+
re-joined. The provider never auto-resets a control plane on upgrade failure - it
92+
fails loud and leaves the node for you to inspect.
93+
94+
## What can go wrong
95+
96+
| Symptom | Cause / fix |
97+
|---------|-------------|
98+
| Reconcile logs `refuse-upgrade` | Skip-level, downgrade, or out-of-window pin. Pin only +1 minor within the window. |
99+
| Upgrade doesn't start | No version pin bump (a newer binary alone won't upgrade). Bump `clusterConfiguration.kubernetesVersion`. |
100+
| Pin/binary mismatch hard error | The pin minor must equal the bundled image's minor. |
101+
| Snapshot skipped warning | Persistent partition encryption unconfirmed - take a manual etcd backup. |
102+
103+
See also [Lifecycle and reset](./lifecycle.md) and [Troubleshooting](./troubleshooting.md).

0 commit comments

Comments
 (0)