Uninstall and cleanup¶
Tear down in the right order. Owner references remove the namespaced child resources, but cross-namespace, cluster-scoped, and hosted-cluster resources require a manual check.
Recommended order¶
flowchart TB
A["1. Delete InfraClusterAttachments<br/>(finalizer deletes named hosted resources,<br/>control-plane NetworkPolicy; inspect OLM leftovers)"]
--> B["2. Delete Infra resources<br/>(GC removes DHCP/DNS/proxy children<br/>and their namespaced workloads)"]
--> C["3. Check unowned resources<br/>(NetworkPolicy and DHCP cluster RBAC)"]
--> D["4. Delete NodePool + HostedCluster<br/>(HyperShift tears down control-plane<br/>namespace, VMs, PVCs)"]
--> E["5. Remove ExternalDNS resources<br/>and public DNS records"]
--> F["6. make undeploy + make uninstall<br/>(operator, RBAC, CRDs)"]
1. Delete attachments first¶
Each InfraClusterAttachment finalizer deletes the configured hosted-cluster
Service, IPAddressPool, L2Advertisement, MetalLB, and Subscription by
name, plus its control-plane NetworkPolicy:
kubectl -n clusters get infraclusterattachment
kubectl -n clusters get infraclusterattachment <name> \
-o jsonpath='{.status.controlPlaneNamespace}{"\n"}'
kubectl -n clusters delete infraclusterattachment <name>
Wait for each attachment to disappear before continuing; a stuck finalizer usually means the hosted cluster is unreachable.
Cleanup is name-based and does not verify ownership. Use names dedicated to oooi. It does not remove OLM CSVs, InstallPlans, operator workloads, or public DNS records; inspect those separately after the attachment is gone.
If the operator is already removed and attachments are stuck Terminating,
remove the finalizers manually:
kubectl -n clusters patch infraclusterattachment <name> --type=merge \
-p '{"metadata":{"finalizers":null}}'
2. Delete Infra second¶
Owner references cascade-delete every child object:
kubectl -n clusters get infra
kubectl -n clusters delete infra <name>
# Child CRs and their namespaced workloads disappear as GC completes:
kubectl -n clusters get dhcpserver,dnsserver,proxyserver # → No resources found
kubectl -n clusters get all -l app=proxy-server
Each attachment-owned external proxy Service is removed when its attachment is
deleted. Apps-ingress resources created in the hosted cluster are not owned by
Infra.
Check the hosted cluster for oooi-ingress, metallb, IPAddressPool, and
L2Advertisement; remove resources that are no longer required before
expecting the apps-ingress VIP to be released.
3. Clean unowned resources¶
Record each attachment's resolved status.controlPlaneNamespace before deleting
it. The control-plane NetworkPolicy is cross-namespace and DHCP's KubeVirt reader
RBAC is cluster-scoped, so Kubernetes cannot use Infra as their owner. Check
and remove those resources after deleting the Infra resource:
kubectl -n <control-plane-namespace> delete networkpolicy allow-infrastructure --ignore-not-found
kubectl delete clusterrole <name>-dhcp-kubevirt-reader --ignore-not-found
kubectl delete clusterrolebinding <name>-dhcp-kubevirt-reader --ignore-not-found
4. Delete the hosted cluster¶
kubectl -n clusters delete nodepool <name> --wait=false
kubectl -n clusters delete hostedcluster <name>
HyperShift's finalizer then removes the resolved control-plane namespace, KubeVirt VMs, and PVCs. This can take several minutes:
Note
The shared clusters namespace is not deleted — only the per-cluster
control-plane namespace is.
5. Remove cluster-specific ExternalDNS instances¶
If you followed Public DNS and OAuth publishing, each hosted cluster may have its own watcher. Delete it before relying on record cleanup — a watcher pointed at a destroyed cluster can no longer sync:
kubectl -n external-dns-operator delete deploy <cluster>-external-dns \
serviceaccount/<cluster>-external-dns \
secret/<cluster>-external-dns-kubeconfig \
secret/<cluster>-external-dns-credentials --ignore-not-found=true
6. Clean public DNS records¶
- Records owned by an ExternalDNS running with
--policy=syncare deleted when their Services disappear. - Orphaned records (owner gone) must be removed from your provider manually, e.g. Route53:
aws route53 change-resource-record-sets --hosted-zone-id <ZONE_ID> \
--change-batch "$(aws route53 list-resource-record-sets \
--hosted-zone-id <ZONE_ID> --output json | jq '{Comment:"cleanup",
Changes: [.ResourceRecordSets[] | select(.Name | contains("<cluster>"))
| {Action:"DELETE", ResourceRecordSet:.}]}')"
Verify convergence (INSYNC) and that nothing resolves anymore.
7. Uninstall the operator¶
make undeploy ignore-not-found=true # Deployment, RBAC, oooi-system namespace
make uninstall ignore-not-found=true # CRDs
Leftover checklist¶
After teardown, sweep for cluster-specific remnants:
| Artifact | Check | Action |
|---|---|---|
| Namespaces | kubectl get ns \| grep <cluster> |
Delete leftover <cluster> / klusterlet-<cluster> namespaces |
| ACM import agents | kubectl get managedcluster |
Detach/delete; clear finalizers only if the managed API is already destroyed |
| ClusterRoles/Bindings | kubectl get clusterrole,clusterrolebinding -o name \| grep <cluster> |
Delete e.g. <cluster>-dhcp-kubevirt-reader, ACM addon roles |
| kube-system RBAC | kubectl -n kube-system get role,rolebinding \| grep <cluster> |
Delete ACM agent roles/bindings |
| Route53/zone records | Query your zone for <cluster> |
Delete per step 4 |
Re-installing later¶
Nothing persists that blocks reuse: re-run make install && make deploy, apply
a fresh Infra, and recreate the hosted cluster. Use fresh static IPs if any
doubt exists about old leases or VIP assignments.