Migrating from Skyhook to NodeWright
July 21, 2026 · View on GitHub
STATUS: DRAFT. This guide describes the in-progress
skyhook.nvidia.com->nodewright.nvidia.comAPI rename. It is written against the mirror-based upgrade flow. Do not treat version numbers here as final.BREAKING CHANGE. The primary CRD moves group and Kind:
skyhook.nvidia.com/v1alpha1 Skyhook->nodewright.nvidia.com/v1alpha1 NodeWright.DeploymentPolicymoves group (same Kind name). The on-node annotation/label/finalizer prefix movesskyhook.nvidia.com/* -> nodewright.nvidia.com/*, and the kubectl plugin is renamedkubectl skyhook -> kubectl nodewright.Scope of the current release. This guide is written against the full migration flow, and most of it has shipped. Available now: the
nodewright.nvidia.comAPI group and CRDs, the mirror controller that pre-creates aNodeWright/DeploymentPolicyfor each legacy object, new-group admission webhooks, legacy deprecation warnings, the primary reconciler reconcilingNodeWright, the on-node annotation/label/condition copy to thenodewright.nvidia.com/*prefix on upgrade (so packages are not re-run), the deferred rollback-safe cleanup of the legacy-labeled package pods and per-node ConfigMaps (gated byLEGACY_CLEANUP_DELAY), the runtime migration hold that waits while any legacySkyhookis non-complete, the renamed CLI plugin (binarykubectl-nodewright), and the Helm chart shipping both groups' CRDs and RBAC. Not yet available (planned for the removal release, do not rely on it today): the chart's pre-upgrade safety hook that refuses to drop the legacy CRD while legacy objects remain. Sections below flag this inline.
TL;DR
- Upgrade the operator. It automatically mirrors every existing
Skyhookinto aNodeWright(and each legacyDeploymentPolicyinto a new-groupDeploymentPolicy). You do not touch your CRs for this step. On upgrade the operator copies the per-node state (annotations, conditions) to thenodewright.nvidia.com/*prefix, so the migratedNodeWrightpicks up live node lifecycle position and packages are not re-run. The oldskyhook.nvidia.com/*state is kept for a rollback window and pruned later (see Rollback window). - Rename your CRs (in git for GitOps, or by re-applying for Helm/manual): change
apiVersionandkind, keep the samemetadata.name. This is a mechanical group/kind swap with no change to the spec body; see the sed one-liner in the CLI reference. - Delete the old
Skyhook/DeploymentPolicyCRs, but only after confirming the correspondingNodeWrightexists and is being reconciled (see safe deletion). Delete the legacyDeploymentPolicyobjects too. The mirroredNodeWrightobjects are the real, reconciled resources and are never deleted by this step.
Recommended runbook: migrate on a quiesced cluster. In order: quiesce (no Skyhook mid-rollout:
none in_progress/erroring/blocked/waiting; complete, paused, and disabled are fine to leave
as-is) -> upgrade the
operator -> rename the CRs atomically (steps 2 and 3 as one change: a rename = delete old + add new
together) -> delete the legacy CRs promptly. The sharp edges this guide warns about (a wedged migration
hold if a Skyhook is not complete, and rejected writes once the legacy CRs go read-only) all come from
deviating from that steady state.
What the operator does automatically
On upgrade, a one-way, level-triggered mirror controller runs:
- For each legacy
Skyhook/DeploymentPolicy, it creates the equivalentnodewright.nvidia.comobject, stampednodewright.nvidia.com/mirrored-from: <legacy-name>. - It is read-only on your legacy objects (never writes to them), so GitOps tools see no drift on the CRs you still manage.
- It backs off if a
NodeWrightof that name already exists that it did not create (a user/GitOps managed object always wins). - It never deletes a
NodeWright, even when the legacy object is deleted; once mirrored, theNodeWrightholds live workload state.
Legacy Skyhook/DeploymentPolicy writes return a deprecation warning pointing here.
The legacy Skyhook/DeploymentPolicy is read-only once migrated; operate the NodeWright. The
post-rename operator's admission webhook rejects any spec or pause/disable change to a legacy
object. Deletions, finalizer edits, and identical re-applies are still allowed, so a steady-state GitOps
sync does not break; only a real edit is rejected. Run every spec and lifecycle change (pause, disable,
resume, enable) against the new object with kubectl nodewright … or by editing the NodeWright /
nodewright DeploymentPolicy directly. A divergent edit to a legacy CR (including via Argo) surfacing as a
rejection is deliberate: it forces the rename rather than letting the legacy object drift from the object
the operator actually reconciles.
Alongside the mirror, the operator adopts the per-node state under the new prefix. It copies the legacy
per-node metadata (nodeState_, status_, version_, cordon_ annotations, status labels, and node
conditions) to the nodewright.nvidia.com/* prefix and adds a nodewright.nvidia.com label to the
per-node ConfigMaps, so the migrated NodeWright picks up live lifecycle position and does not re-run
package stages. It does this additively: the legacy skyhook.nvidia.com/* copies, the pre-rename
package pods, and the legacy ConfigMap labels are kept so a rollback stays possible, and are pruned
later, not on upgrade (see Rollback window).
Checking migration status: a Skyhook is migrated when a NodeWright of the same name with the
nodewright.nvidia.com/mirrored-from stamp exists:
kubectl get nodewrights.nodewright.nvidia.com -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.nodewright\.nvidia\.com/mirrored-from}{"\n"}{end}'
Argo CD (GitOps)
The key property: because the operator pre-creates the NodeWright, when your git later declares it,
Argo finds it already present and adopts it (applies its tracking label, shows in-sync) rather than
creating a duplicate. This assumes Argo's default label resource-tracking method; if you run with
annotation or annotation+label tracking (application.resourceTrackingMethod in argocd-cm), the same
adoption still applies but via the tracking annotation. Orphaned-resource monitoring on the AppProject
is separate: it only controls warnings about untracked objects and does not affect adoption.
-
Upgrade the operator through its own Argo Application (bump the chart/image). The mirror imports your existing
Skyhooks intoNodeWrights. At this point theNodeWrights exist but are not part of any Argo Application (untracked) - Argo neither prunes nor flags them, and yourSkyhookApplication stays in-sync because the mirror never writes to yourSkyhooks. -
Rename the CRs in git as a single commit. For each
Skyhook, change:apiVersion: skyhook.nvidia.com/v1alpha1->nodewright.nvidia.com/v1alpha1kind: Skyhook->kind: NodeWright- keep
metadata.nameidentical.
Same for
DeploymentPolicy(onlyapiVersionchanges;kindstaysDeploymentPolicy). This is a mechanical group/kind swap; see the sed one-liner in the CLI reference. The commit should remove the oldSkyhookmanifest and add theNodeWrightmanifest so Argo prunes the old and adopts the new in one sync. -
Sync. Argo adopts the existing
NodeWright(in-sync, no re-create) and prunes theSkyhook. Once theSkyhookis gone, the mirror has nothing left to import for it.
Do not leave the old Skyhook in git after adding the NodeWright: in that window both the mirror
and Argo write the NodeWright, and editing it via git could be stomped by the mirror re-importing the
stale Skyhook. Renaming in one commit avoids this entirely.
If you keep CRDs in a separate Argo Application: during the transition it should track both CRDs. The
operator Helm chart ships both groups' CRDs and RBAC, so an operator upgrade installs the
nodewright.nvidia.com CRDs for you. Do not remove the skyhook.nvidia.com CRD until every Skyhook is
migrated and deleted (removing the CRD cascade-deletes any remaining Skyhooks). See
Removal.
Helm (helm upgrade)
Helm here refers to installing the operator via its chart. Your Skyhook/NodeWright instances
are usually authored separately (not part of the operator chart); adjust to your setup.
-
helm upgradethe operator chart. The mirror imports your existingSkyhooks intoNodeWrights. The chart ships the CRDs and RBAC for both groups, so the upgrade installs thenodewright.nvidia.comCRDs for you. Per-node state is migrated on upgrade (see above), so packages are not re-run. -
Update your CR manifests to
NodeWrightwherever you keep them, and apply. This is a mechanical group/kind swap with no change to the spec body:sed -e 's|skyhook\.nvidia\.com/|nodewright.nvidia.com/|g' \ -e 's|kind: Skyhook|kind: NodeWright|' my-skyhook.yaml > my-nodewright.yaml kubectl apply -f my-nodewright.yamlBecause the operator already created the
NodeWrightvia the mirror, this apply is a no-op adoption of the existing object (same name, same spec) rather than a fresh create. -
Delete the old CRs once you have confirmed the
NodeWrightexists and reconciles (see safe deletion):kubectl delete skyhook.skyhook.nvidia.com <name> kubectl delete deploymentpolicy.skyhook.nvidia.com <name> # legacy DeploymentPolicies tooThe
NodeWrightremains (the mirror never deletes it).
Planned, not yet in this release: at the removal release the chart is intended to add a pre-upgrade
safety hook (legacy.blockUpgradeIfPresent) that aborts the upgrade if any legacy
Skyhook/DeploymentPolicy still exist, so you cannot accidentally drop the old CRD (and cascade-delete
live objects) before migrating. That hook is not implemented yet; until it ships, verify manually that no
legacy objects remain before removing the legacy CRD.
Manual / kubectl
- Upgrade the operator (apply the new manifests / image, or
helm upgradethe chart, which ships both groups' CRDs and RBAC). The mirror imports yourSkyhooks and the per-node state is migrated on upgrade. - Convert your CR manifests by hand (change
apiVersion/kind, keepmetadata.name) and apply them. The mirror has already created theNodeWrightobjects, so this apply is a no-op adoption:sed -e 's|skyhook\.nvidia\.com/|nodewright.nvidia.com/|g' \ -e 's|kind: Skyhook|kind: NodeWright|' my-skyhook.yaml > my-nodewright.yaml kubectl apply -f my-nodewright.yaml # adopts the mirrored object - Delete the old CRs only after confirming the
NodeWrightreconciles (see safe deletion); delete legacyDeploymentPolicyobjects too:kubectl delete skyhooks.skyhook.nvidia.com --all kubectl delete deploymentpolicies.skyhook.nvidia.com --all
Safe deletion ordering
Delete a legacy Skyhook (or DeploymentPolicy) only after you have confirmed its NodeWright
counterpart exists and is being reconciled. The mirror never deletes a NodeWright, but deleting the
legacy object before the new one is live loses your intent. For each object:
- Confirm the mirrored
NodeWrightexists and carries thenodewright.nvidia.com/mirrored-fromstamp (see the status check above). - Confirm it is reconciling (its
.statusis populated and it is not erroring). - Then delete the legacy
Skyhook/DeploymentPolicy.
A legacy Skyhook created by the pre-rename operator carries the old skyhook.nvidia.com/skyhook
finalizer, so kubectl delete briefly parks the object in Terminating until the mirror strips that
finalizer (the post-rename operator manages a finalizer only on NodeWright). This is automatic and
usually completes in seconds; you do not need to remove the finalizer by hand. It also means deletion
only completes while the operator is running, so delete legacy objects before you scale the operator
down for the final removal step.
Delete legacy DeploymentPolicy objects as part of the same pass: they move group too, and a stale legacy
DeploymentPolicy left behind will keep emitting deprecation warnings and block eventual removal of the
skyhook.nvidia.com CRD.
Ordering within the pass matters: the legacy DeploymentPolicy admission webhook rejects deletion
while any legacy Skyhook still references it. Delete the referencing Skyhooks first, then the
DeploymentPolicy. The Helm and Manual command sequences above already delete skyhooks before
deploymentpolicies for this reason.
Prerequisite: all Skyhooks must be complete
Perform the operator upgrade only when every Skyhook is complete with no nodes in progress, i.e.
no package is mid-rollout. This is a requirement, not just a recommendation: the per-node state
migration renames the operator's package pods and per-node ConfigMaps, and the flow assumes there is no
in-flight package work to disrupt. Upgrading against idle Skyhooks avoids handing a migrated NodeWright
a stage that was mid-rollout.
Check before upgrading:
kubectl get skyhooks.skyhook.nvidia.com -o custom-columns=NAME:.metadata.name,STATUS:.status.status,INPROGRESS:.status.nodesInProgress
Proceed only when no Skyhook is actively rolling out. paused and disabled Skyhooks are fine to leave
as-is and do not need to be enabled or unpaused first: the mirror carries their paused/disabled
state onto the NodeWright, which then does not roll out, so they migrate in that state. The runtime hold
only waits on Skyhooks whose rollout is genuinely in flight (in_progress, erroring, blocked,
waiting, unknown). Finish, roll back, or delete those before upgrading (they show up in the STATUS
column above). If you have already upgraded and one is wedging the migration, delete the offending
legacy Skyhook (its status cannot advance without the old operator, and it is read-only) or roll back to
the pre-rename operator; the hold clears once it is gone or complete.
The operator enforces this at runtime as a fail-safe. On startup, while any legacy Skyhook is still
actively rolling out (see the statuses above), the operator
holds: it does not take over any node, requeues every 20 seconds, and logs a warning naming the
in-flight Skyhooks. This prevents the new operator from double-driving a node the pre-rename operator may
still be mutating. It is a stop, not an auto-resume: because a legacy Skyhook's status is frozen once the
operator stops reconciling that kind, an in-flight hold clears only when those Skyhooks are finished (roll
back to the pre-rename operator, let the rollout complete, then upgrade again) or deleted. A complete,
paused, or disabled Skyhook, and a legacy Skyhook created after the upgrade (with no status,
mirrored into a NodeWright), do not trigger the hold.
Rollback window (LEGACY_CLEANUP_DELAY)
The migration hold above keeps rollback safe while a rollout was in flight at upgrade; the rollback
window keeps it safe after a Skyhook has finished migrating. When the operator adopts a node it
copies the legacy skyhook.nvidia.com/* annotations, labels, and conditions to the
nodewright.nvidia.com/* prefix but keeps the legacy copies, and it leaves the pre-rename package pods
in place. So a rolled-back pre-rename operator still finds its state and resumes cleanly, and a bad upgrade
stays recoverable even after migration completes.
The legacy copies (and the old package pods, and the legacy ConfigMap labels) are pruned only once
LEGACY_CLEANUP_DELAY has elapsed since the Skyhook was adopted, tracked by a
nodewright.nvidia.com/legacy-migrated-at timestamp on the NodeWright. The delay defaults to 24h
(controllerManager.manager.env.legacyCleanupDelay in the chart). Set it to 0 to prune immediately (no
rollback window), or longer to keep the door open longer. Pruning is the point of no return: after it
runs, rolling back to the pre-rename operator would re-apply every package from scratch.
This is transition-only behavior and is removed together with the skyhook.nvidia.com group at the
removal release.
Downstream consumers (e.g. aicr)
Projects that ship or depend on Skyhook CRs need a coordinated update. For example aicr (Argo-based) references Skyhook in several places; the equivalent changes for any such consumer are:
- CRs you deploy (e.g. aicr's
recipe.yamlSkyhook): renameapiVersion+kindtonodewright.nvidia.com/v1alpha1/NodeWright(keep the name). Because the operator pre-creates theNodeWright, Argo adopts it on next sync. - CRD readiness waits / assertions (e.g. chainsaw
assert-crds.yaml, waits onskyhooks.skyhook.nvidia.comEstablished): point atnodewrights.nodewright.nvidia.com. - Cleanup tooling referencing the
skyhook.nvidia.comgroup: add/switch tonodewright.nvidia.com.
These land as a companion PR in the consumer repo, timed with (or shortly after) the operator upgrade.
Verification checklist
kubectl get nodewrights.nodewright.nvidia.comlists aNodeWrightfor each formerSkyhook.- Node state carried over: a node's annotations are copied under
nodewright.nvidia.com/(the legacyskyhook.nvidia.com/keys remain during the rollback window) and no package re-ran. - No drift reported by Argo on your (soon-to-be-deleted)
Skyhookobjects during the transition. - After you delete the old CRs, the
NodeWrightobjects remain and reconcile normally. - Legacy
Skyhook/DeploymentPolicywrites emit the deprecation warning.
Removal (future release)
The legacy skyhook.nvidia.com group is kept for a multi-release migration window (targeting roughly two
releases, adoption-gated - not a fixed release number). In the removal release:
- The legacy admission webhook flips from warning to denying writes.
- The legacy CRDs are removed from the chart/manifests, sequenced behind a preflight that refuses removal while any legacy objects remain (the Helm/Argo pre-upgrade hook, planned above).
- Removing the
skyhook.nvidia.comCRD cascade-deletes any remainingSkyhooks, so migrate before the removal release.
FAQ
Will my nodes reboot during the upgrade? No (unless a package interrupt was mid-flight, and even then
the agent skips an already-completed interrupt). Per-node state is migrated and legacy workloads are
cleaned up, so packages are not re-run; still upgrade against idle, complete Skyhooks as required by the
prerequisite above.
Does the operator write to my Skyhook objects? No. The mirror is read-only on the source, so GitOps
tools see no drift. "Migrated" is derived from the existence of the stamped NodeWright.
What if I create a new Skyhook after upgrading? It still works (the CRD is served during the
transition) and gets mirrored, but you will get a deprecation warning. Prefer creating NodeWright
directly.
Can I roll the operator back? Yes, within the rollback window. The operator keeps the legacy node
state, package pods, and Skyhook CRs during the transition, so rolling back to a version that reconciles
Skyhook resumes cleanly. Two things close the door: the per-node prune after LEGACY_CLEANUP_DELAY
(default 24h; see Rollback window), after which a rolled-back
operator sees fresh nodes and re-applies packages; and deleting the Skyhooks, which strands the
NodeWrights. Keep the Skyhooks (and raise LEGACY_CLEANUP_DELAY) until you are confident.