You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(bind-mount): add validated bind mount for pre-provisioned local NVMe (#89)
## Description
**TL;DR.** New standalone package `bind-mount`: it exposes one existing
host mount at a second stable path through a persistent, owner-marked
systemd bind mount, with a fail-closed safety contract on every
destructive path. It does no device discovery, no RAID assembly, and no
mkfs; it only aliases a mount that platform bootstrap already created,
and it survives reboots (which a raw `mount --bind` in user data does
not).
### What the package does
- Validates the source in the host mount namespace: it must be an exact,
read-write mount point, with optional fstype (`expected-source-fstype`)
and device-prefix (`expected-source-device-prefix`) assertions.
- Renders and enables a systemd `.mount` unit (bind) tagged with an
owner marker, then relies on the package's controlled interrupt (reboot)
to activate it; the post-interrupt check proves the active mount binds
the desired source before the node returns to service.
- Persists root-only receipts under
`/var/lib/nodewright-packages/bind-mount/` before any systemd operation,
so uninstall works even when the operator runs uninstall without the
original ConfigMap.
- All 10 lifecycle modes are wired in `config.json` and idempotent;
`make validate-standalone PACKAGE=bind-mount` passes against the agent
v1 schemas.
### Fail-closed guarantees
- Refuses a non-empty target directory (never hides existing data), a
symlinked target, a target owned by `/etc/fstab`, and any pre-existing
unit file with different content (foreign units are never overwritten or
removed; removal requires the owner marker).
- Refuses source/target overlap, a read-only source or target mount, and
a config change while the old target is still mounted (explicit
remediation message instead of silent re-pointing).
- Uninstall never unmounts a live target: it removes persistence only
and tells the operator to recycle the node, so a running workload is
never yanked out from under.
- A unit rejected by `systemd-analyze verify` is rolled back; a failed
`systemctl enable` retains the receipt so cleanup still works.
### Included hardening (folded in after an adversarial review of the
initial draft)
1. **ConfigMap-less uninstall of a never-installed package is now a
clean no-op.** Previously, if install had never succeeded (no receipts)
and the operator sent an uninstall without the package ConfigMap (both
the legacy stub-uninstall lineage and the current faux-package path do
this), `load_desired_config` hard-failed on the missing `source` key and
package removal wedged on that node forever. Now that state degrades to
a best-effort sweep: remove any owner-marked `.mount` units, succeed,
write nothing. Covered by a new lifecycle test
(`test_uninstall_of_never_installed_package_is_clean_noop`): failed
install (wrong source fstype), ConfigMaps deleted, then `uninstall` and
`uninstall-check` both succeed.
2. **Kubelet ordering parity in the rendered unit.** The unit now
renders `Before=local-fs.target kubelet.service
snap.kubelet-eks.daemon.service`, matching the equivalent
launch-template user-data variant, so on reboot the mount orders before
kubelet on both EKS AMI flavors. systemd ignores `Before=` entries for
units that do not exist on the node, so this is inert on non-Kubernetes
hosts. This is ordering hygiene, not a success gate; the post-interrupt
check is the gate.
### Consumer context
First consumer is DGXC AV TestStudio (Skyhook/NodeWright on EKS,
internal tracker NKX-12808): moving BuildBarn worker file pools from the
root EBS volume onto pre-provisioned local NVMe (`/mnt/k8s-disks/0`
RAID0 from the EKS AMI's `setup-local-disks`, exposed at `/mnt/data`).
Publication of `ghcr.io/nvidia/nodewright-packages/bind-mount:0.1.0`
unblocks that fleet rollout; the consumer-side Skyhook config is already
staged behind a default-off flag pending this image.
### Testing
- `make test-package PACKAGE=bind-mount`: 32 passed (16 test functions x
ubuntu:22.04/24.04), including the new never-installed uninstall test.
- `make validate-standalone PACKAGE=bind-mount`: schema, step files, and
executable-bit validation all pass against
`ghcr.io/nvidia/skyhook/agent:latest`.
- `shellcheck` (stable) is clean on `bind_mount.sh`.
- Lifecycle tests run the real script in containers against fakes for
`findmnt`/`nsenter`/`stat`/`systemctl`/`systemd-analyze`/`systemd-escape`;
no real systemd or mounts are exercised. On-node validation on an EKS
GPU node is planned on the consumer side before any fleet enablement.
## Checklist
- [x] I am familiar with the [Contributing
Guidelines](https://github.com/NVIDIA/nodewright-packages/blob/HEAD/CONTRIBUTING.md).
- [x] New or existing tests cover these changes.
- [x] The documentation is up to date with these changes.
Signed-off-by: Gregory Giecold <ggiecold@nvidia.com>
Copy file name to clipboardExpand all lines: AGENTS.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,11 +15,11 @@ This repo holds the pre-built **packages** consumed by the [NVIDIA NodeWright op
15
15
16
16
This repo does **not** contain the operator, the agent, or any Go code. It is shell scripts (could be python, or any executable, but currently is all shell), `Dockerfile`s, package config, and Python-based tests/tooling.
17
17
18
-
**Rename status (Skyhook to NodeWright):** the project is being renamed from Skyhook to NodeWright. Many public names still use `skyhook` on purpose: the published image path (`ghcr.io/nvidia/skyhook-packages/...`), the CRD group (`skyhook.nvidia.com/v1alpha1`), the on-host `SKYHOOK_DIR`/`skyhook_dir` names, and the CLI. The GitHub repo itself is now `nodewright-packages` (with `skyhook-packages` redirecting). Do not "fix" `skyhook`to `nodewright` or vice versa; match whatever the surrounding code already uses.
18
+
**Rename status (Skyhook to NodeWright):** the project is being renamed from Skyhook to NodeWright. New package releases publish at `ghcr.io/nvidia/nodewright-packages/...`; older package versions may still exist only under the legacy `ghcr.io/nvidia/skyhook-packages/...` namespace. The CRD group (`skyhook.nvidia.com/v1alpha1`), the on-host `SKYHOOK_DIR`/`skyhook_dir` names, and the CLI still use `skyhook` on purpose. The GitHub repo itself is now `nodewright-packages` (with `skyhook-packages` redirecting). Do not rename remaining `skyhook`references without checking their specific compatibility contract.
19
19
20
20
## Repository layout
21
21
22
-
-**Each top-level directory that contains a `Dockerfile` is a package.** Current packages: `shellscript`, `tuning`, `tuned`, `kdump`, `nvidia-setup`, `nvidia-tuning-gke`, `nvidia-tuned`, `copy-fail`.
22
+
-**Each top-level directory that contains a `Dockerfile` is a package.** Current packages: `shellscript`, `tuning`, `tuned`, `kdump`, `nvidia-setup`, `nvidia-tuning-gke`, `nvidia-tuned`, `copy-fail`, `bind-mount`.
-`tests/integration/`: Docker-based pytest suites, one subdir per package that has tests (with hyphens converted to underscores, e.g. `nvidia-setup` to `nvidia_setup`; not every package has a subdir).
25
25
-`.github/`: workflows, the `build-package` composite action, issue/PR templates, `labels.yml`, `CODEOWNERS`.
@@ -105,7 +105,7 @@ Almost all package logic is bash. The existing scripts are not yet consistent (s
105
105
106
106
-**On PR (`pr_build.yaml`):** detects which package directories changed, then for each changed package runs validate, test, and a dev build tagged `<version>-dev.sha<short>`. Changed-package detection keys on a directory having a `Dockerfile`; `scripts/` and hidden dirs are skipped.
107
107
-**On tag push `<package>/<version>`:** two independent workflows run. `build_container.yaml` builds and pushes the multi-arch image (`linux/amd64,linux/arm64`) with a build-provenance attestation; `release.yml` creates the GitHub Release with `git-cliff` notes scoped to that package. The image build (here and in `pr_build.yaml`) goes through the `.github/actions/build-package` composite action. (`build_package.yml` is a reusable `workflow_call` workflow that exists but is not currently wired up.)
108
-
-**Published image path:**`ghcr.io/nvidia/skyhook-packages/<package>:<version>` (the `skyhook-packages` segment is retained pending the rename).
108
+
-**Published image path for new releases:**`ghcr.io/nvidia/nodewright-packages/<package>:<version>`. Verify legacy versions before changing an existing consumer because some older tags remain only under `ghcr.io/nvidia/skyhook-packages/`.
109
109
- An optional `<package>/preprocess.sh` is run before the image build and can emit `BUILD_ARGS`.
The `bind-mount` package exposes one existing host mount at a second stable path through a persistent systemd bind mount. It does not discover, format, assemble, or delete storage.
4
+
5
+
The package is intended for nodes where platform bootstrap owns the source filesystem and a workload needs a stable path. It validates the source in the host mount namespace, writes and enables a systemd mount unit, and relies on a NodeWright interrupt to activate the unit before workloads resume.
`source` and `target` are required. `expected-source-fstype` and `expected-source-device-prefix` are optional fail-closed assertions.
32
+
33
+
The image may be referenced under multiple package map keys on the same node to
34
+
manage multiple bind mounts. Each instance has its own ownership receipt and
35
+
owner marker, scoped to the Skyhook resource UID and package map key. Targets
36
+
must be distinct: two instances that name the same systemd mount unit fail
37
+
closed rather than sharing or replacing it.
38
+
39
+
## Safety contract
40
+
41
+
- The source must already be an exact, read-write mount point.
42
+
- The target must be empty, or already be the same bind mount backed by this package instance's owned unit; otherwise the package fails.
43
+
- The systemd unit rechecks target emptiness at activation and fails if the target is already non-empty, reducing the risk of silently hiding newly written data.
44
+
- Existing symlinks, foreign systemd units, and `/etc/fstab` ownership are rejected.
45
+
- Source and target paths must be disjoint; neither may contain the other.
46
+
- Prepared and active checks reject a read-only target mount.
47
+
- A source or target configuration change fails while that instance's old target remains mounted. Remove the package and recycle the node before changing paths.
48
+
- Uninstall disables and removes the persistent unit but deliberately does not unmount a live target. Recycle the node to complete rollback safely.
49
+
- A root-only ownership receipt supports ConfigMap-less uninstall on operators older than v0.16.0 and preserves cleanup after an interrupted install on newer operators. The receipt is removed after `uninstall-check` confirms that no owned unit remains.
50
+
- Uninstall of a package instance that never installed succeeds as a clean no-op, even when the uninstall runs without a ConfigMap. It only sweeps units carrying that exact Skyhook-resource/package-key owner marker and cannot remove another instance's unit.
51
+
- The mount unit orders before `local-fs.target` and the kubelet units (`kubelet.service`, `snap.kubelet-eks.daemon.service`) so a rebooting node mounts the target before workloads start. Ordering is hygiene, not a success gate; the post-interrupt check is the gate.
52
+
53
+
The apply and config checks validate the prepared unit before an interrupt. The post-interrupt check validates the active host mount. Consumers should use `runtimeRequired: true` and a bounded interruption budget.
0 commit comments