mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
fix(oci): preserve PocketBase initial data on first install
This commit is contained in:
@@ -1,4 +1,38 @@
|
||||
{
|
||||
"container_contract": {
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/pocketbase/pb_data",
|
||||
"compose_source_example": "/DATA/AppData/$AppID/pb_data",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 8
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"proxmox": {
|
||||
"installer_profile": {
|
||||
"volume_seeds": [
|
||||
{
|
||||
"container_path": "/pocketbase/pb_data",
|
||||
"source_path": "/pocketbase/pb_data",
|
||||
"mount_types": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"credentials": [
|
||||
{
|
||||
|
||||
+128
-2
@@ -246,6 +246,10 @@ SECCOMP_PROFILE_FILE=""
|
||||
CT_CREATED=0
|
||||
INSTALL_COMPLETE=0
|
||||
PRESERVE_FAILED_CT=0
|
||||
VOLUME_SEED_STAGE_ROOT=""
|
||||
VOLUME_SEED_TARGETS=()
|
||||
VOLUME_SEED_STAGES=()
|
||||
VOLUME_SEED_MOUNT_TYPES=()
|
||||
|
||||
cleanup_runtime_console_log() {
|
||||
# The console log is the container's own log from here on, and its line in
|
||||
@@ -270,6 +274,7 @@ healthcheck_probe() {
|
||||
cleanup_failed_install() {
|
||||
local status=$?
|
||||
stop_spinner
|
||||
cleanup_volume_seed_staging || true
|
||||
if [[ -n ${PROXMENUX_OCI_TRANSACTION:-} ]]; then
|
||||
# The transaction owns recovery. Destroying this CT could destroy reused data.
|
||||
cleanup_runtime_console_log || true
|
||||
@@ -326,6 +331,121 @@ ensure_rootfs_directory() {
|
||||
done
|
||||
}
|
||||
|
||||
is_reused_managed_mount() {
|
||||
local target=$1
|
||||
[[ -n ${PROXMENUX_OCI_TRANSACTION:-} ]] || return 1
|
||||
jq -e --arg path "$target" \
|
||||
'[.transaction_reuse_mounts[]? | select(.container_path == $path)] | length > 0' \
|
||||
"$DEPLOYMENT_FILE" >/dev/null
|
||||
}
|
||||
|
||||
cleanup_volume_seed_staging() {
|
||||
[[ -n ${VOLUME_SEED_STAGE_ROOT:-} ]] || return 0
|
||||
[[ $VOLUME_SEED_STAGE_ROOT == "/var/tmp/proxmenux-oci-seed-${VMID}."* && -d $VOLUME_SEED_STAGE_ROOT && ! -L $VOLUME_SEED_STAGE_ROOT ]] \
|
||||
|| { oci_log "Refusing to remove an unexpected image-volume staging directory: ${VOLUME_SEED_STAGE_ROOT}"; return 1; }
|
||||
rm -rf -- "$VOLUME_SEED_STAGE_ROOT" || return 1
|
||||
VOLUME_SEED_STAGE_ROOT=""
|
||||
}
|
||||
|
||||
capture_volume_seeds() {
|
||||
local seed_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
|
||||
|
||||
seed_count=$(jq '.proxmox.installer_profile.volume_seeds? // [] | length' "$TEMPLATE_FILE")
|
||||
(( seed_count > 0 )) || return 0
|
||||
|
||||
mount_ct_rootfs
|
||||
mounted=1
|
||||
VOLUME_SEED_STAGE_ROOT=$(mktemp -d "/var/tmp/proxmenux-oci-seed-${VMID}.XXXXXX") || failed=1
|
||||
while (( failed == 0 )) && IFS= read -r encoded; do
|
||||
[[ -n $encoded ]] || continue
|
||||
item=$(printf '%s' "$encoded" | base64 -d)
|
||||
target=$(jq -er '.container_path' <<<"$item")
|
||||
source_path=$(jq -er '.source_path' <<<"$item")
|
||||
mount_types=$(jq -c '.mount_types // ["managed-volume"]' <<<"$item")
|
||||
selected_mount_type=$(jq -r --arg target "$target" \
|
||||
'[.mounts[]? | select(.container_path == $target) | .type] | first // empty' \
|
||||
"$DEPLOYMENT_FILE")
|
||||
[[ $target == /* && $target != *[[:space:]]* && $target != *","* ]] \
|
||||
|| { oci_log "Invalid volume seed target: $target"; failed=1; break; }
|
||||
[[ $source_path == /* && $source_path != *[[:space:]]* && $source_path != *","* ]] \
|
||||
|| { oci_log "Invalid image volume seed source: $source_path"; failed=1; break; }
|
||||
jq -e --arg type "$selected_mount_type" '
|
||||
type == "array" and length > 0
|
||||
and all(.[]; . == "managed-volume" or . == "host-bind")
|
||||
and index($type) != null' <<<"$mount_types" >/dev/null \
|
||||
|| { oci_log "Skipping image volume seed for $target: the selected mount type is not allowed"; continue; }
|
||||
if [[ $selected_mount_type != managed-volume && $selected_mount_type != host-bind ]]; then
|
||||
oci_log "Skipping image volume seed for $target: unsupported mount type ${selected_mount_type:-none}"
|
||||
continue
|
||||
fi
|
||||
if [[ $selected_mount_type == managed-volume ]] && is_reused_managed_mount "$target"; then
|
||||
oci_log "Keeping the reused persistent disk without seeding it: $target"
|
||||
continue
|
||||
fi
|
||||
[[ ! -L "${rootfs}${source_path}" ]] \
|
||||
|| { oci_log "The image volume seed source must not be a link: $source_path"; failed=1; break; }
|
||||
source=$(readlink -e -- "${rootfs}${source_path}") \
|
||||
|| { oci_log "The image volume seed source does not exist: $source_path"; failed=1; break; }
|
||||
[[ $source == "${rootfs}"/* && -d $source ]] \
|
||||
|| { oci_log "The image volume seed source escapes the rootfs or is not a directory: $source_path"; failed=1; break; }
|
||||
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+=("$selected_mount_type")
|
||||
index=$((index + 1))
|
||||
done < <(jq -r '.proxmox.installer_profile.volume_seeds[]? | @base64' "$TEMPLATE_FILE")
|
||||
|
||||
if (( mounted == 1 )); then
|
||||
oci_quiet pct unmount "$VMID" || failed=1
|
||||
fi
|
||||
if (( failed != 0 )); then
|
||||
oci_log "Could not capture the image data for a persistent volume"
|
||||
die "$(translate "Could not apply the installer profile")"
|
||||
fi
|
||||
if (( ${#VOLUME_SEED_TARGETS[@]} == 0 )); then
|
||||
cleanup_volume_seed_staging || die "$(translate "Could not apply the installer profile")"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_volume_seeds() {
|
||||
local rootfs="/var/lib/lxc/${VMID}/rootfs" index target stage mount_type destination existing failed=0
|
||||
(( ${#VOLUME_SEED_TARGETS[@]} > 0 )) || return 0
|
||||
|
||||
for index in "${!VOLUME_SEED_TARGETS[@]}"; do
|
||||
target=${VOLUME_SEED_TARGETS[$index]}
|
||||
stage=${VOLUME_SEED_STAGES[$index]}
|
||||
mount_type=${VOLUME_SEED_MOUNT_TYPES[$index]}
|
||||
[[ -d $stage && ! -L $stage ]] \
|
||||
|| { oci_log "The prepared image data is not available for the persistent volume: $target"; failed=1; break; }
|
||||
[[ ! -L "${rootfs}${target}" ]] \
|
||||
|| { oci_log "The persistent volume target must not be a link: $target"; failed=1; break; }
|
||||
destination=$(readlink -e -- "${rootfs}${target}") \
|
||||
|| { oci_log "The persistent volume target does not exist: $target"; failed=1; break; }
|
||||
[[ $destination == "${rootfs}"/* && -d $destination ]] \
|
||||
|| { oci_log "The persistent volume target escapes the rootfs or is not a directory: $target"; failed=1; break; }
|
||||
existing=$(find "$destination" -mindepth 1 -maxdepth 1 ! -name lost+found -print -quit)
|
||||
if [[ -n $existing ]]; then
|
||||
if [[ $mount_type == host-bind ]]; then
|
||||
oci_log "Keeping existing host data without seeding it: $target"
|
||||
rm -rf -- "$stage" || { failed=1; break; }
|
||||
continue
|
||||
fi
|
||||
oci_log "Refusing to seed a persistent volume that already contains data: $target"
|
||||
failed=1
|
||||
break
|
||||
fi
|
||||
cp -a -- "${stage}/." "${destination}/" \
|
||||
|| { oci_log "Could not seed the persistent volume from the image: $target"; failed=1; break; }
|
||||
rm -rf -- "$stage" \
|
||||
|| { oci_log "Could not remove the temporary image data: $target"; failed=1; break; }
|
||||
oci_log "Image data copied into the new persistent volume: $target"
|
||||
done
|
||||
cleanup_volume_seed_staging || { oci_log "Could not remove the temporary image-volume staging directory"; failed=1; }
|
||||
(( failed == 0 ))
|
||||
}
|
||||
|
||||
prepare_file_mount_target() {
|
||||
local requested_target=$1
|
||||
local rootfs="/var/lib/lxc/${VMID}/rootfs"
|
||||
@@ -830,7 +950,7 @@ apply_runtime_groups() {
|
||||
}
|
||||
|
||||
apply_installer_profile() {
|
||||
local generated_count preparation_count tls_count mounted=0 failed=0
|
||||
local generated_count preparation_count tls_count seeded_volumes mounted=0 failed=0
|
||||
local rootfs="/var/lib/lxc/${VMID}/rootfs"
|
||||
local encoded item path mode owner destination target remove_lost_found owner_strategy
|
||||
local only_when_mount_type selected_mount_type
|
||||
@@ -842,11 +962,16 @@ apply_installer_profile() {
|
||||
generated_count=$(jq '.proxmox.installer_profile.generated_files? // [] | length' "$TEMPLATE_FILE")
|
||||
preparation_count=$(jq '.proxmox.installer_profile.volume_preparations? // [] | length' "$TEMPLATE_FILE")
|
||||
tls_count=$(jq 'if .proxmox.installer_profile.self_signed_tls? then 1 else 0 end' "$TEMPLATE_FILE")
|
||||
if (( generated_count > 0 || preparation_count > 0 || tls_count > 0 )); then
|
||||
seeded_volumes=${#VOLUME_SEED_TARGETS[@]}
|
||||
if (( generated_count > 0 || preparation_count > 0 || tls_count > 0 || seeded_volumes > 0 )); then
|
||||
mount_ct_rootfs
|
||||
mounted=1
|
||||
fi
|
||||
|
||||
if (( seeded_volumes > 0 )); then
|
||||
apply_volume_seeds || failed=1
|
||||
fi
|
||||
|
||||
while IFS= read -r encoded; do
|
||||
[[ -n $encoded ]] || continue
|
||||
item=$(printf '%s' "$encoded" | base64 -d)
|
||||
@@ -1418,6 +1543,7 @@ if [[ $UNPRIVILEGED_FLAG == 0 ]]; then
|
||||
|| die "$(translate "Could not enable the privileged profile before the first start")"
|
||||
msg_ok "$(translate "Container converted to privileged")"
|
||||
fi
|
||||
capture_volume_seeds
|
||||
MOUNT_INDEX=0
|
||||
CONTAINER_PUID=$(jq -r '[.environment[]? | select(.name == "PUID" or .name == "USER_ID" or .name == "UID") | .value] | last // "0"' "$DEPLOYMENT_FILE")
|
||||
CONTAINER_PGID=$(jq -r '[.environment[]? | select(.name == "PGID" or .name == "GROUP_ID" or .name == "GID") | .value] | last // "0"' "$DEPLOYMENT_FILE")
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Regression coverage for PocketBase's native OCI data mount."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
INSTALLER = ROOT / "remote" / "install_oci.sh"
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
|
||||
|
||||
class PocketBasePersistenceTests(unittest.TestCase):
|
||||
def test_data_volume_uses_the_image_working_directory(self):
|
||||
template = Catalog(ROOT).compose("pocketbase")
|
||||
volumes = template["container_contract"]["volumes"]
|
||||
|
||||
self.assertEqual(len(volumes), 1)
|
||||
self.assertEqual(volumes[0]["container_path"], "/pocketbase/pb_data")
|
||||
self.assertTrue(volumes[0]["required"])
|
||||
self.assertTrue(volumes[0]["managed_volume"]["backup"])
|
||||
|
||||
def test_first_install_seeds_only_the_new_managed_data_volume(self):
|
||||
template = Catalog(ROOT).compose("pocketbase")
|
||||
seeds = template["proxmox"]["installer_profile"]["volume_seeds"]
|
||||
|
||||
self.assertEqual(seeds, [{
|
||||
"container_path": "/pocketbase/pb_data",
|
||||
"source_path": "/pocketbase/pb_data",
|
||||
"mount_types": ["managed-volume", "host-bind"],
|
||||
}])
|
||||
|
||||
def test_seed_mechanism_is_opt_in_and_never_overwrites_existing_data(self):
|
||||
installer = INSTALLER.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn('and all(.[]; . == "managed-volume" or . == "host-bind")', installer)
|
||||
self.assertIn('[[ $selected_mount_type == managed-volume ]] && is_reused_managed_mount "$target";', installer)
|
||||
self.assertIn('Refusing to seed a persistent volume that already contains data:', installer)
|
||||
self.assertIn('Keeping existing host data without seeding it:', installer)
|
||||
self.assertIn('mktemp -d "/var/tmp/proxmenux-oci-seed-${VMID}.XXXXXX"', installer)
|
||||
self.assertIn('cleanup_volume_seed_staging || true', installer)
|
||||
self.assertLess(installer.index('capture_volume_seeds\nMOUNT_INDEX=0'),
|
||||
installer.index('while IFS=$\'\\t\' read -r TYPE TARGET'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user