From 1e30a8c27f00982ffee73934793f828f10309feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 4 Jul 2026 02:04:21 +0300 Subject: [PATCH 1/2] chore(release): promote to 1.0.0 GA --- Dockerfile | 4 +- build/Dockerfile.embedded | 2 +- scripts/check-dockerfile-digests.sh | 249 ++++++++++++++++++++++++ test/deployment-yamls/controlplane.yaml | 18 +- 4 files changed, 261 insertions(+), 12 deletions(-) create mode 100755 scripts/check-dockerfile-digests.sh diff --git a/Dockerfile b/Dockerfile index 3551175..d35eb08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # --build-arg VERSION=v0.0.0-test \ # -t ghcr.io/eclipse-iofog/edgelet:test . -FROM golang:1.26.4-trixie@sha256:0dcba0d95dbfb072e9917a106b9e07d7cc298097dc83e9307056ef1889de654d AS builder +FROM golang:1.26.4-trixie@sha256:68b7145ec43d1820b9a56704554b53d1520aa2a15cb5233e374188a31b2a1bce AS builder ARG BUILDARCH ARG TARGETARCH @@ -42,7 +42,7 @@ RUN set -eux; \ test -f "build/out/data-linux-${ARCH}.tar.zst" # Unpack embed bundle + minimal rootfs. -FROM alpine:3.22@sha256:310c62b5e7ca5b08167e4384c68db0fd2905dd9c7493756d356e893909057601 AS base +FROM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce AS base RUN apk add --no-cache ca-certificates zstd tzdata ARG TARGETARCH ARG VERSION=dev diff --git a/build/Dockerfile.embedded b/build/Dockerfile.embedded index cf577f9..6ac41d6 100644 --- a/build/Dockerfile.embedded +++ b/build/Dockerfile.embedded @@ -2,7 +2,7 @@ # macOS dev: # docker build -f build/Dockerfile.embedded -t edgelet-embed-ci . # docker run --rm -v "$(pwd)":/src -w /src edgelet-embed-ci ./scripts/ci -FROM golang:1.26.4-trixie@sha256:0dcba0d95dbfb072e9917a106b9e07d7cc298097dc83e9307056ef1889de654d +FROM golang:1.26.4-trixie@sha256:68b7145ec43d1820b9a56704554b53d1520aa2a15cb5233e374188a31b2a1bce COPY scripts/install-embed-build-deps /usr/local/bin/install-embed-build-deps RUN chmod +x /usr/local/bin/install-embed-build-deps \ diff --git a/scripts/check-dockerfile-digests.sh b/scripts/check-dockerfile-digests.sh new file mode 100755 index 0000000..01f0a28 --- /dev/null +++ b/scripts/check-dockerfile-digests.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +# Compare digest-pinned base images in a Dockerfile against registry tags via skopeo. +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: check-dockerfile-digests.sh [OPTIONS] DOCKERFILE + +Check digest-pinned base images in a Dockerfile against registry tags. + +Options: + -h, --help Show this help + --min-archs N Minimum platform count for multi-arch (default: 2) + --require PLATFORMS Comma-separated required platforms (e.g. linux/amd64,linux/arm64) + +Environment: + MIN_ARCHES Same as --min-archs + REQUIRE_PLATFORMS Same as --require + +Requires: skopeo, jq +EOF +} + +DOCKERFILE="" +MIN_ARCHES="${MIN_ARCHES:-2}" +REQUIRE_PLATFORMS="${REQUIRE_PLATFORMS:-}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage; exit 0 ;; + --min-archs) MIN_ARCHES="$2"; shift 2 ;; + --require) REQUIRE_PLATFORMS="$2"; shift 2 ;; + -*) echo "error: unknown option $1" >&2; exit 1 ;; + *) + if [[ -n "$DOCKERFILE" ]]; then + echo "error: only one Dockerfile path allowed" >&2 + exit 1 + fi + DOCKERFILE="$1" + shift + ;; + esac +done + +DOCKERFILE="${DOCKERFILE:-Dockerfile}" + +if ! command -v skopeo >/dev/null 2>&1; then + cat >&2 <<'EOF' +error: skopeo is required but not found in PATH. + +Install: + macOS: brew install skopeo + Fedora: dnf install skopeo + Ubuntu: apt install skopeo + +Skopeo reads registry auth from ~/.docker/config.json (docker login / podman login). +EOF + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required" >&2 + exit 1 +fi + +if [[ ! -f "$DOCKERFILE" ]]; then + echo "error: Dockerfile not found: $DOCKERFILE" >&2 + exit 1 +fi + +COMMENT_TAG_RE='^[[:space:]]*#[[:space:]]*(.+)[[:space:]]—[[:space:]]*pin manifest list digest' + +resolve_tag() { + local image_ref="$1" prev_line="$2" + + if [[ "$image_ref" == *:* ]]; then + echo "${image_ref##*:}" + return 0 + fi + + if [[ "$prev_line" =~ $COMMENT_TAG_RE ]]; then + local hinted="${BASH_REMATCH[1]}" + if [[ "$hinted" == *:* ]]; then + echo "${hinted##*:}" + else + echo "latest" + fi + return 0 + fi + + echo "latest" +} + +image_without_tag() { + local image_ref="$1" + if [[ "$image_ref" == *:* ]]; then + echo "${image_ref%%:*}" + else + echo "$image_ref" + fi +} + +platforms_from_raw() { + jq -r ' + if (.manifests // .Manifests) then + [(.manifests // .Manifests)[] + | (.platform // .Platform) + | select(.architecture != null and .architecture != "unknown") + | "\(.os)/\(.architecture)" + ] | unique | .[] + elif (.architecture // .Architecture) then + "\(.os // .Os)/\(.architecture // .Architecture)" + else + empty + end + ' +} + +is_index() { + case "$1" in + application/vnd.docker.distribution.manifest.list.v2+json|application/vnd.oci.image.index.v1+json) + return 0 + ;; + *) + return 1 + ;; + esac +} + +report_multi_arch() { + local label="$1" raw_file="$2" + local media_type platforms count + + media_type="$(jq -r '.mediaType // .MediaType // empty' "$raw_file")" + + if is_index "$media_type"; then + platforms="$(platforms_from_raw < "$raw_file" | sort -u)" + count="$(printf '%s\n' "$platforms" | sed '/^$/d' | wc -l | tr -d ' ')" + echo " ${label}: multi-arch yes (${count} platforms)" + while IFS= read -r platform; do + [[ -n "$platform" ]] && echo " - ${platform}" + done <<< "$platforms" + + if [[ "$count" -lt "$MIN_ARCHES" ]]; then + echo " WARNING: ${label} has fewer than ${MIN_ARCHES} platforms." + return 1 + fi + else + local single + single="$(platforms_from_raw < "$raw_file" | head -n1)" + echo " ${label}: multi-arch no (single platform: ${single:-unknown})" + return 1 + fi + + if [[ -n "$REQUIRE_PLATFORMS" ]]; then + IFS=',' read -ra required <<< "$REQUIRE_PLATFORMS" + for req in "${required[@]}"; do + req="$(echo "$req" | xargs)" + if ! printf '%s\n' "$platforms" | grep -qx "$req"; then + echo " WARNING: ${label} missing required platform: ${req}" + return 1 + fi + done + fi + + return 0 +} + +skopeo_tag_ref() { + local image_ref="$1" tag="$2" + local image + image="$(image_without_tag "$image_ref")" + echo "docker://${image}:${tag}" +} + +skopeo_digest_ref() { + local image_ref="$1" digest="$2" + echo "docker://$(image_without_tag "$image_ref")@${digest}" +} + +skopeo_digest() { + # Override host OS/arch so multi-arch tags resolve to the manifest-list digest on macOS too. + skopeo inspect "$1" \ + --override-os linux \ + --override-arch amd64 \ + --format '{{.Digest}}' +} + +skopeo_raw_to_file() { + local dest="$1" + skopeo inspect --raw "$2" > "$dest" +} + +exit_code=0 +seen=() +prev_line="" + +while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^FROM[[:space:]]+([^[:space:]]+)@sha256:([a-f0-9]{64}) ]]; then + image_ref="${BASH_REMATCH[1]}" + pinned="sha256:${BASH_REMATCH[2]}" + + if [[ " ${seen[*]:-} " == *" ${image_ref} "* ]]; then + prev_line="$line" + continue + fi + seen+=("$image_ref") + + tag="$(resolve_tag "$image_ref" "$prev_line")" + tag_ref="$(skopeo_tag_ref "$image_ref" "$tag")" + digest_ref="$(skopeo_digest_ref "$image_ref" "$pinned")" + + echo "==> ${image_ref} (tag: ${tag})" + echo " pinned: ${pinned}" + + latest_digest="$(skopeo_digest "$tag_ref")" + echo " latest: ${latest_digest}" + + latest_raw="$(mktemp)" + pinned_raw="$(mktemp)" + skopeo_raw_to_file "$latest_raw" "$tag_ref" + skopeo_raw_to_file "$pinned_raw" "$digest_ref" + + if [[ "$pinned" == "$latest_digest" ]]; then + echo " status: OK" + else + echo " status: OUTDATED" + echo + echo " Suggestion — update Dockerfile pin to latest multi-arch manifest list:" + echo " FROM ${image_ref}@${latest_digest}" + echo + exit_code=1 + fi + + report_multi_arch "Pinned digest" "$pinned_raw" || exit_code=1 + report_multi_arch "Latest tag" "$latest_raw" || exit_code=1 + + rm -f "$latest_raw" "$pinned_raw" + echo + fi + prev_line="$line" +done < "$DOCKERFILE" + +if [[ "${#seen[@]}" -eq 0 ]]; then + echo "error: no digest-pinned FROM lines in ${DOCKERFILE}" >&2 + exit 1 +fi + +exit "$exit_code" diff --git a/test/deployment-yamls/controlplane.yaml b/test/deployment-yamls/controlplane.yaml index f6676f9..99cfa50 100644 --- a/test/deployment-yamls/controlplane.yaml +++ b/test/deployment-yamls/controlplane.yaml @@ -5,7 +5,7 @@ metadata: namespace: bar spec: controller: - image: emirhandurmus/controller:3.8.0-beta.1 + image: ghcr.io/datasance/controller:3.8.0 registry: 1 port: 51121 # publicUrl: https://controller.example.com @@ -43,15 +43,15 @@ spec: captureIpAddress: true systemMicroservices: router: - # amd64: ghcr.io/datasance/router:3.8.0-beta.0 - arm64: emirhandurmus/router:3.8.0-beta.5 - # riscv64: ghcr.io/datasance/router:3.8.0-beta.0 - # arm: ghcr.io/datasance/router:3.8.0-beta.0 + # amd64: ghcr.io/datasance/router:3.8.0 + arm64: ghcr.io/datasance/router:3.8.0 + # riscv64: ghcr.io/datasance/router:3.8.0 + # arm: ghcr.io/datasance/router:3.8.0 nats: # when nats.enabled is true - # amd64: ghcr.io/datasance/nats:2.14.2-rc.4 - arm64: ghcr.io/datasance/nats:2.14.2-rc.4 - # riscv64: ghcr.io/datasance/nats:2.12.4 - # arm: ghcr.io/datasance/nats:2.12.4 + # amd64: ghcr.io/datasance/nats:2.14.2 + arm64: ghcr.io/datasance/nats:2.14.2 + # riscv64: ghcr.io/datasance/nats:2.14.2 + # arm: ghcr.io/datasance/nats:2.14.2 nats: # optional; when present and enabled, NATS is deployed with JetStream enabled: true # set to false to disable NATS logLevel: debug From 9552852c05419882e516497fbd43b10a4121782e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Mon, 6 Jul 2026 13:34:26 +0300 Subject: [PATCH 2/2] (doc):edgelet volume ref docs created --- docs/edgelet/README.md | 1 + docs/edgelet/container-engine.md | 2 +- docs/edgelet/manifest-reference.md | 2 +- docs/edgelet/modules/README.md | 1 + docs/edgelet/modules/volumemount.md | 2 +- docs/edgelet/persistence.md | 3 + docs/edgelet/troubleshooting.md | 2 + docs/edgelet/volumes.md | 179 ++++++++++++++++++++++++++++ 8 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 docs/edgelet/volumes.md diff --git a/docs/edgelet/README.md b/docs/edgelet/README.md index 89f2fd2..64b32c4 100644 --- a/docs/edgelet/README.md +++ b/docs/edgelet/README.md @@ -22,6 +22,7 @@ Operator and developer documentation for the Edgelet edge agent. | [dns.md](dns.md) | Bridge DNS, embedded resolver, docker/podman aliases and ExtraHosts | | [workload-metadata.md](workload-metadata.md) | Container labels and `EDGELET_*` env contract | | [workload-continuity.md](workload-continuity.md) | Reconcile behavior across restarts and engine changes | +| [volumes.md](volumes.md) | Volume types, delete vs prune behavior, disk layout | | [edgeguard.md](edgeguard.md) | Hardware attestation (`edgeGuardFrequency`) | | [control-plane.md](control-plane.md) | Local Datasance Controller deployment | | [exec-sessions.md](exec-sessions.md) | Multi-session exec (local CLI and controller-initiated) | diff --git a/docs/edgelet/container-engine.md b/docs/edgelet/container-engine.md index e07e166..fa185f9 100644 --- a/docs/edgelet/container-engine.md +++ b/docs/edgelet/container-engine.md @@ -47,7 +47,7 @@ Isolated from host Docker/Podman installations: | `/var/lib/edgelet/data/current/bin/edgelet` | **Fat** runtime ELF (linux): daemon + in-process containerd | | `/var/lib/edgelet/data/current/bin/` | Shim (`containerd-shim-runc-v2`), `crun`, CNI multicall + symlinks | | `/var/lib/edgelet/data/current` / `previous` | Symlinks to active / prior extracted bundle directories | -| `/var/lib/edgelet/` | User data (`diskDirectory`) | +| `/var/lib/edgelet/` | User data (`diskDirectory`); workload volume data under `volumes/data/` — see [volumes.md](volumes.md) | | `/var/lib/edgelet-containerd/` | Containerd images, snapshots, CNI state | | `/run/edgelet/containerd.sock` | Containerd API socket | | `/run/edgelet/edgelet.sock` | EdgeletAPI Unix socket | diff --git a/docs/edgelet/manifest-reference.md b/docs/edgelet/manifest-reference.md index 7144d89..4acfcf6 100644 --- a/docs/edgelet/manifest-reference.md +++ b/docs/edgelet/manifest-reference.md @@ -78,7 +78,7 @@ spec: | `env` | `{key,value}[]` | User env (`EDGELET_*` reserved) | | `extraHosts` | `{name,address}[]` or legacy strings | `/etc/hosts` + docker ExtraHosts | | `ports` | `{internal,external,protocol}[]` | Port mappings | -| `volumes` | `{hostDestination,containerDestination,accessMode,type}[]` | Bind mounts / volumes | +| `volumes` | `{hostDestination,containerDestination,accessMode,type}[]` | `BIND`, `VOLUME`, or controller `VOLUME_MOUNT`. **Delete does not remove `VOLUME` data** on the embedded engine — see [volumes.md](volumes.md). | | `commands` | []string | Container command override | | `cpuSetCpus` | string | cpuset | | `memoryLimit` | int64 | Memory limit bytes | diff --git a/docs/edgelet/modules/README.md b/docs/edgelet/modules/README.md index 23c9d82..14b446c 100644 --- a/docs/edgelet/modules/README.md +++ b/docs/edgelet/modules/README.md @@ -123,6 +123,7 @@ Edge Guard, Pruning, Volume Mount, and SSH Proxy are not in this fixed array; th | EdgeletAPI usage | [../edgelet-api-v1.md](../edgelet-api-v1.md) | | Controller sync / CP deploy | [../control-plane.md](../control-plane.md) | | SQLite backup / wipe | [../persistence.md](../persistence.md) | +| Workload volumes | [../volumes.md](../volumes.md) | | Container engines | [../container-engine.md](../container-engine.md) | | DNS | [../dns.md](../dns.md) | | Workload labels/env | [../workload-metadata.md](../workload-metadata.md) | diff --git a/docs/edgelet/modules/volumemount.md b/docs/edgelet/modules/volumemount.md index a3cfd64..f8dc288 100644 --- a/docs/edgelet/modules/volumemount.md +++ b/docs/edgelet/modules/volumemount.md @@ -67,7 +67,7 @@ Triggered from Field Agent sync path (`fieldagent/sync.go`). ### Cleanup -`CleanupMicroserviceVolumes(microserviceUUID)` when container removed — preserves shared secret/configmap data dirs where appropriate (see tests). +`CleanupMicroserviceVolumes(microserviceUUID)` when container removed — preserves shared secret/configmap data dirs where appropriate (see tests). Operator guide: [../volumes.md](../volumes.md). ## Volume types diff --git a/docs/edgelet/persistence.md b/docs/edgelet/persistence.md index 27b1ab5..7c28a8a 100644 --- a/docs/edgelet/persistence.md +++ b/docs/edgelet/persistence.md @@ -42,6 +42,8 @@ Tables are grouped by **source prefix** (v1 schema): Image layers and containerd state live **outside** `diskDirectory` (for example `/var/lib/edgelet-containerd/` on the embedded engine). Backing up `edgelet.db` does **not** back up pulled images or running container filesystems. +Stateful **`VOLUME`** mappings persist files under `{diskDirectory}/volumes/data/` outside SQLite. Include that tree in backups for stateful workloads. See [volumes.md](volumes.md). + --- ## Backup runbook (R85) @@ -289,3 +291,4 @@ go test ./internal/store/... ./internal/fieldagent/... ./internal/processmanager | [troubleshooting.md](troubleshooting.md) | Daemon won't start (includes disk space under `/var/lib/edgelet`) | | [control-plane.md](control-plane.md) | ControlPlane redeploy after DB wipe | | [container-engine.md](container-engine.md) | `/var/lib/edgelet` vs `edgelet-containerd` data paths | +| [volumes.md](volumes.md) | `volumes/data/` lifecycle and backup scope | diff --git a/docs/edgelet/troubleshooting.md b/docs/edgelet/troubleshooting.md index afe7cfe..c4b340a 100644 --- a/docs/edgelet/troubleshooting.md +++ b/docs/edgelet/troubleshooting.md @@ -37,6 +37,8 @@ Common issues when running Edgelet on edge nodes. df -h /var/lib/edgelet /var/lib/edgelet-containerd ``` + If usage grows after deleting microservices, orphaned `VOLUME` data may remain under `/var/lib/edgelet/volumes/data/` until prune runs — see [volumes.md](volumes.md). + --- ## Cgroup delegation (embedded edgelet engine) diff --git a/docs/edgelet/volumes.md b/docs/edgelet/volumes.md new file mode 100644 index 0000000..756bf20 --- /dev/null +++ b/docs/edgelet/volumes.md @@ -0,0 +1,179 @@ +# Workload volumes and lifecycle + +How Edgelet stores microservice volume data on disk, what is removed when a workload is deleted, and how operators reclaim space. + +Applies to all container engines; **embedded `edgelet` engine** behavior is called out where it differs from Docker/Podman. + +--- + +## Why this matters + +On the embedded `edgelet` engine, a `VOLUME` mapping does **not** use Docker's volume subsystem. Edgelet creates a persistent directory under `{diskDirectory}/volumes/data/` and bind-mounts it into the container. + +**Deleting a microservice removes the container, but usually not the volume data.** Orphaned data is reclaimed by **volume prune** (scheduled or manual), not at delete time. + +Operators who need immediate cleanup must run prune or delete the directories manually. + +--- + +## On-disk layout + +Base path: `{diskDirectory}` (default `/var/lib/edgelet/`). + +``` +volumes/ + data/{microservice-uuid}/{volume-name}/ # VOLUME-type persistent data (edgelet engine) + microservices/{microservice-uuid}/ # Per-MS mount staging (secrets/configmaps) + secrets/{volume-name}/ # Controller secret payloads (shared) + configMaps/{volume-name}/ # Controller configmap payloads (shared) +``` + +Image layers and container filesystems live under `/var/lib/edgelet-containerd/` (embedded engine), **not** under `volumes/data/`. Backing up `edgelet.db` alone does **not** back up `VOLUME` data — see [persistence.md](persistence.md). + +--- + +## Volume mapping types + +| Type | `hostDestination` | Edgelet behavior | +|------|-------------------|------------------| +| **`VOLUME`** | Relative name, e.g. `mydata` | Creates `{diskDirectory}/volumes/data/{ms-uuid}/mydata/` and bind-mounts it | +| **`VOLUME_MOUNT`** | Controller volume name, e.g. `my-secret` or `my-secret/key` | Materializes under `volumes/microservices/{ms-uuid}/` from shared `secrets/` or `configMaps/` trees | +| **`BIND`** | Absolute host path, e.g. `/opt/data` | Bind-mounts that path; Edgelet never deletes it | + +Local deploy manifests (`edgelet deploy -f`) support **`BIND`** and **`VOLUME`** only. Controller workloads may also use **`VOLUME_MOUNT`** for secrets and configmaps synced from Pot. + +--- + +## What happens when a microservice is deleted + +Deletion paths: + +- **Controller:** microservice marked `delete: true` in Pot snapshot → process manager reconciles removal +- **Local:** `edgelet ms rm ` or EdgeletAPI delete of a local workload + +In all cases Edgelet: + +1. Stops and removes the container +2. Runs volume-mount cleanup for that microservice UUID + +### Always removed + +| Path | When | +|------|------| +| Container / CRI sandbox | On delete | +| `volumes/microservices/{uuid}/` | On delete (per-MS secret/configmap symlinks) | + +### Not removed on delete + +| Path | Reason | +|------|--------| +| `volumes/data/{uuid}/` | **VOLUME** data is intentionally persistent until prune | +| `volumes/secrets/`, `volumes/configMaps/` | Shared controller artifacts; other workloads may still reference them | +| **`BIND`** host paths | Operator-managed; Edgelet does not touch them | + +--- + +## `deleteWithCleanup` (controller workloads) + +When Pot sets `deleteWithCleanup: true` on a microservice: + +| Engine | Effect | +|--------|--------| +| **docker / podman** | Container remove may pass `RemoveVolumes` to the engine; also removes the image when cleanup is requested | +| **edgelet (embedded)** | **`deleteWithCleanup` does not delete `volumes/data/`** — the engine ignores the cleanup flag for volumes. The main extra effect is **image removal** when cleanup is requested | + +Local `edgelet ms rm` always uses non-cleanup removal (container only; no automatic image or volume data delete). + +--- + +## When volume data is actually deleted + +### Volume prune (embedded engine) + +`PruneVolumes` removes orphaned directories under: + +- `volumes/data/{uuid}/` +- `volumes/microservices/{uuid}/` + +for any microservice UUID that has **no running container**. + +Triggered by: + +- Scheduled prune (`pruningFrequency` / `frequencyInterval` in config) — runs containers → volumes → images +- Manual prune: + +```bash +edgelet system prune volumes +edgelet system prune all +``` + +### Full deprovision (`scope=all`) + +Agent deprovision stops all workloads, then runs container and volume prune hooks. This is the broadest automatic cleanup short of wiping `diskDirectory`. + +### Controller artifact clear (`scope=local` deprovision) + +Clears `secrets/` and `configMaps/` and SQLite volume-mount rows, but **preserves** `volumes/data/` (local workload data). + +### Manual cleanup + +If you must reclaim space immediately after deleting a microservice: + +```bash +# After confirming the MS UUID is gone and data is not needed +sudo rm -rf /var/lib/edgelet/volumes/data/ +``` + +Or run `edgelet system prune volumes` to remove all orphaned volume trees. + +--- + +## Operator checklist + +| Goal | Action | +|------|--------| +| Delete workload, keep data for redeploy | `edgelet ms rm` or controller delete — data remains under `volumes/data/{uuid}/` | +| Delete workload and reclaim disk soon | `edgelet system prune volumes` or wait for scheduled prune | +| Backup stateful workloads | Copy `edgelet.db` **and** `volumes/data/` (and BIND paths if used) | +| Wipe node completely | Stop services, remove `diskDirectory` (see [persistence.md](persistence.md)) | +| Secret/configmap only used by one MS | Shared trees are not auto-deleted on MS delete; controller deprovision or manual removal | + +--- + +## Troubleshooting + +### Disk usage grows after microservices are deleted + +1. List remaining volume data: + + ```bash + sudo du -sh /var/lib/edgelet/volumes/data/* + ``` + +2. Confirm no container still references the UUID: + + ```bash + edgelet ms ls + ``` + +3. Prune orphans: + + ```bash + edgelet system prune volumes + ``` + +### Redeployed microservice has empty volume + +`VOLUME` data is keyed by **microservice UUID**. A new deployment with a new UUID gets a **new** directory under `volumes/data/`. To preserve data across UUID changes, use **`BIND`** to a fixed host path you control. + +--- + +## Related documentation + +| Document | Topic | +|----------|--------| +| [manifest-reference.md](manifest-reference.md) | `spec.container.volumes` YAML fields | +| [persistence.md](persistence.md) | SQLite backup; include `volumes/data/` for stateful apps | +| [container-engine.md](container-engine.md) | `diskDirectory` vs containerd paths | +| [modules/volumemount.md](modules/volumemount.md) | Implementation details (Volume Mount Manager) | +| [modules/pruning.md](modules/pruning.md) | Pruning manager and scheduled prune order |