Skip to content

Commit a105ac1

Browse files
committed
fix: update channel flexibility
1 parent bc7f84c commit a105ac1

File tree

6 files changed

+265
-36
lines changed

6 files changed

+265
-36
lines changed

admin/app/services/system_update_service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import logger from '@adonisjs/core/services/logger'
22
import { readFileSync, existsSync } from 'fs'
33
import { writeFile } from 'fs/promises'
44
import { join } from 'path'
5+
import KVStore from '#models/kv_store'
56

67
interface UpdateStatus {
78
stage: 'idle' | 'starting' | 'pulling' | 'pulled' | 'recreating' | 'complete' | 'error'
@@ -21,21 +22,25 @@ export class SystemUpdateService {
2122
*/
2223
async requestUpdate(): Promise<{ success: boolean; message: string }> {
2324
try {
24-
const currentStatus = this.getUpdateStatus()
25+
const currentStatus = this.getUpdateStatus()
2526
if (currentStatus && !['idle', 'complete', 'error'].includes(currentStatus.stage)) {
2627
return {
2728
success: false,
2829
message: `Update already in progress (stage: ${currentStatus.stage})`,
2930
}
3031
}
3132

33+
// Determine the Docker image tag to install.
34+
const latestVersion = await KVStore.getValue('system.latestVersion')
35+
3236
const requestData = {
3337
requested_at: new Date().toISOString(),
3438
requester: 'admin-api',
39+
target_tag: latestVersion || 'latest', // We should always have a latest version, but fallback to 'latest' just in case
3540
}
3641

3742
await writeFile(SystemUpdateService.REQUEST_FILE, JSON.stringify(requestData, null, 2))
38-
logger.info('[SystemUpdateService]: System update requested - sidecar will process shortly')
43+
logger.info(`[SystemUpdateService]: System update requested (target tag: ${requestData.target_tag}) - sidecar will process shortly`)
3944

4045
return {
4146
success: true,

admin/inertia/pages/settings/update.tsx

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -278,28 +278,6 @@ export default function SystemUpdatePage(props: { system: Props }) {
278278
return () => clearInterval(interval)
279279
}, [isUpdating])
280280

281-
// Poll health endpoint when update is in recreating stage
282-
useEffect(() => {
283-
if (updateStatus?.stage !== 'recreating') return
284-
285-
const interval = setInterval(async () => {
286-
try {
287-
const response = await api.healthCheck()
288-
if (!response) {
289-
throw new Error('Health check failed')
290-
}
291-
if (response.status === 'ok') {
292-
// Reload page when container is back up
293-
window.location.reload()
294-
}
295-
} catch (err) {
296-
// Still restarting, continue polling...
297-
}
298-
}, 3000)
299-
300-
return () => clearInterval(interval)
301-
}, [updateStatus?.stage])
302-
303281
const handleStartUpdate = async () => {
304282
try {
305283
setError(null)

install/management_compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ services:
4444
timeout: 10s
4545
retries: 3
4646
dozzle:
47-
image: amir20/dozzle:latest
47+
image: amir20/dozzle:v10.0
4848
container_name: nomad_dozzle
4949
restart: unless-stopped
5050
ports:
@@ -93,7 +93,7 @@ services:
9393
restart: unless-stopped
9494
volumes:
9595
- /var/run/docker.sock:/var/run/docker.sock # Allows communication with the Host's Docker daemon
96-
- /opt/project-nomad:/opt/project-nomad:ro # Read-only access to the project dir for config files and scripts
96+
- /opt/project-nomad:/opt/project-nomad # Writable access required so the updater can set the correct image tag in compose.yml
9797
- nomad-update-shared:/shared # Shared volume for communication with admin container
9898

9999
volumes:

install/run_updater_fixes.sh

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/bin/bash
2+
3+
# Project N.O.M.A.D. - One-Time Updater Fix Script
4+
#
5+
# Script | Project N.O.M.A.D. One-Time Updater Fix Script
6+
# Version | 1.0.0
7+
# Author | Crosstalk Solutions, LLC
8+
# Website | https://crosstalksolutions.com
9+
#
10+
# PURPOSE:
11+
# This is a one-time migration script. It deploys two fixes to the sidecar
12+
# updater that cannot be applied through the normal in-app update mechanism:
13+
#
14+
# Fix 1 — Sidecar volume write access
15+
# Removes the :ro (read-only) flag from the sidecar's /opt/project-nomad
16+
# volume mount in compose.yml. The sidecar must be able to write to
17+
# compose.yml so it can set the correct Docker image tag when installing
18+
# RC or stable versions.
19+
#
20+
# Fix 2 — RC-aware sidecar watcher
21+
# Downloads the updated sidecar Dockerfile (adds jq) and update-watcher.sh
22+
# (reads target_tag from the update request and applies it to compose.yml
23+
# before pulling images), then rebuilds and restarts the sidecar container.
24+
#
25+
# NOTE: The companion fix in the admin service (system_update_service.ts,
26+
# which writes the target_tag into the update request) ships in the GHCR
27+
# image and will take effect automatically on the next normal app update.
28+
29+
###############################################################################
30+
# Color Codes
31+
###############################################################################
32+
33+
RESET='\033[0m'
34+
YELLOW='\033[1;33m'
35+
RED='\033[1;31m'
36+
GREEN='\033[1;32m'
37+
WHITE_R='\033[39m'
38+
39+
###############################################################################
40+
# Constants
41+
###############################################################################
42+
43+
NOMAD_DIR="/opt/project-nomad"
44+
COMPOSE_FILE="${NOMAD_DIR}/compose.yml"
45+
SIDECAR_DIR="${NOMAD_DIR}/sidecar-updater"
46+
COMPOSE_PROJECT_NAME="project-nomad"
47+
48+
SIDECAR_DOCKERFILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/Dockerfile"
49+
SIDECAR_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/update-watcher.sh"
50+
51+
###############################################################################
52+
# Pre-flight Checks
53+
###############################################################################
54+
55+
check_is_bash() {
56+
if [[ -z "$BASH_VERSION" ]]; then
57+
echo -e "${RED}#${RESET} This script must be run with bash."
58+
echo -e "${RED}#${RESET} Example: bash $(basename "$0")"
59+
exit 1
60+
fi
61+
echo -e "${GREEN}#${RESET} Running in bash.\n"
62+
}
63+
64+
check_confirmation() {
65+
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."
66+
echo -e "${YELLOW}#${RESET} Please ensure you have a backup of your data before proceeding."
67+
read -rp "Do you want to continue? (y/N) " response
68+
if [[ ! "$response" =~ ^[Yy]$ ]]; then
69+
echo -e "${RED}#${RESET} Aborting. No changes have been made."
70+
exit 0
71+
fi
72+
echo -e "${GREEN}#${RESET} Confirmation received. Proceeding with fixes...\n"
73+
}
74+
75+
check_has_sudo() {
76+
if sudo -n true 2>/dev/null; then
77+
echo -e "${GREEN}#${RESET} Sudo permissions confirmed.\n"
78+
else
79+
echo -e "${RED}#${RESET} This script requires sudo permissions."
80+
echo -e "${RED}#${RESET} Example: sudo bash $(basename "$0")"
81+
exit 1
82+
fi
83+
}
84+
85+
check_docker_running() {
86+
if ! command -v docker &>/dev/null; then
87+
echo -e "${RED}#${RESET} Docker is not installed. Cannot proceed."
88+
exit 1
89+
fi
90+
if ! systemctl is-active --quiet docker; then
91+
echo -e "${RED}#${RESET} Docker is not running. Please start Docker and try again."
92+
exit 1
93+
fi
94+
echo -e "${GREEN}#${RESET} Docker is running.\n"
95+
}
96+
97+
check_compose_file() {
98+
if [[ ! -f "$COMPOSE_FILE" ]]; then
99+
echo -e "${RED}#${RESET} compose.yml not found at ${COMPOSE_FILE}."
100+
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
101+
exit 1
102+
fi
103+
echo -e "${GREEN}#${RESET} Found compose.yml at ${COMPOSE_FILE}.\n"
104+
}
105+
106+
check_sidecar_dir() {
107+
if [[ ! -d "$SIDECAR_DIR" ]]; then
108+
echo -e "${RED}#${RESET} Sidecar directory not found at ${SIDECAR_DIR}."
109+
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
110+
exit 1
111+
fi
112+
echo -e "${GREEN}#${RESET} Found sidecar directory at ${SIDECAR_DIR}.\n"
113+
}
114+
115+
###############################################################################
116+
# Fix 1 — Remove :ro from sidecar volume mount
117+
###############################################################################
118+
119+
backup_compose_file() {
120+
local backup="${COMPOSE_FILE}.bak.$(date +%Y%m%d%H%M%S)"
121+
echo -e "${YELLOW}#${RESET} Backing up compose.yml to ${backup}..."
122+
if cp "$COMPOSE_FILE" "$backup"; then
123+
echo -e "${GREEN}#${RESET} Backup created at ${backup}.\n"
124+
else
125+
echo -e "${RED}#${RESET} Failed to create backup. Aborting."
126+
exit 1
127+
fi
128+
}
129+
130+
fix_sidecar_volume_mount() {
131+
# Idempotent: skip if :ro is already absent from the sidecar mount line
132+
if ! grep -q '/opt/project-nomad:/opt/project-nomad:ro' "$COMPOSE_FILE"; then
133+
echo -e "${GREEN}#${RESET} Sidecar volume mount is already writable — no change needed.\n"
134+
return 0
135+
fi
136+
137+
echo -e "${YELLOW}#${RESET} Removing :ro restriction from sidecar volume mount in compose.yml..."
138+
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"
139+
140+
if grep -q '/opt/project-nomad:/opt/project-nomad:ro' "$COMPOSE_FILE"; then
141+
echo -e "${RED}#${RESET} Failed to remove :ro from compose.yml. Please update it manually:"
142+
echo -e "${WHITE_R} - /opt/project-nomad:/opt/project-nomad:ro${RESET}${WHITE_R}- /opt/project-nomad:/opt/project-nomad${RESET}"
143+
exit 1
144+
fi
145+
146+
echo -e "${GREEN}#${RESET} Sidecar volume mount updated successfully.\n"
147+
}
148+
149+
###############################################################################
150+
# Fix 2 — Download updated sidecar files and rebuild
151+
###############################################################################
152+
153+
download_updated_sidecar_files() {
154+
echo -e "${YELLOW}#${RESET} Downloading updated sidecar Dockerfile..."
155+
if ! curl -fsSL "$SIDECAR_DOCKERFILE_URL" -o "${SIDECAR_DIR}/Dockerfile"; then
156+
echo -e "${RED}#${RESET} Failed to download sidecar Dockerfile. Check your network connection."
157+
exit 1
158+
fi
159+
echo -e "${GREEN}#${RESET} Sidecar Dockerfile updated.\n"
160+
161+
echo -e "${YELLOW}#${RESET} Downloading updated update-watcher.sh..."
162+
if ! curl -fsSL "$SIDECAR_SCRIPT_URL" -o "${SIDECAR_DIR}/update-watcher.sh"; then
163+
echo -e "${RED}#${RESET} Failed to download update-watcher.sh. Check your network connection."
164+
exit 1
165+
fi
166+
chmod +x "${SIDECAR_DIR}/update-watcher.sh"
167+
echo -e "${GREEN}#${RESET} update-watcher.sh updated.\n"
168+
}
169+
170+
rebuild_sidecar() {
171+
echo -e "${YELLOW}#${RESET} Rebuilding the updater container (this may take a moment)..."
172+
if ! docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" build updater; then
173+
echo -e "${RED}#${RESET} Failed to rebuild the updater container. See output above for details."
174+
exit 1
175+
fi
176+
echo -e "${GREEN}#${RESET} Updater container rebuilt successfully.\n"
177+
}
178+
179+
restart_sidecar() {
180+
echo -e "${YELLOW}#${RESET} Restarting the updater container..."
181+
if ! docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" up -d --force-recreate updater; then
182+
echo -e "${RED}#${RESET} Failed to restart the updater container."
183+
exit 1
184+
fi
185+
echo -e "${GREEN}#${RESET} Updater container restarted.\n"
186+
}
187+
188+
verify_sidecar_running() {
189+
sleep 3
190+
if docker ps --filter "name=nomad_updater" --filter "status=running" --format '{{.Names}}' | grep -q "nomad_updater"; then
191+
echo -e "${GREEN}#${RESET} Updater container is running.\n"
192+
else
193+
echo -e "${RED}#${RESET} Updater container does not appear to be running."
194+
echo -e "${RED}#${RESET} Check its logs with: docker logs nomad_updater"
195+
exit 1
196+
fi
197+
}
198+
199+
###############################################################################
200+
# Main
201+
###############################################################################
202+
203+
echo -e "${GREEN}#########################################################################${RESET}"
204+
echo -e "${GREEN}#${RESET} Project N.O.M.A.D. — One-Time Updater Fix Script ${GREEN}#${RESET}"
205+
echo -e "${GREEN}#########################################################################${RESET}\n"
206+
207+
check_is_bash
208+
check_has_sudo
209+
chech_confirmation
210+
check_docker_running
211+
check_compose_file
212+
check_sidecar_dir
213+
214+
echo -e "${YELLOW}#${RESET} Starting Fix 1: Sidecar volume write access...\n"
215+
backup_compose_file
216+
fix_sidecar_volume_mount
217+
218+
echo -e "${YELLOW}#${RESET} Starting Fix 2: RC-aware sidecar watcher...\n"
219+
download_updated_sidecar_files
220+
rebuild_sidecar
221+
restart_sidecar
222+
verify_sidecar_running
223+
224+
echo -e "${GREEN}#########################################################################${RESET}"
225+
echo -e "${GREEN}#${RESET} All fixes applied successfully!"
226+
echo -e "${GREEN}#${RESET}"
227+
echo -e "${GREEN}#${RESET} The updater sidecar can now install RC and stable versions correctly."
228+
echo -e "${GREEN}#${RESET} The remaining fix (admin service target_tag support) will apply"
229+
echo -e "${GREEN}#${RESET} automatically the next time you update N.O.M.A.D. via the UI."
230+
echo -e "${GREEN}#########################################################################${RESET}\n"

install/sidecar-updater/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM alpine:3.20
22

33
# Install Docker CLI for compose operations
4-
RUN apk add --no-cache docker-cli docker-cli-compose bash
4+
RUN apk add --no-cache docker-cli docker-cli-compose bash jq
55

66
# Copy the update watcher script
77
COPY update-watcher.sh /usr/local/bin/update-watcher.sh

install/sidecar-updater/update-watcher.sh

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,32 @@ EOF
2929
}
3030

3131
perform_update() {
32-
log "Update request received - starting system update"
33-
32+
local target_tag="$1"
33+
34+
log "Update request received - starting system update (target tag: ${target_tag})"
35+
3436
# Clear old logs
3537
> "$LOG_FILE"
36-
38+
3739
# Stage 1: Starting
3840
write_status "starting" 0 "System update initiated"
3941
log "System update initiated"
4042
sleep 1
41-
43+
44+
# Apply target image tag to compose.yml before pulling
45+
log "Applying image tag '${target_tag}' to compose.yml..."
46+
if sed -i "s|\(image: ghcr\.io/crosstalk-solutions/project-nomad\):.*|\1:${target_tag}|" "$COMPOSE_FILE" 2>> "$LOG_FILE"; then
47+
log "Successfully updated compose.yml admin image tag to '${target_tag}'"
48+
else
49+
log "ERROR: Failed to update compose.yml image tag"
50+
write_status "error" 0 "Failed to update compose.yml image tag - check logs"
51+
return 1
52+
fi
53+
4254
# Stage 2: Pulling images
4355
write_status "pulling" 20 "Pulling latest Docker images..."
4456
log "Pulling latest Docker images..."
45-
57+
4658
if docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" pull >> "$LOG_FILE" 2>&1; then
4759
log "Successfully pulled latest images"
4860
write_status "pulled" 60 "Images pulled successfully"
@@ -112,14 +124,18 @@ while true; do
112124
if [ -f "$REQUEST_FILE" ]; then
113125
log "Found update request file"
114126

115-
# Read request details (could contain metadata like requester, timestamp, etc.)
127+
# Read request details
116128
REQUEST_DATA=$(cat "$REQUEST_FILE" 2>/dev/null || echo "{}")
117129
log "Request data: $REQUEST_DATA"
118-
130+
131+
# Extract target tag from request (defaults to "latest" if not provided)
132+
TARGET_TAG=$(echo "$REQUEST_DATA" | jq -r '.target_tag // "latest"')
133+
log "Target image tag: ${TARGET_TAG}"
134+
119135
# Remove the request file to prevent re-processing
120136
rm -f "$REQUEST_FILE"
121-
122-
if perform_update; then
137+
138+
if perform_update "$TARGET_TAG"; then
123139
log "Update completed successfully"
124140
else
125141
log "Update failed - see logs for details"

0 commit comments

Comments
 (0)