From 2fde49969a92e34b1bac81f1c3ecc7ad84f7460d Mon Sep 17 00:00:00 2001 From: Jiri Uhlir Date: Wed, 10 Jun 2026 08:02:45 +0200 Subject: [PATCH] Preserve target environment during restore --- scripts/restore-appfactory.sh | 239 ++++++++++++++++++++++++++++------ 1 file changed, 202 insertions(+), 37 deletions(-) diff --git a/scripts/restore-appfactory.sh b/scripts/restore-appfactory.sh index 0ca7cae..073a262 100755 --- a/scripts/restore-appfactory.sh +++ b/scripts/restore-appfactory.sh @@ -17,10 +17,109 @@ Usage: USAGE } +env_get_raw() { + local file="$1" + local key="$2" + + if [ ! -f "$file" ]; then + return 1 + fi + + grep -E "^${key}=" "$file" | tail -n 1 | cut -d= -f2- || true +} + +env_value_is_non_empty() { + local value="$1" + + value="${value%\"}" + value="${value#\"}" + value="${value%\'}" + value="${value#\'}" + + [ -n "$value" ] +} + +env_has_non_empty_key() { + local file="$1" + local key="$2" + local value + + value="$(env_get_raw "$file" "$key" || true)" + env_value_is_non_empty "$value" +} + +set_env_value() { + local file="$1" + local key="$2" + local value="$3" + + if grep -q "^${key}=" "$file"; then + sed -i "s|^${key}=.*|${key}=${value}|" "$file" + else + echo "${key}=${value}" >> "$file" + fi +} + +set_env_if_missing_or_empty() { + local file="$1" + local key="$2" + local value="$3" + + if env_has_non_empty_key "$file" "$key"; then + return 0 + fi + + set_env_value "$file" "$key" "$value" +} + +merge_env_missing_keys() { + local source_file="$1" + local target_file="$2" + + [ -f "$source_file" ] || return 0 + [ -f "$target_file" ] || cp "$source_file" "$target_file" + + while IFS= read -r line; do + case "$line" in + ""|\#*) + continue + ;; + *=*) + key="${line%%=*}" + value="${line#*=}" + + if env_has_non_empty_key "$target_file" "$key"; then + continue + fi + + set_env_value "$target_file" "$key" "$value" + ;; + esac + done < "$source_file" +} + +resolve_runtime_uid_gid() { + local user="$1" + local uid + local gid + + uid="$(id -u "$user" 2>/dev/null || echo 0)" + gid="$(id -g "$user" 2>/dev/null || echo 0)" + + if [ "$uid" = "0" ]; then + uid="1000" + fi + + if [ "$gid" = "0" ]; then + gid="1000" + fi + + echo "$uid:$gid" +} + [ -n "$BACKUP_FILE_INPUT" ] || { usage; fail "Backup file argument is required"; } [ "$(id -u)" -eq 0 ] || fail "Run as root via sudo" [ -f "$BACKUP_FILE_INPUT" ] || fail "Backup file not found: $BACKUP_FILE_INPUT" -id "$TARGET_USER" >/dev/null 2>&1 || fail "Target user does not exist: $TARGET_USER" command -v realpath >/dev/null 2>&1 || fail "realpath missing" command -v tar >/dev/null 2>&1 || fail "tar missing" @@ -28,19 +127,34 @@ command -v docker >/dev/null 2>&1 || fail "docker missing" BACKUP_FILE="$(realpath "$BACKUP_FILE_INPUT")" -APPFACTORY_UID="$(id -u "$TARGET_USER")" -APPFACTORY_GID="$(id -g "$TARGET_USER")" +if ! id "$TARGET_USER" >/dev/null 2>&1; then + warn "Target user does not exist: $TARGET_USER" + warn "Using numeric fallback 1000:1000 for runtime ownership" +fi + +RUNTIME_IDS="$(resolve_runtime_uid_gid "$TARGET_USER")" +APPFACTORY_UID="${RUNTIME_IDS%%:*}" +APPFACTORY_GID="${RUNTIME_IDS##*:}" [ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock" APPFACTORY_DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)" -docker ps >/dev/null 2>&1 || fail "docker ps failed. Is $TARGET_USER in docker group and was session restarted?" +docker ps >/dev/null 2>&1 || fail "docker ps failed" log "Backup: $BACKUP_FILE" log "Target user: $TARGET_USER" -log "APPFACTORY_UID=$APPFACTORY_UID" -log "APPFACTORY_GID=$APPFACTORY_GID" -log "APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID" +log "Calculated runtime APPFACTORY_UID=$APPFACTORY_UID" +log "Calculated runtime APPFACTORY_GID=$APPFACTORY_GID" +log "Calculated runtime APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID" + +PRESERVED_ENV="" +EXISTING_ENV="$APPFACTORY_ROOT/config/appfactory.env" + +if [ -f "$EXISTING_ENV" ]; then + PRESERVED_ENV="$(mktemp)" + cp "$EXISTING_ENV" "$PRESERVED_ENV" + ok "Preserved existing env before restore: $EXISTING_ENV" +fi log "Removing existing AppFactory containers if present" mapfile -t APPFACTORY_CONTAINERS < <( @@ -78,35 +192,66 @@ mkdir -p \ "$APPFACTORY_ROOT/workspace" ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env" -[ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE" +BACKUP_ENV_COPY="" -set_env_value() { - local key="$1" - local value="$2" +if [ -f "$ENV_FILE" ]; then + BACKUP_ENV_COPY="$(mktemp)" + cp "$ENV_FILE" "$BACKUP_ENV_COPY" +fi - if grep -q "^${key}=" "$ENV_FILE"; then - sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" - else - echo "${key}=${value}" >> "$ENV_FILE" +if [ -n "$PRESERVED_ENV" ] && [ -f "$PRESERVED_ENV" ]; then + log "Restoring target env and merging missing keys from backup env" + + cp "$PRESERVED_ENV" "$ENV_FILE" + + if [ -n "$BACKUP_ENV_COPY" ] && [ -f "$BACKUP_ENV_COPY" ]; then + merge_env_missing_keys "$BACKUP_ENV_COPY" "$ENV_FILE" fi -} -log "Updating appfactory.env for target server" -set_env_value "APPFACTORY_DIR" "$APPFACTORY_ROOT" -set_env_value "WORKSPACE_DIR" "$APPFACTORY_ROOT/workspace" -set_env_value "BACKUP_DIR" "$APPFACTORY_ROOT/backups" -set_env_value "APPFACTORY_UID" "$APPFACTORY_UID" -set_env_value "APPFACTORY_GID" "$APPFACTORY_GID" -set_env_value "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID" + ENV_SNAPSHOT="$APPFACTORY_ROOT/config/appfactory.env.before-restore-merge-$(date +%Y%m%d-%H%M%S)" + cp "$PRESERVED_ENV" "$ENV_SNAPSHOT" + ok "Saved pre-restore env snapshot: $ENV_SNAPSHOT" +else + [ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE" -grep -q '^APPFACTORY_ENABLE_HTTPS=' "$ENV_FILE" || echo 'APPFACTORY_ENABLE_HTTPS=false' >> "$ENV_FILE" -grep -q '^APPFACTORY_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_DOMAIN=' >> "$ENV_FILE" -grep -q '^APPFACTORY_PORTAL_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_PORTAL_DOMAIN=' >> "$ENV_FILE" -grep -q '^APPFACTORY_GITEA_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_GITEA_DOMAIN=' >> "$ENV_FILE" -grep -q '^APPFACTORY_REGISTRY_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_REGISTRY_DOMAIN=' >> "$ENV_FILE" -grep -q '^GOOGLE_CLIENT_ID=' "$ENV_FILE" || echo 'GOOGLE_CLIENT_ID=' >> "$ENV_FILE" -grep -q '^GOOGLE_CLIENT_SECRET=' "$ENV_FILE" || echo 'GOOGLE_CLIENT_SECRET=' >> "$ENV_FILE" -grep -q '^GOOGLE_REDIRECT_URI=' "$ENV_FILE" || echo 'GOOGLE_REDIRECT_URI=' >> "$ENV_FILE" + log "No existing target env found; using backup env and applying target runtime defaults" + + set_env_value "$ENV_FILE" "APPFACTORY_UID" "$APPFACTORY_UID" + set_env_value "$ENV_FILE" "APPFACTORY_GID" "$APPFACTORY_GID" + set_env_value "$ENV_FILE" "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID" +fi + +log "Ensuring mandatory env keys without overwriting existing non-empty values" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DIR" "$APPFACTORY_ROOT" +set_env_if_missing_or_empty "$ENV_FILE" "WORKSPACE_DIR" "$APPFACTORY_ROOT/workspace" +set_env_if_missing_or_empty "$ENV_FILE" "BACKUP_DIR" "$APPFACTORY_ROOT/backups" + +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_UID" "$APPFACTORY_UID" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_GID" "$APPFACTORY_GID" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID" + +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_ENABLE_HTTPS" "false" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DOMAIN" "" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_PORTAL_DOMAIN" "" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_GITEA_DOMAIN" "" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_REGISTRY_DOMAIN" "" +set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_PORTAL_PUBLIC_URL" "" + +set_env_if_missing_or_empty "$ENV_FILE" "GITEA_DOMAIN" "" +set_env_if_missing_or_empty "$ENV_FILE" "GITEA_ROOT_URL" "" +set_env_if_missing_or_empty "$ENV_FILE" "GITEA_URL" "" +set_env_if_missing_or_empty "$ENV_FILE" "WEBHOOK_URL" "" +set_env_if_missing_or_empty "$ENV_FILE" "GITEA_OAUTH_REDIRECT_URI" "" +set_env_if_missing_or_empty "$ENV_FILE" "GITEA_SSH_PORT" "2222" + +set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_OAUTH_ENABLED" "false" +set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_CLIENT_ID" "" +set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_CLIENT_SECRET" "" +set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_REDIRECT_URI" "" +set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_ALLOWED_DOMAIN" "" + +log "Effective runtime env:" +grep -E '^(APPFACTORY_UID|APPFACTORY_GID|APPFACTORY_DOCKER_GID|APPFACTORY_ENABLE_HTTPS|APPFACTORY_DOMAIN|APPFACTORY_PORTAL_DOMAIN|APPFACTORY_GITEA_DOMAIN|APPFACTORY_REGISTRY_DOMAIN|APPFACTORY_PORTAL_PUBLIC_URL|GITEA_DOMAIN|GITEA_ROOT_URL|GITEA_URL|WEBHOOK_URL|GITEA_OAUTH_REDIRECT_URI)=' "$ENV_FILE" || true DB_FILE="$APPFACTORY_ROOT/data/appfactory/appfactory.db" [ -f "$DB_FILE" ] || fail "Missing AppFactory DB after restore: $DB_FILE" @@ -114,8 +259,28 @@ DB_FILE="$APPFACTORY_ROOT/data/appfactory/appfactory.db" COMPOSE_FILE="$APPFACTORY_ROOT/deploy/docker-compose.yml" [ -f "$COMPOSE_FILE" ] || fail "Missing compose file: $COMPOSE_FILE" +APPFACTORY_UID_EFFECTIVE="$(env_get_raw "$ENV_FILE" "APPFACTORY_UID" || echo "$APPFACTORY_UID")" +APPFACTORY_GID_EFFECTIVE="$(env_get_raw "$ENV_FILE" "APPFACTORY_GID" || echo "$APPFACTORY_GID")" + +APPFACTORY_UID_EFFECTIVE="${APPFACTORY_UID_EFFECTIVE%\"}" +APPFACTORY_UID_EFFECTIVE="${APPFACTORY_UID_EFFECTIVE#\"}" +APPFACTORY_GID_EFFECTIVE="${APPFACTORY_GID_EFFECTIVE%\"}" +APPFACTORY_GID_EFFECTIVE="${APPFACTORY_GID_EFFECTIVE#\"}" + +if [ "$APPFACTORY_UID_EFFECTIVE" = "0" ]; then + warn "APPFACTORY_UID is 0; changing to 1000 because Gitea cannot run as root" + set_env_value "$ENV_FILE" "APPFACTORY_UID" "1000" + APPFACTORY_UID_EFFECTIVE="1000" +fi + +if [ "$APPFACTORY_GID_EFFECTIVE" = "0" ]; then + warn "APPFACTORY_GID is 0; changing to 1000 because Gitea cannot run as root" + set_env_value "$ENV_FILE" "APPFACTORY_GID" "1000" + APPFACTORY_GID_EFFECTIVE="1000" +fi + log "Fixing ownership" -chown -R "$APPFACTORY_UID:$APPFACTORY_GID" "$APPFACTORY_ROOT" +chown -R "$APPFACTORY_UID_EFFECTIVE:$APPFACTORY_GID_EFFECTIVE" "$APPFACTORY_ROOT" log "Fixing script permissions" find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true @@ -128,13 +293,13 @@ build_image() { [ -f "$dir/Dockerfile" ] || fail "Missing Dockerfile: $dir/Dockerfile" log "Building image: $image from $dir" - sudo -u "$TARGET_USER" docker build -t "$image:latest" "$dir" + docker build -t "$image:latest" "$dir" } GENERATE_CADDY="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/generate-caddyfile.sh" if [ -f "$GENERATE_CADDY" ]; then log "Generating Caddyfile before starting Caddy" - sudo -u "$TARGET_USER" "$GENERATE_CADDY" || true + "$GENERATE_CADDY" || true fi build_image "appfactory-portal" "$APPFACTORY_ROOT/workspace/appfactory-portal" @@ -143,7 +308,7 @@ build_image "appfactory-monitor" "$APPFACTORY_ROOT/workspace/appfactory-monitor" build_image "appfactory-webhook" "$APPFACTORY_ROOT/workspace/appfactory-webhook" log "Deploying core compose stack" -sudo -u "$TARGET_USER" docker compose \ +docker compose \ --env-file "$ENV_FILE" \ -f "$COMPOSE_FILE" \ up -d --force-recreate @@ -184,7 +349,7 @@ done if [ -f "$GENERATE_CADDY" ]; then log "Regenerating/reloading Caddyfile" - sudo -u "$TARGET_USER" "$GENERATE_CADDY" + "$GENERATE_CADDY" fi PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh" @@ -192,7 +357,7 @@ PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-che chmod +x "$PREFLIGHT" log "Running final preflight" -sudo -u "$TARGET_USER" "$PREFLIGHT" +"$PREFLIGHT" echo echo "APPFACTORY RESTORE: READY"