diff --git a/oci/remote/install_oci.sh b/oci/remote/install_oci.sh index 760a1935..85514401 100755 --- a/oci/remote/install_oci.sh +++ b/oci/remote/install_oci.sh @@ -440,11 +440,13 @@ cleanup_volume_seed_staging() { } capture_volume_seeds() { - local seed_count=0 rootfs="/var/lib/lxc/${VMID}/rootfs" + local seed_count=0 volume_count=0 rootfs="/var/lib/lxc/${VMID}/rootfs" local encoded item target source_path selected_mount_type mount_types source stage index=0 mounted=0 failed=0 + local -A declared_seeds=() seed_count=$(jq '.proxmox.installer_profile.volume_seeds? // [] | length' "$TEMPLATE_FILE") - (( seed_count > 0 )) || return 0 + volume_count=$(jq '[.mounts[]? | select(.type == "managed-volume")] | length' "$DEPLOYMENT_FILE") + (( seed_count > 0 || volume_count > 0 )) || return 0 mount_ct_rootfs mounted=1 @@ -453,6 +455,7 @@ capture_volume_seeds() { [[ -n $encoded ]] || continue item=$(printf '%s' "$encoded" | base64 -d) target=$(jq -er '.container_path' <<<"$item") + declared_seeds[$target]=1 source_path=$(jq -er '.source_path' <<<"$item") mount_types=$(jq -c '.mount_types // ["managed-volume"]' <<<"$item") selected_mount_type=$(jq -r --arg target "$target" \ @@ -489,6 +492,24 @@ capture_volume_seeds() { index=$((index + 1)) done < <(jq -r '.proxmox.installer_profile.volume_seeds[]? | @base64' "$TEMPLATE_FILE") + # Docker fills a new volume with what the image has at that path, with its + # owner and permissions. Every new container volume gets the same here, so + # an image that runs as a user of its own finds its data directory its own. + while (( failed == 0 )) && IFS= read -r target; do + [[ -n $target && -z ${declared_seeds[$target]:-} ]] || continue + [[ $target == /* && $target != *[[:space:]]* && $target != *","* ]] || continue + is_reused_managed_mount "$target" && continue + [[ -d "${rootfs}${target}" && ! -L "${rootfs}${target}" ]] || continue + source=$(readlink -e -- "${rootfs}${target}") || continue + [[ $source == "${rootfs}"/* ]] || continue + stage="${VOLUME_SEED_STAGE_ROOT}/${index}" + install -d -m 0700 "$stage" && cp -a -- "${source}/." "${stage}/" || { failed=1; break; } + VOLUME_SEED_TARGETS+=("$target") + VOLUME_SEED_STAGES+=("$stage") + VOLUME_SEED_MOUNT_TYPES+=("managed-volume") + index=$((index + 1)) + done < <(jq -r '.mounts[]? | select(.type == "managed-volume") | .container_path' "$DEPLOYMENT_FILE") + if (( mounted == 1 )); then oci_quiet pct unmount "$VMID" || failed=1 fi diff --git a/oci/tests/test_content_volume_owner.py b/oci/tests/test_content_volume_owner.py index f98c7c17..f311b163 100644 --- a/oci/tests/test_content_volume_owner.py +++ b/oci/tests/test_content_volume_owner.py @@ -71,6 +71,16 @@ class ContentVolumeOwnerTests(unittest.TestCase): self.assertNotIn("/etc/timezone", paths, app) self.assertNotIn("/etc/localtime", owners(template), app) + def test_a_new_volume_is_filled_from_the_image_as_docker_does(self): + # Declared or not: an image that runs as a user of its own, as Node-RED + # or Grafana, finds its data directory with its own owner. + installer = (ROOT / "remote" / "install_oci.sh").read_text() + capture = installer[installer.index("capture_volume_seeds() {"):installer.index("apply_volume_seeds() {")] + self.assertIn('select(.type == "managed-volume") | .container_path\' "$DEPLOYMENT_FILE")', capture) + self.assertIn('is_reused_managed_mount "$target" && continue', capture) + self.assertIn('cp -a -- "${source}/." "${stage}/"', capture) + self.assertIn("[[ -n $target && -z ${declared_seeds[$target]:-} ]] || continue", capture) + def test_a_recipe_without_preparations_is_left_alone(self): contract = {"volumes": [{"container_path": "/downloads", "installation_choice": ["managed-volume", "host-bind"]}]} self.catalog._apply_volume_policy("an-application", contract, None)