One feature announced as one thing was three gates with three different beta lengths, all deleted two releases after the GA this post celebrates, and on a cluster with no CSI driver what is left is a controller that runs, a field that validates, and a claim whose capacity never moves¶
Post — Kubernetes 1.24: Volume Expansion Now A Stable Feature, 2022-05-05, Kubernetes v1.24. 103 lines, 5,097 bytes, one author and one vendor — well under 2022's median post of 7,935 bytes, and the first of the year's thirteen walked posts.
As written — the post opens with a version history and a promise, and the history is one sentence long:
Volume expansion was introduced as a alpha feature in Kubernetes 1.8 and it went beta in 1.11 and with Kubernetes 1.24 we are excited to announce general availability(GA) of volume expansion.
Then the instruction, at :19-37: edit the spec of a PersistentVolumeClaim, ask for more storage,
and watch. The post is specific about where to watch:
Once you've changed the requested size, watch the
status.conditionsfield of the PVC to see if the resize has completed. [...] When Kubernetes starts expanding the volume - it will addResizingcondition to the PVC, which will be removed once expansion completes.
:46-70 is the qualifier. Not everything is expandable: in-tree hostPath volumes are not
expandable at all, a CSI driver must carry the EXPAND_VOLUME capability, and the StorageClass
must opt in. The post prints one StorageClass to show what opting in looks like:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp2-default
provisioner: kubernetes.io/aws-ebs
parameters:
secretNamespace: ""
secretName: ""
allowVolumeExpansion: true
:72-90 separates online from offline expansion and closes the year's best typo — His behaviour
has been changed — and :92-103 is a two-item forecast: recovery from a failed expansion,
introduced in v1.23, and a proposal to expand every claim under a StatefulSet by editing the
StatefulSet.
As it runs now — the instruction still works, and on this lab it does nothing at all, for a reason the post states itself and does not connect to its own example.
One feature, three gates. The post says alpha in 1.8, beta in 1.11, GA in 1.24, and that is
the history of ExpandPersistentVolumes — one of three gates whose graduation the post is
announcing. The online-expansion section is ExpandInUsePersistentVolumes, alpha at 1.11 and beta
at 1.15; the storage-driver section is ExpandCSIVolumes, alpha at 1.14 and beta at 1.16. Three
features that started six releases apart were switched on together at 1.24 and described as one, and
the post names none of the three. The ladder below is what the sentence flattens.
All three are gone, and the population says that is rare in this exact way. Each of the three
gate files carries removed: true and a stable row ending at 1.26, so all three stopped
existing at v1.27. Of the 487 gate files at the pin, 230 are removed, and 182 of those reached
stable first — removal after GA is the normal end of a gate's life, not an unusual one. What is
unusual is the cohort: exactly five gates carry a stable range of 1.24 through 1.26, and
three of the five are these. The other two are CSIMigrationAzureDisk and
ControllerManagerLeaderMigration.
The controller outlived its switches. kube-controller-manager.md:437 lists
persistentvolume-expander-controller among the controllers enabled by default, with no gate
anywhere that could turn it off. It is running on this cluster right now with nothing to do, which
is the sharpest available statement of what GA meant here.
The list of expandable volume types has emptied of everything you can build by hand.
persistent-volumes.md:381-387 says expansion works for csi, flexVolume (deprecated) and
portworxVolume (deprecated) — three entries, two of them marked deprecated in the same list. The
supported-plugin list 120 lines later at :501-509 offers six in-tree types you can still create —
csi, fc, hostPath, iscsi, local, nfs — and not one of the five non-CSI entries appears
in the expandable list. The post's own sentence, that in-tree hostPath is not expandable at all,
is still exactly right, and it is now the general case rather than the exception it was written as.
The StorageClass example names a provisioner the documentation no longer contains.
kubernetes.io/aws-ebs occurs zero times under content/en/docs at the pin and six times in
the blog archive. The volume type survives — awsElasticBlockStore is still in the deprecated
plugin list at :515-516, marked migration on by default starting v1.23 — but the in-tree
provisioner string the post's example turns on is not written down anywhere a reader could check it.
The parameters are stranger still: the pin's equivalent example at :391-403 carries resturl,
restuser, secretNamespace and secretName under a deliberately fictional
vendor-name.example/magicstorage, and the post's version keeps two of those four keys under a real
provisioner name. Those keys belong to GlusterFS, which the pin records as not available starting
v1.26.
EXPAND_VOLUME is not a word the documentation knows. The capability the post tells you to look
for in your driver occurs zero times under content/en/docs and twice in the whole archive — here,
and in the 2021 post that the access-mode
exercise walks, which found the same silence
for SINGLE_NODE_MULTI_WRITER. The CSI specification's vocabulary reaches Kubernetes users through
blog posts or not at all; two posts a year apart are the second and third data points for that.
And the pin disagrees with itself about where to watch. This is a statement about the
documentation rather than about the post, so it belongs here and not in the next section.
persistent-volumes.md never mentions a Resizing condition; its recovery section at :488 tells
you to watch .status.allocatedResourceStatuses. The generated API reference still carries the
post's advice: persistent-volume-claim-v1.md:138 says "If underlying persistent volume is being
resized then the Condition will be set to 'Resizing'", and :229 repeats it for the condition's
reason. Both are true; one is five years older than the other, and the hand-written page has moved
on without the generated one following. The controller has the same problem in the other direction:
rbac.md:828 calls it system:controller:expand-controller while kube-controller-manager.md:437
calls it persistentvolume-expander-controller, two files in the same reference directory naming
one component twice.
What this exercise does not cover, and where it lives. The two-phase mechanics — which phase
sets FileSystemResizePending, which clears it, what must be true of the Pod before the node phase
runs, and the recovery drill that reduces a failed request — are an existing storage
lab, run on a pair topology with a working CSI driver
behind it. That lab is the happy path and its interesting failure. This exercise is deliberately the
opposite: one node, no driver, and every step below is about what the API does when the thing that
would carry out the resize is not there. The two do not overlap, and the lab is the one to run if
you want to see a volume actually get bigger.
The diff, and why — four of the seven cases.
Retired by being agreed with: the gates. The post asks you to celebrate that expansion is on by
default and no longer needs a switch. The project agreed so completely that two releases later it
deleted all three switches, and a reader who goes looking for ExpandPersistentVolumes today finds
a file whose frontmatter says removed: true and whose _build block says render: false — a page
that exists only so the ladder is not lost. The forecast was honoured, and the way you can tell is
that there is nothing left to point at.
Broke: the StorageClass example. A reader who copies :60-70 gets an object the API server will
accept and no provisioner will ever answer, under a name the documentation has erased. This is the
smallest kind of break and the most common in the storage posts: the mechanism is fine, the example
is addressed to a world that has been migrated out from under it. Step 3 builds the class the post
should have printed for a cluster with no cloud behind it.
Still right: the instruction, and the one negative claim that makes this lab silent. Edit the
claim, ask for more, the control plane resizes the volume underneath and never creates a new one —
all of that is persistent-volumes.md:405-407 word for word. And the post's aside that in-tree
hostPath is not expandable at all is still true, which is why the interesting half of this
exercise is what happens when the instruction is followed on a cluster that cannot carry it out. The
post never connects its own caveat to its own example; the reader who notices is the one who learns
what GA bought.
A plan the project abandoned: expansion through StatefulSets. :100-103 says the contributor
community is also discussing the potential for StatefulSet-driven storage expansion, and links an
enhancement proposal. Four years and thirteen releases later, statefulset.md contains zero
occurrences of the string expan — not the feature, not a forecast, not a note that it was dropped.
The link in :103 is the only occurrence of that enhancement's URL in the entire content tree. The
claim template is still the only way to size a StatefulSet's storage, and resizing one still means
editing every claim by hand.
Not the seventh case, and the near miss is worth recording. The pin does cite this post —
csi-nodeexpandsecret.md:41 says "Please read" and links it — but that citation is in another
blog post, and the seventh case requires a page under content/en/docs. EXPAND_VOLUME passes the
identifier test and the post fails the first one, so this is a post the archive leans on, not a post
the documentation leans on.
The ladder
Three gates, transcribed from their stages: lists parsed as YAML. Reading them side by side is the
point: the post's one-sentence history is the first row of the first table.
ExpandPersistentVolumes alpha false 1.8 - 1.10
beta true 1.11 - 1.23
stable true 1.24 - 1.26 removed
ExpandInUsePersistentVolumes alpha false 1.11 - 1.14
beta true 1.15 - 1.23
stable true 1.24 - 1.26 removed
ExpandCSIVolumes alpha false 1.14 - 1.15
beta true 1.16 - 1.23
stable true 1.24 - 1.26 removed
Thirteen releases of beta on the first gate is long and not remarkable: of 376 gates with a beta
stage, six sat in beta for exactly thirteen releases and fourteen sat there longer, the record being
AppArmor at twenty-seven. What the three tables show that no prose summary can is that the three
lines converge — three different alphas, three different betas, one shared stable range, one
shared deletion — which is the shape of a feature assembled from parts and shipped as a whole.
A fourth gate belongs to the post's forecast rather than its subject.
RecoverVolumeExpansionFailure, the v1.23 alpha :96-98 points at, is stable and locked: true
since 1.34 and is not laddered here; it is the one thing in Next steps that arrived. Step 6
tests the behaviour it locked in, because that behaviour is pure API validation and needs no driver.
Topology — solo: one node at 10.10.10.180, 4096MB, 4
vCPU, 25G, brought up with the provisioning sequence.
Two static directories on the node, two StorageClasses that provision nothing, and one edit to a
control-plane manifest. A second node would add nothing and would take something away: hostPath is
single-node by definition, persistent-volumes.md:503-505 says so in the plugin list itself, and
the whole argument here depends on the volume being one the cluster cannot resize.
Do
- Ask the cluster about all three gates before reading anything about them. The metric is the only machine-readable answer, and here the informative result is the empty one:
kubectl version -o json | python3 -c 'import json,sys
print("server:", json.load(sys.stdin)["serverVersion"]["gitVersion"])'
for G in ExpandPersistentVolumes ExpandInUsePersistentVolumes ExpandCSIVolumes \
RecoverVolumeExpansionFailure; do
printf '%-32s apiserver=%s\n' "$G" \
"$(kubectl get --raw /metrics | grep -c "kubernetes_feature_enabled.*\"$G\"" || true)"
done
kubectl get --raw /metrics | grep -c 'kubernetes_feature_enabled'
- Try to turn one of them back on, in the component that would do the resizing. The API server is left alone on purpose, so the cluster stays reachable whatever happens; the static pod restarts the moment the file is written:
sudo cp /etc/kubernetes/manifests/kube-controller-manager.yaml /root/kcm.yaml.bak
sudo sed -i '/- kube-controller-manager/a\ - --feature-gates=ExpandPersistentVolumes=true' \
/etc/kubernetes/manifests/kube-controller-manager.yaml
sleep 25
kubectl -n kube-system get pods -l component=kube-controller-manager
sudo crictl logs "$(sudo crictl ps -a -q --name kube-controller-manager | head -1)" 2>&1 | tail -6
sudo cp /root/kcm.yaml.bak /etc/kubernetes/manifests/kube-controller-manager.yaml
sleep 25
kubectl -n kube-system get pods -l component=kube-controller-manager
- Build the ground the post should have printed. Two StorageClasses that provision nothing,
differing in one field; two 1Gi
hostPathvolumes; two claims and two Pods, so that the same edit can be made against a class that opts in and a class that does not:
for i in 0 1; do sudo mkdir -p /srv/bw-exp/$i; done
kubectl apply -f - <<'YAML'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: bw-noexp }
provisioner: kubernetes.io/no-provisioner
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: bw-exp }
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true
YAML
for i in 0 1; do
SC=$([ $i = 0 ] && echo bw-noexp || echo bw-exp)
kubectl apply -f - <<YAML
apiVersion: v1
kind: PersistentVolume
metadata: { name: bw-exp-$i }
spec:
capacity: { storage: 1Gi }
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: $SC
hostPath: { path: /srv/bw-exp/$i }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: bw-exp-$i }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: $SC
volumeName: bw-exp-$i
resources: { requests: { storage: 1Gi } }
---
apiVersion: v1
kind: Pod
metadata: { name: bw-exp-$i }
spec:
containers:
- name: c
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
volumeMounts: [{ name: d, mountPath: /data }]
volumes:
- name: d
persistentVolumeClaim: { claimName: bw-exp-$i }
YAML
done
kubectl wait --for=condition=Ready pod/bw-exp-0 pod/bw-exp-1 --timeout=180s
kubectl get sc bw-noexp bw-exp -o custom-columns=NAME:.metadata.name,EXPAND:.allowVolumeExpansion
kubectl get pvc
- Follow the post's instruction against the class that never opted in. This is the one failure mode the post mentions in passing and never shows:
kubectl patch pvc bw-exp-0 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}' 2>&1 | tail -3
kubectl get pvc bw-exp-0 -o jsonpath='{.spec.resources.requests.storage}{"\t"}{.status.capacity.storage}'; echo
- Now follow it against the class that did opt in, and then wait. The polling loop is the step: the request is accepted, and the interesting measurement is how long nothing happens for:
kubectl patch pvc bw-exp-1 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}'
for i in $(seq 1 10); do
kubectl get pvc bw-exp-1 -o jsonpath='{.spec.resources.requests.storage}{"\t"}{.status.capacity.storage}{"\t"}{.status.conditions[*].type}{"\t"}{.status.allocatedResourceStatuses}{"\n"}'
sleep 6
done
kubectl describe pvc bw-exp-1 | tail -12
kubectl get events --field-selector involvedObject.name=bw-exp-1 -o wide | tail -5
kubectl -n kube-system logs -l component=kube-controller-manager --tail=200 2>/dev/null \
| grep -i 'resiz\|expand' | tail -5 || echo "no resize lines"
- Walk the validation boundary, which is the only part of this feature that works without a driver. Two edits that should be refused and one that the gate stable since v1.34 was written to allow — and the third is the one to record, because it is the behaviour the post's Next steps forecast:
kubectl patch pvc bw-exp-1 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"500Mi"}}}}' 2>&1 | tail -3
kubectl patch pvc bw-exp-1 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"1Gi"}}}}' 2>&1 | tail -3
kubectl patch pvc bw-exp-1 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"1500Mi"}}}}' 2>&1 | tail -3
kubectl get pvc bw-exp-1 -o jsonpath='{.spec.resources.requests.storage}{"\t"}{.status.capacity.storage}'; echo
- Reproduce the warning the concept page prints and the post does not. Grow the PersistentVolume first, then make the claim match it, and watch the control plane conclude that its work is already done:
kubectl patch pv bw-exp-1 --type=merge -p '{"spec":{"capacity":{"storage":"2Gi"}}}'
kubectl get pv bw-exp-1 -o jsonpath='{.spec.capacity.storage}'; echo
kubectl patch pvc bw-exp-1 --type=merge \
-p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}'
sleep 20
kubectl get pvc bw-exp-1 -o jsonpath='{.spec.resources.requests.storage}{"\t"}{.status.capacity.storage}{"\t"}{.status.conditions[*].type}'; echo
sudo df -h /srv/bw-exp/1 | tail -1
kubectl exec bw-exp-1 -- df -h /data | tail -1
- Find the controller that has been idle through all of this, and find both of its names. One is in the binary's own flag documentation and one is in the RBAC that ships with the cluster:
kubectl -n kube-system get pod -l component=kube-controller-manager \
-o jsonpath='{.items[0].spec.containers[0].command}' | tr ',' '\n' | grep -i 'controller\|feature'
kubectl get clusterrole system:controller:expand-controller \
-o jsonpath='{range .rules[*]}{.apiGroups}{" "}{.resources}{" "}{.verbs}{"\n"}{end}'
kubectl get clusterrolebinding system:controller:expand-controller \
-o jsonpath='{.subjects}'; echo
kubectl get clusterrole -o name | grep -c 'expander' || echo "0 roles named expander"
- Census the vocabulary in the pinned tree. Four names the post uses, one of which the documentation has never used, one it has erased, and one that survives only in generated output:
cd /path/to/kubernetes/website
for S in EXPAND_VOLUME kubernetes.io/aws-ebs allowVolumeExpansion allocatedResourceStatuses; do
printf '%-28s docs=%s blog=%s\n' "$S" \
"$(grep -rl "$S" content/en/docs --include='*.md' | wc -l | tr -d ' ')" \
"$(grep -rl "$S" content/en/blog --include='*.md' | wc -l | tr -d ' ')"
done
grep -rn "'Resizing'" content/en/docs/reference/kubernetes-api/core/persistent-volume-claim-v1.md
grep -c 'Resizing' content/en/docs/concepts/storage/persistent-volumes.md || echo 0
grep -c -i 'expan' content/en/docs/concepts/workloads/controllers/statefulset.md || echo 0
sed -n '381,387p;501,509p' content/en/docs/concepts/storage/persistent-volumes.md
-
Offline, in the pinned checkout, measure the two things the ladder turns on: how many removed gates reached stable before they went, and how crowded the 1.24-to-1.26 stable range is:
cd /path/to/kubernetes/website python3 - <<'PY' import os, yaml G = "content/en/docs/reference/command-line-tools-reference/feature-gates" gates = {} for fn in sorted(os.listdir(G)): if not fn.endswith(".md") or fn == "index.md": continue gates[fn[:-3]] = yaml.safe_load(open(os.path.join(G, fn)).read().split("---")[1]) print("gate files:", len(gates)) rm = {k: v for k, v in gates.items() if v.get("removed")} print("removed:", len(rm)) def row(v, want): for s in v.get("stages") or []: if str(s.get("stage")).strip() == want: return s return None reached = {k: row(v, "stable") for k, v in rm.items()} reached = {k: s for k, s in reached.items() if s} print("removed after reaching stable:", len(reached)) print("removed without ever reaching stable:", len(rm) - len(reached)) cohort = [k for k, s in reached.items() if str(s.get("fromVersion")) == "1.24" and str(s.get("toVersion")) == "1.26"] print("stable 1.24-1.26 exactly:", sorted(cohort)) spans = [] for k, v in gates.items(): s = row(v, "beta") if not s: continue a = int(str(s["fromVersion"]).split(".")[1]) b = int(str(s["toVersion"]).split(".")[1]) if s.get("toVersion") else 37 spans.append((b - a + 1, k)) spans.sort(reverse=True) print("gates with a beta stage:", len(spans)) print("beta longer than 13:", len([x for x in spans if x[0] > 13]), "| exactly 13:", len([x for x in spans if x[0] == 13])) print("longest:", spans[0]) PY
Expect
Step 1 finds nothing for the three expansion gates and a number in the hundreds for the metric as a
whole. That is the answer: kubernetes_feature_enabled reports the gates the binary knows about,
and a binary compiled after v1.26 does not know these three exist. RecoverVolumeExpansionFailure
is the control — it should be present, at value 1, with a stage label of STABLE, which is what a
gate that is still a gate looks like.
Step 2 should stop the controller manager from starting. A removed gate is not an unset gate: the flag parser rejects a name it has no entry for, and the container crash-loops with a line naming the unrecognized gate. Read it before restoring, because it is the difference between off and gone and the cluster only says it out loud here. The restore is a file copy and the component is back inside half a minute; do not leave the flag in place.
Step 3 gives you two bound claims, two running Pods and two classes differing in one boolean. Both
PVCs report 1Gi for both the request and status.capacity, because a statically created volume
binds at the size it was declared with.
Step 4 is refused, and the message is the point: the API server names the class and the field, so
the reader learns that allowVolumeExpansion is enforced at admission and not by the controller.
The claim's request is unchanged afterwards — nothing was half-applied.
Step 5 is accepted and then silent. spec reads 2Gi, status.capacity stays 1Gi, and
status.conditions and status.allocatedResourceStatuses stay empty for the whole minute the loop
runs. There is no error, no event and no controller log line, because there is no plugin that claims
this volume: hostPath is not in the expandable list at persistent-volumes.md:381-387 and there
is no CSI driver installed. This is what the post's GA looks like on a cluster the post was not
written for — a request that is valid, accepted, recorded, and never acted on. A reader who trusted
status.conditions would wait forever for a condition that is never set.
Step 6 is three edits and the third is the finding. 500Mi is below status.capacity and is
refused outright — Kubernetes does not shrink volumes, which persistent-volumes.md:490-493 states
flatly. 1Gi is exactly status.capacity; record which way that one goes, because the boundary is
inclusive or it is not and the pin does not say. 1500Mi is below the previous request and above
the current capacity, which is precisely the window RecoverVolumeExpansionFailure opened, and the
pin describes it at :479-493 for a PVC whose expansion failed rather than one whose expansion
was never attempted. Whether the validation cares about the difference is the thing this step
measures, and the pin will not tell you.
Step 7 reproduces the warning at :409-417 exactly. The PV goes to 2Gi, the claim goes to 2Gi,
and nothing resizes — but for the second reason, not the first. On a cluster with a real driver this
is a trap, because the control plane compares desired to desired and concludes the work is done.
Here the work was never going to be done anyway, so what you are checking is that the two states are
indistinguishable from outside: df inside the Pod and df on the node both report the size of the
node's filesystem, because a hostPath volume has no size of its own and never did. The number in
capacity was always a label.
Step 8 finds the controller under both of its names. The command line carries no --controllers
flag at all, which means the default set, which means persistentvolume-expander-controller is
running; and system:controller:expand-controller exists as a ClusterRole with permissions on
persistentvolumeclaims and persistentvolumeclaims/status. Two names, one component, both
shipped. The last command should find no ClusterRole spelled expander — the RBAC objects use the
older spelling and the flag documentation uses the newer one.
Step 9 should report EXPAND_VOLUME at docs=0 blog=2, kubernetes.io/aws-ebs at docs=0 blog=6,
allowVolumeExpansion present in both, Resizing absent from the concept page and present twice in
the generated reference, and expan absent from statefulset.md entirely. The two sed ranges
printed side by side are the sentence this exercise turns on: three expandable types, six creatable
types, and an intersection of one that you cannot build without a driver.
Step 10 should report 487 gate files, 230 removed, 182 of them removed after reaching stable and 48
removed before ever getting there, the five-gate cohort with a 1.24-to-1.26 stable range, 376
gates carrying a beta stage, fourteen with a beta longer than thirteen releases and six at exactly
thirteen, and AppArmor at twenty-seven as the record.
Read on
Five, and the last one is the question this exercise cannot close.
-
Expansion is two phases, and you can get stuck — the same feature with a driver underneath it, on a two-node topology. Everything this exercise measures by its absence, that lab measures by watching it happen.
-
The access mode whose gate was deleted — the other half of the CSI-vocabulary finding. That exercise measured
SINGLE_NODE_MULTI_WRITERat zero occurrences in the documentation; this one measuresEXPAND_VOLUMEat zero. Same specification, same silence, a year apart. -
The post that became the documentation — where
allowVolumeExpansion's neighbours on the StorageClass came from, and the earliest exercise in the corpus built onkubernetes.io/no-provisioner, which is the trick that makes this one runnable at all. -
persistent-volumes.md:453-495in the pinned tree — the recovery section, written as two tabs for two different people. It is the only part of this feature's documentation that assumes the expansion will fail, and it is what the post's Next steps was pointing at. -
Unanswerable from the pin: whether
kubernetes.io/aws-ebsever acceptedsecretNamespaceandsecretName. The in-tree plugin's parameter reference is gone from the tree along with the provisioner name, so the pin can show that the post's four-key example matches a GlusterFS example and cannot show whether the two keys it kept were ever valid for the provisioner it moved them to. The example may have been wrong the day it was published; nothing in this checkout can settle it.
Teardown
Objects first, then the node, then the control-plane manifest — and check the manifest even if you think you restored it in step 2:
kubectl delete pod bw-exp-0 bw-exp-1 --ignore-not-found
kubectl delete pvc bw-exp-0 bw-exp-1 --ignore-not-found
kubectl delete pv bw-exp-0 bw-exp-1 --ignore-not-found
kubectl delete storageclass bw-noexp bw-exp --ignore-not-found
sudo rm -rf /srv/bw-exp
ls -l /etc/kubernetes/manifests/kube-controller-manager.yaml
sudo grep -c 'feature-gates' /etc/kubernetes/manifests/kube-controller-manager.yaml || true
sudo rm -f /root/kcm.yaml.bak
kubectl -n kube-system get pods -l component=kube-controller-manager
Leave the guest up if you are going straight on; nothing here has changed the node beyond one directory tree and one manifest edit that step 2 undid. Otherwise destroy it.