Preserve target environment during restore
This commit is contained in:
+202
-37
@@ -17,10 +17,109 @@ Usage:
|
|||||||
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"; }
|
[ -n "$BACKUP_FILE_INPUT" ] || { usage; fail "Backup file argument is required"; }
|
||||||
[ "$(id -u)" -eq 0 ] || fail "Run as root via sudo"
|
[ "$(id -u)" -eq 0 ] || fail "Run as root via sudo"
|
||||||
[ -f "$BACKUP_FILE_INPUT" ] || fail "Backup file not found: $BACKUP_FILE_INPUT"
|
[ -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 realpath >/dev/null 2>&1 || fail "realpath missing"
|
||||||
command -v tar >/dev/null 2>&1 || fail "tar 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")"
|
BACKUP_FILE="$(realpath "$BACKUP_FILE_INPUT")"
|
||||||
|
|
||||||
APPFACTORY_UID="$(id -u "$TARGET_USER")"
|
if ! id "$TARGET_USER" >/dev/null 2>&1; then
|
||||||
APPFACTORY_GID="$(id -g "$TARGET_USER")"
|
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"
|
[ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock"
|
||||||
APPFACTORY_DOCKER_GID="$(stat -c '%g' /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 "Backup: $BACKUP_FILE"
|
||||||
log "Target user: $TARGET_USER"
|
log "Target user: $TARGET_USER"
|
||||||
log "APPFACTORY_UID=$APPFACTORY_UID"
|
log "Calculated runtime APPFACTORY_UID=$APPFACTORY_UID"
|
||||||
log "APPFACTORY_GID=$APPFACTORY_GID"
|
log "Calculated runtime APPFACTORY_GID=$APPFACTORY_GID"
|
||||||
log "APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_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"
|
log "Removing existing AppFactory containers if present"
|
||||||
mapfile -t APPFACTORY_CONTAINERS < <(
|
mapfile -t APPFACTORY_CONTAINERS < <(
|
||||||
@@ -78,35 +192,66 @@ mkdir -p \
|
|||||||
"$APPFACTORY_ROOT/workspace"
|
"$APPFACTORY_ROOT/workspace"
|
||||||
|
|
||||||
ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env"
|
ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env"
|
||||||
[ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE"
|
BACKUP_ENV_COPY=""
|
||||||
|
|
||||||
set_env_value() {
|
if [ -f "$ENV_FILE" ]; then
|
||||||
local key="$1"
|
BACKUP_ENV_COPY="$(mktemp)"
|
||||||
local value="$2"
|
cp "$ENV_FILE" "$BACKUP_ENV_COPY"
|
||||||
|
fi
|
||||||
|
|
||||||
if grep -q "^${key}=" "$ENV_FILE"; then
|
if [ -n "$PRESERVED_ENV" ] && [ -f "$PRESERVED_ENV" ]; then
|
||||||
sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE"
|
log "Restoring target env and merging missing keys from backup env"
|
||||||
else
|
|
||||||
echo "${key}=${value}" >> "$ENV_FILE"
|
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
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
log "Updating appfactory.env for target server"
|
ENV_SNAPSHOT="$APPFACTORY_ROOT/config/appfactory.env.before-restore-merge-$(date +%Y%m%d-%H%M%S)"
|
||||||
set_env_value "APPFACTORY_DIR" "$APPFACTORY_ROOT"
|
cp "$PRESERVED_ENV" "$ENV_SNAPSHOT"
|
||||||
set_env_value "WORKSPACE_DIR" "$APPFACTORY_ROOT/workspace"
|
ok "Saved pre-restore env snapshot: $ENV_SNAPSHOT"
|
||||||
set_env_value "BACKUP_DIR" "$APPFACTORY_ROOT/backups"
|
else
|
||||||
set_env_value "APPFACTORY_UID" "$APPFACTORY_UID"
|
[ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE"
|
||||||
set_env_value "APPFACTORY_GID" "$APPFACTORY_GID"
|
|
||||||
set_env_value "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID"
|
|
||||||
|
|
||||||
grep -q '^APPFACTORY_ENABLE_HTTPS=' "$ENV_FILE" || echo 'APPFACTORY_ENABLE_HTTPS=false' >> "$ENV_FILE"
|
log "No existing target env found; using backup env and applying target runtime defaults"
|
||||||
grep -q '^APPFACTORY_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_DOMAIN=' >> "$ENV_FILE"
|
|
||||||
grep -q '^APPFACTORY_PORTAL_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_PORTAL_DOMAIN=' >> "$ENV_FILE"
|
set_env_value "$ENV_FILE" "APPFACTORY_UID" "$APPFACTORY_UID"
|
||||||
grep -q '^APPFACTORY_GITEA_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_GITEA_DOMAIN=' >> "$ENV_FILE"
|
set_env_value "$ENV_FILE" "APPFACTORY_GID" "$APPFACTORY_GID"
|
||||||
grep -q '^APPFACTORY_REGISTRY_DOMAIN=' "$ENV_FILE" || echo 'APPFACTORY_REGISTRY_DOMAIN=' >> "$ENV_FILE"
|
set_env_value "$ENV_FILE" "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID"
|
||||||
grep -q '^GOOGLE_CLIENT_ID=' "$ENV_FILE" || echo 'GOOGLE_CLIENT_ID=' >> "$ENV_FILE"
|
fi
|
||||||
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 "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"
|
DB_FILE="$APPFACTORY_ROOT/data/appfactory/appfactory.db"
|
||||||
[ -f "$DB_FILE" ] || fail "Missing AppFactory DB after restore: $DB_FILE"
|
[ -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"
|
COMPOSE_FILE="$APPFACTORY_ROOT/deploy/docker-compose.yml"
|
||||||
[ -f "$COMPOSE_FILE" ] || fail "Missing compose file: $COMPOSE_FILE"
|
[ -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"
|
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"
|
log "Fixing script permissions"
|
||||||
find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
|
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"
|
[ -f "$dir/Dockerfile" ] || fail "Missing Dockerfile: $dir/Dockerfile"
|
||||||
|
|
||||||
log "Building image: $image from $dir"
|
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"
|
GENERATE_CADDY="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/generate-caddyfile.sh"
|
||||||
if [ -f "$GENERATE_CADDY" ]; then
|
if [ -f "$GENERATE_CADDY" ]; then
|
||||||
log "Generating Caddyfile before starting Caddy"
|
log "Generating Caddyfile before starting Caddy"
|
||||||
sudo -u "$TARGET_USER" "$GENERATE_CADDY" || true
|
"$GENERATE_CADDY" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build_image "appfactory-portal" "$APPFACTORY_ROOT/workspace/appfactory-portal"
|
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"
|
build_image "appfactory-webhook" "$APPFACTORY_ROOT/workspace/appfactory-webhook"
|
||||||
|
|
||||||
log "Deploying core compose stack"
|
log "Deploying core compose stack"
|
||||||
sudo -u "$TARGET_USER" docker compose \
|
docker compose \
|
||||||
--env-file "$ENV_FILE" \
|
--env-file "$ENV_FILE" \
|
||||||
-f "$COMPOSE_FILE" \
|
-f "$COMPOSE_FILE" \
|
||||||
up -d --force-recreate
|
up -d --force-recreate
|
||||||
@@ -184,7 +349,7 @@ done
|
|||||||
|
|
||||||
if [ -f "$GENERATE_CADDY" ]; then
|
if [ -f "$GENERATE_CADDY" ]; then
|
||||||
log "Regenerating/reloading Caddyfile"
|
log "Regenerating/reloading Caddyfile"
|
||||||
sudo -u "$TARGET_USER" "$GENERATE_CADDY"
|
"$GENERATE_CADDY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh"
|
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"
|
chmod +x "$PREFLIGHT"
|
||||||
|
|
||||||
log "Running final preflight"
|
log "Running final preflight"
|
||||||
sudo -u "$TARGET_USER" "$PREFLIGHT"
|
"$PREFLIGHT"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "APPFACTORY RESTORE: READY"
|
echo "APPFACTORY RESTORE: READY"
|
||||||
|
|||||||
Reference in New Issue
Block a user