mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
fix: update channel flexibility
This commit is contained in:
parent
bc7f84c123
commit
a105ac1a83
|
|
@ -2,6 +2,7 @@ import logger from '@adonisjs/core/services/logger'
|
||||||
import { readFileSync, existsSync } from 'fs'
|
import { readFileSync, existsSync } from 'fs'
|
||||||
import { writeFile } from 'fs/promises'
|
import { writeFile } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import KVStore from '#models/kv_store'
|
||||||
|
|
||||||
interface UpdateStatus {
|
interface UpdateStatus {
|
||||||
stage: 'idle' | 'starting' | 'pulling' | 'pulled' | 'recreating' | 'complete' | 'error'
|
stage: 'idle' | 'starting' | 'pulling' | 'pulled' | 'recreating' | 'complete' | 'error'
|
||||||
|
|
@ -21,7 +22,7 @@ export class SystemUpdateService {
|
||||||
*/
|
*/
|
||||||
async requestUpdate(): Promise<{ success: boolean; message: string }> {
|
async requestUpdate(): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
const currentStatus = this.getUpdateStatus()
|
const currentStatus = this.getUpdateStatus()
|
||||||
if (currentStatus && !['idle', 'complete', 'error'].includes(currentStatus.stage)) {
|
if (currentStatus && !['idle', 'complete', 'error'].includes(currentStatus.stage)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -29,13 +30,17 @@ export class SystemUpdateService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the Docker image tag to install.
|
||||||
|
const latestVersion = await KVStore.getValue('system.latestVersion')
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
requested_at: new Date().toISOString(),
|
requested_at: new Date().toISOString(),
|
||||||
requester: 'admin-api',
|
requester: 'admin-api',
|
||||||
|
target_tag: latestVersion || 'latest', // We should always have a latest version, but fallback to 'latest' just in case
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(SystemUpdateService.REQUEST_FILE, JSON.stringify(requestData, null, 2))
|
await writeFile(SystemUpdateService.REQUEST_FILE, JSON.stringify(requestData, null, 2))
|
||||||
logger.info('[SystemUpdateService]: System update requested - sidecar will process shortly')
|
logger.info(`[SystemUpdateService]: System update requested (target tag: ${requestData.target_tag}) - sidecar will process shortly`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -278,28 +278,6 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [isUpdating])
|
}, [isUpdating])
|
||||||
|
|
||||||
// Poll health endpoint when update is in recreating stage
|
|
||||||
useEffect(() => {
|
|
||||||
if (updateStatus?.stage !== 'recreating') return
|
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.healthCheck()
|
|
||||||
if (!response) {
|
|
||||||
throw new Error('Health check failed')
|
|
||||||
}
|
|
||||||
if (response.status === 'ok') {
|
|
||||||
// Reload page when container is back up
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Still restarting, continue polling...
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [updateStatus?.stage])
|
|
||||||
|
|
||||||
const handleStartUpdate = async () => {
|
const handleStartUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ services:
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
dozzle:
|
dozzle:
|
||||||
image: amir20/dozzle:latest
|
image: amir20/dozzle:v10.0
|
||||||
container_name: nomad_dozzle
|
container_name: nomad_dozzle
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -93,7 +93,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Allows communication with the Host's Docker daemon
|
- /var/run/docker.sock:/var/run/docker.sock # Allows communication with the Host's Docker daemon
|
||||||
- /opt/project-nomad:/opt/project-nomad:ro # Read-only access to the project dir for config files and scripts
|
- /opt/project-nomad:/opt/project-nomad # Writable access required so the updater can set the correct image tag in compose.yml
|
||||||
- nomad-update-shared:/shared # Shared volume for communication with admin container
|
- nomad-update-shared:/shared # Shared volume for communication with admin container
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
230
install/run_updater_fixes.sh
Normal file
230
install/run_updater_fixes.sh
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Project N.O.M.A.D. - One-Time Updater Fix Script
|
||||||
|
#
|
||||||
|
# Script | Project N.O.M.A.D. One-Time Updater Fix Script
|
||||||
|
# Version | 1.0.0
|
||||||
|
# Author | Crosstalk Solutions, LLC
|
||||||
|
# Website | https://crosstalksolutions.com
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# This is a one-time migration script. It deploys two fixes to the sidecar
|
||||||
|
# updater that cannot be applied through the normal in-app update mechanism:
|
||||||
|
#
|
||||||
|
# Fix 1 — Sidecar volume write access
|
||||||
|
# Removes the :ro (read-only) flag from the sidecar's /opt/project-nomad
|
||||||
|
# volume mount in compose.yml. The sidecar must be able to write to
|
||||||
|
# compose.yml so it can set the correct Docker image tag when installing
|
||||||
|
# RC or stable versions.
|
||||||
|
#
|
||||||
|
# Fix 2 — RC-aware sidecar watcher
|
||||||
|
# Downloads the updated sidecar Dockerfile (adds jq) and update-watcher.sh
|
||||||
|
# (reads target_tag from the update request and applies it to compose.yml
|
||||||
|
# before pulling images), then rebuilds and restarts the sidecar container.
|
||||||
|
#
|
||||||
|
# NOTE: The companion fix in the admin service (system_update_service.ts,
|
||||||
|
# which writes the target_tag into the update request) ships in the GHCR
|
||||||
|
# image and will take effect automatically on the next normal app update.
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Color Codes
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
RESET='\033[0m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[1;31m'
|
||||||
|
GREEN='\033[1;32m'
|
||||||
|
WHITE_R='\033[39m'
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Constants
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
NOMAD_DIR="/opt/project-nomad"
|
||||||
|
COMPOSE_FILE="${NOMAD_DIR}/compose.yml"
|
||||||
|
SIDECAR_DIR="${NOMAD_DIR}/sidecar-updater"
|
||||||
|
COMPOSE_PROJECT_NAME="project-nomad"
|
||||||
|
|
||||||
|
SIDECAR_DOCKERFILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/Dockerfile"
|
||||||
|
SIDECAR_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/update-watcher.sh"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Pre-flight Checks
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
check_is_bash() {
|
||||||
|
if [[ -z "$BASH_VERSION" ]]; then
|
||||||
|
echo -e "${RED}#${RESET} This script must be run with bash."
|
||||||
|
echo -e "${RED}#${RESET} Example: bash $(basename "$0")"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Running in bash.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_confirmation() {
|
||||||
|
echo -e "${YELLOW}#${RESET} This is a very specific fix script for a very specific issue. You probably don't need to run this unless you were specifically directed to by the N.O.M.A.D. team."
|
||||||
|
echo -e "${YELLOW}#${RESET} Please ensure you have a backup of your data before proceeding."
|
||||||
|
read -rp "Do you want to continue? (y/N) " response
|
||||||
|
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}#${RESET} Aborting. No changes have been made."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Confirmation received. Proceeding with fixes...\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_has_sudo() {
|
||||||
|
if sudo -n true 2>/dev/null; then
|
||||||
|
echo -e "${GREEN}#${RESET} Sudo permissions confirmed.\n"
|
||||||
|
else
|
||||||
|
echo -e "${RED}#${RESET} This script requires sudo permissions."
|
||||||
|
echo -e "${RED}#${RESET} Example: sudo bash $(basename "$0")"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_docker_running() {
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo -e "${RED}#${RESET} Docker is not installed. Cannot proceed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! systemctl is-active --quiet docker; then
|
||||||
|
echo -e "${RED}#${RESET} Docker is not running. Please start Docker and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Docker is running.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_compose_file() {
|
||||||
|
if [[ ! -f "$COMPOSE_FILE" ]]; then
|
||||||
|
echo -e "${RED}#${RESET} compose.yml not found at ${COMPOSE_FILE}."
|
||||||
|
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Found compose.yml at ${COMPOSE_FILE}.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_sidecar_dir() {
|
||||||
|
if [[ ! -d "$SIDECAR_DIR" ]]; then
|
||||||
|
echo -e "${RED}#${RESET} Sidecar directory not found at ${SIDECAR_DIR}."
|
||||||
|
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Found sidecar directory at ${SIDECAR_DIR}.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Fix 1 — Remove :ro from sidecar volume mount
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
backup_compose_file() {
|
||||||
|
local backup="${COMPOSE_FILE}.bak.$(date +%Y%m%d%H%M%S)"
|
||||||
|
echo -e "${YELLOW}#${RESET} Backing up compose.yml to ${backup}..."
|
||||||
|
if cp "$COMPOSE_FILE" "$backup"; then
|
||||||
|
echo -e "${GREEN}#${RESET} Backup created at ${backup}.\n"
|
||||||
|
else
|
||||||
|
echo -e "${RED}#${RESET} Failed to create backup. Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_sidecar_volume_mount() {
|
||||||
|
# Idempotent: skip if :ro is already absent from the sidecar mount line
|
||||||
|
if ! grep -q '/opt/project-nomad:/opt/project-nomad:ro' "$COMPOSE_FILE"; then
|
||||||
|
echo -e "${GREEN}#${RESET} Sidecar volume mount is already writable — no change needed.\n"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}#${RESET} Removing :ro restriction from sidecar volume mount in compose.yml..."
|
||||||
|
sed -i 's|/opt/project-nomad:/opt/project-nomad:ro.*|/opt/project-nomad:/opt/project-nomad # Writable access required so the updater can set the correct image tag in compose.yml|' "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
if grep -q '/opt/project-nomad:/opt/project-nomad:ro' "$COMPOSE_FILE"; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to remove :ro from compose.yml. Please update it manually:"
|
||||||
|
echo -e "${WHITE_R} - /opt/project-nomad:/opt/project-nomad:ro${RESET} → ${WHITE_R}- /opt/project-nomad:/opt/project-nomad${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}#${RESET} Sidecar volume mount updated successfully.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Fix 2 — Download updated sidecar files and rebuild
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
download_updated_sidecar_files() {
|
||||||
|
echo -e "${YELLOW}#${RESET} Downloading updated sidecar Dockerfile..."
|
||||||
|
if ! curl -fsSL "$SIDECAR_DOCKERFILE_URL" -o "${SIDECAR_DIR}/Dockerfile"; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to download sidecar Dockerfile. Check your network connection."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Sidecar Dockerfile updated.\n"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}#${RESET} Downloading updated update-watcher.sh..."
|
||||||
|
if ! curl -fsSL "$SIDECAR_SCRIPT_URL" -o "${SIDECAR_DIR}/update-watcher.sh"; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to download update-watcher.sh. Check your network connection."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod +x "${SIDECAR_DIR}/update-watcher.sh"
|
||||||
|
echo -e "${GREEN}#${RESET} update-watcher.sh updated.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuild_sidecar() {
|
||||||
|
echo -e "${YELLOW}#${RESET} Rebuilding the updater container (this may take a moment)..."
|
||||||
|
if ! docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" build updater; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to rebuild the updater container. See output above for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Updater container rebuilt successfully.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_sidecar() {
|
||||||
|
echo -e "${YELLOW}#${RESET} Restarting the updater container..."
|
||||||
|
if ! docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" up -d --force-recreate updater; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to restart the updater container."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}#${RESET} Updater container restarted.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_sidecar_running() {
|
||||||
|
sleep 3
|
||||||
|
if docker ps --filter "name=nomad_updater" --filter "status=running" --format '{{.Names}}' | grep -q "nomad_updater"; then
|
||||||
|
echo -e "${GREEN}#${RESET} Updater container is running.\n"
|
||||||
|
else
|
||||||
|
echo -e "${RED}#${RESET} Updater container does not appear to be running."
|
||||||
|
echo -e "${RED}#${RESET} Check its logs with: docker logs nomad_updater"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Main
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
echo -e "${GREEN}#########################################################################${RESET}"
|
||||||
|
echo -e "${GREEN}#${RESET} Project N.O.M.A.D. — One-Time Updater Fix Script ${GREEN}#${RESET}"
|
||||||
|
echo -e "${GREEN}#########################################################################${RESET}\n"
|
||||||
|
|
||||||
|
check_is_bash
|
||||||
|
check_has_sudo
|
||||||
|
chech_confirmation
|
||||||
|
check_docker_running
|
||||||
|
check_compose_file
|
||||||
|
check_sidecar_dir
|
||||||
|
|
||||||
|
echo -e "${YELLOW}#${RESET} Starting Fix 1: Sidecar volume write access...\n"
|
||||||
|
backup_compose_file
|
||||||
|
fix_sidecar_volume_mount
|
||||||
|
|
||||||
|
echo -e "${YELLOW}#${RESET} Starting Fix 2: RC-aware sidecar watcher...\n"
|
||||||
|
download_updated_sidecar_files
|
||||||
|
rebuild_sidecar
|
||||||
|
restart_sidecar
|
||||||
|
verify_sidecar_running
|
||||||
|
|
||||||
|
echo -e "${GREEN}#########################################################################${RESET}"
|
||||||
|
echo -e "${GREEN}#${RESET} All fixes applied successfully!"
|
||||||
|
echo -e "${GREEN}#${RESET}"
|
||||||
|
echo -e "${GREEN}#${RESET} The updater sidecar can now install RC and stable versions correctly."
|
||||||
|
echo -e "${GREEN}#${RESET} The remaining fix (admin service target_tag support) will apply"
|
||||||
|
echo -e "${GREEN}#${RESET} automatically the next time you update N.O.M.A.D. via the UI."
|
||||||
|
echo -e "${GREEN}#########################################################################${RESET}\n"
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
# Install Docker CLI for compose operations
|
# Install Docker CLI for compose operations
|
||||||
RUN apk add --no-cache docker-cli docker-cli-compose bash
|
RUN apk add --no-cache docker-cli docker-cli-compose bash jq
|
||||||
|
|
||||||
# Copy the update watcher script
|
# Copy the update watcher script
|
||||||
COPY update-watcher.sh /usr/local/bin/update-watcher.sh
|
COPY update-watcher.sh /usr/local/bin/update-watcher.sh
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,32 @@ EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
perform_update() {
|
perform_update() {
|
||||||
log "Update request received - starting system update"
|
local target_tag="$1"
|
||||||
|
|
||||||
|
log "Update request received - starting system update (target tag: ${target_tag})"
|
||||||
|
|
||||||
# Clear old logs
|
# Clear old logs
|
||||||
> "$LOG_FILE"
|
> "$LOG_FILE"
|
||||||
|
|
||||||
# Stage 1: Starting
|
# Stage 1: Starting
|
||||||
write_status "starting" 0 "System update initiated"
|
write_status "starting" 0 "System update initiated"
|
||||||
log "System update initiated"
|
log "System update initiated"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
|
# Apply target image tag to compose.yml before pulling
|
||||||
|
log "Applying image tag '${target_tag}' to compose.yml..."
|
||||||
|
if sed -i "s|\(image: ghcr\.io/crosstalk-solutions/project-nomad\):.*|\1:${target_tag}|" "$COMPOSE_FILE" 2>> "$LOG_FILE"; then
|
||||||
|
log "Successfully updated compose.yml admin image tag to '${target_tag}'"
|
||||||
|
else
|
||||||
|
log "ERROR: Failed to update compose.yml image tag"
|
||||||
|
write_status "error" 0 "Failed to update compose.yml image tag - check logs"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Stage 2: Pulling images
|
# Stage 2: Pulling images
|
||||||
write_status "pulling" 20 "Pulling latest Docker images..."
|
write_status "pulling" 20 "Pulling latest Docker images..."
|
||||||
log "Pulling latest Docker images..."
|
log "Pulling latest Docker images..."
|
||||||
|
|
||||||
if docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" pull >> "$LOG_FILE" 2>&1; then
|
if docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" pull >> "$LOG_FILE" 2>&1; then
|
||||||
log "Successfully pulled latest images"
|
log "Successfully pulled latest images"
|
||||||
write_status "pulled" 60 "Images pulled successfully"
|
write_status "pulled" 60 "Images pulled successfully"
|
||||||
|
|
@ -112,14 +124,18 @@ while true; do
|
||||||
if [ -f "$REQUEST_FILE" ]; then
|
if [ -f "$REQUEST_FILE" ]; then
|
||||||
log "Found update request file"
|
log "Found update request file"
|
||||||
|
|
||||||
# Read request details (could contain metadata like requester, timestamp, etc.)
|
# Read request details
|
||||||
REQUEST_DATA=$(cat "$REQUEST_FILE" 2>/dev/null || echo "{}")
|
REQUEST_DATA=$(cat "$REQUEST_FILE" 2>/dev/null || echo "{}")
|
||||||
log "Request data: $REQUEST_DATA"
|
log "Request data: $REQUEST_DATA"
|
||||||
|
|
||||||
|
# Extract target tag from request (defaults to "latest" if not provided)
|
||||||
|
TARGET_TAG=$(echo "$REQUEST_DATA" | jq -r '.target_tag // "latest"')
|
||||||
|
log "Target image tag: ${TARGET_TAG}"
|
||||||
|
|
||||||
# Remove the request file to prevent re-processing
|
# Remove the request file to prevent re-processing
|
||||||
rm -f "$REQUEST_FILE"
|
rm -f "$REQUEST_FILE"
|
||||||
|
|
||||||
if perform_update; then
|
if perform_update "$TARGET_TAG"; then
|
||||||
log "Update completed successfully"
|
log "Update completed successfully"
|
||||||
else
|
else
|
||||||
log "Update failed - see logs for details"
|
log "Update failed - see logs for details"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user