fix: update Semaphore port to 3000 and enhance database backup process
This commit is contained in:
+22
-24
@@ -15,14 +15,12 @@
|
|||||||
semaphore_project: semaphore
|
semaphore_project: semaphore
|
||||||
semaphore_container: semaphore
|
semaphore_container: semaphore
|
||||||
semaphore_compose_filename: "docker-compose-semaphore.yml"
|
semaphore_compose_filename: "docker-compose-semaphore.yml"
|
||||||
# Published port on the VM. NOT 3000 — that only exists in the container.
|
semaphore_port: 3000
|
||||||
semaphore_port: 3008
|
|
||||||
|
|
||||||
semaphore_update_log: "{{ compose_remote_base }}/semaphore-update.log"
|
semaphore_update_log: "{{ compose_remote_base }}/semaphore-update.log"
|
||||||
|
semaphore_update_delay: 20
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
# This job gets killed when the container is recreated, so the final
|
|
||||||
# verification lives in the log and is reported on the next run.
|
|
||||||
- name: Show result of the previous update
|
- name: Show result of the previous update
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: [tail, -n, "12", "{{ semaphore_update_log }}"]
|
argv: [tail, -n, "12", "{{ semaphore_update_log }}"]
|
||||||
@@ -34,9 +32,6 @@
|
|||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
var: prev_log.stdout_lines
|
var: prev_log.stdout_lines
|
||||||
|
|
||||||
# Env vars override /etc/semaphore/config.json. A stray SEMAPHORE_DB_*
|
|
||||||
# points Semaphore at a different database; a stray
|
|
||||||
# SEMAPHORE_ACCESS_KEY_ENCRYPTION makes every credential undecryptable.
|
|
||||||
- name: Refuse compose that overrides DB or encryption config
|
- name: Refuse compose that overrides DB or encryption config
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
@@ -87,8 +82,6 @@
|
|||||||
dest: "{{ compose_remote_dir }}"
|
dest: "{{ compose_remote_dir }}"
|
||||||
remote_src: true
|
remote_src: true
|
||||||
|
|
||||||
# Online snapshot via the sqlite backup API — consistent without stopping
|
|
||||||
# the server. Reads the DB path from the live config instead of guessing.
|
|
||||||
- name: Back up Semaphore database
|
- name: Back up Semaphore database
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
@@ -102,7 +95,7 @@
|
|||||||
cfg = json.load(open('/etc/semaphore/config.json'))
|
cfg = json.load(open('/etc/semaphore/config.json'))
|
||||||
out = '/etc/semaphore/backups'
|
out = '/etc/semaphore/backups'
|
||||||
os.makedirs(out, exist_ok=True)
|
os.makedirs(out, exist_ok=True)
|
||||||
ts = datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')
|
ts = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%d-%H%M%S')
|
||||||
dst = f'{out}/database-{ts}.sqlite'
|
dst = f'{out}/database-{ts}.sqlite'
|
||||||
a = sqlite3.connect('file:' + cfg['sqlite']['host'] + '?mode=ro', uri=True)
|
a = sqlite3.connect('file:' + cfg['sqlite']['host'] + '?mode=ro', uri=True)
|
||||||
b = sqlite3.connect(dst)
|
b = sqlite3.connect(dst)
|
||||||
@@ -113,6 +106,10 @@
|
|||||||
print(dst, os.path.getsize(dst), 'bytes')
|
print(dst, os.path.getsize(dst), 'bytes')
|
||||||
for old in sorted(glob.glob(f'{out}/database-*.sqlite'), reverse=True)[10:]:
|
for old in sorted(glob.glob(f'{out}/database-*.sqlite'), reverse=True)[10:]:
|
||||||
os.remove(old)
|
os.remove(old)
|
||||||
|
stale = f"{out}/config-{os.path.basename(old)[9:-7]}.json"
|
||||||
|
if os.path.exists(stale):
|
||||||
|
os.remove(stale)
|
||||||
|
print('pruned', old)
|
||||||
register: sem_db_backup
|
register: sem_db_backup
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
@@ -120,15 +117,6 @@
|
|||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
var: sem_db_backup.stdout_lines
|
var: sem_db_backup.stdout_lines
|
||||||
|
|
||||||
- name: Pull latest Semaphore image
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_name: "{{ semaphore_project }}"
|
|
||||||
project_src: "{{ compose_remote_dir }}"
|
|
||||||
files:
|
|
||||||
- "{{ semaphore_compose_filename }}"
|
|
||||||
pull: always
|
|
||||||
|
|
||||||
|
|
||||||
- name: Recreate Semaphore in the background
|
- name: Recreate Semaphore in the background
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
cat > "{{ compose_remote_base }}/semaphore-selfupdate.sh" <<'SH'
|
cat > "{{ compose_remote_base }}/semaphore-selfupdate.sh" <<'SH'
|
||||||
@@ -137,17 +125,27 @@
|
|||||||
exec 2>&1
|
exec 2>&1
|
||||||
cd "{{ compose_remote_dir }}" || exit 1
|
cd "{{ compose_remote_dir }}" || exit 1
|
||||||
COMPOSE=(docker compose -p "{{ semaphore_project }}" -f "{{ semaphore_compose_filename }}")
|
COMPOSE=(docker compose -p "{{ semaphore_project }}" -f "{{ semaphore_compose_filename }}")
|
||||||
|
|
||||||
|
# Let the calling job finish and its status get written to the DB
|
||||||
|
# before we potentially replace the container running it.
|
||||||
|
sleep {{ semaphore_update_delay }}
|
||||||
|
|
||||||
echo "=== $(date -Is) update starting ==="
|
echo "=== $(date -Is) update starting ==="
|
||||||
"${COMPOSE[@]}" pull || { echo "FAIL: pull failed" >&2; exit 1; }
|
"${COMPOSE[@]}" pull || { echo "FAIL: pull failed" >&2; exit 1; }
|
||||||
|
|
||||||
|
# No --force-recreate: compose replaces the container only when the
|
||||||
|
# image or the resolved config (including env_file) actually changed,
|
||||||
|
# so unchanged runs cause no downtime.
|
||||||
"${COMPOSE[@]}" up -d --remove-orphans --wait --wait-timeout 300
|
"${COMPOSE[@]}" up -d --remove-orphans --wait --wait-timeout 300
|
||||||
|
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
"http://127.0.0.1:{{ semaphore_port }}/api/ping" || true)
|
"http://127.0.0.1:{{ semaphore_port }}/api/ping" || true)
|
||||||
if [ "$code" = "200" ]; then
|
if [ "$code" = "200" ]; then
|
||||||
echo "image: $(docker inspect "{{ semaphore_container }}" \
|
echo "image: $(docker inspect "{{ semaphore_container }}" \
|
||||||
--format '{{ '{{' }}.Config.Image{{ '}}' }}')"
|
--format '{{ '{{' }}.Config.Image{{ '}}' }}')"
|
||||||
# --tail 40 misses the startup line when the container was not
|
# Grep the whole log: when the container was not replaced, the
|
||||||
# recreated; grep the whole log instead
|
# startup line is far outside the last few lines.
|
||||||
docker logs "{{ semaphore_container }}" 2>&1 \
|
docker logs "{{ semaphore_container }}" 2>&1 \
|
||||||
| grep -m1 'SQLite @' || echo "WARN: no 'SQLite @' line"
|
| grep -m1 'SQLite @' || echo "WARN: no 'SQLite @' line"
|
||||||
echo "OK: semaphore healthy at $(date -Is)"
|
echo "OK: semaphore healthy at $(date -Is)"
|
||||||
@@ -169,6 +167,6 @@
|
|||||||
- name: What happens next
|
- name: What happens next
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg:
|
msg:
|
||||||
- "Update launched detached. If the image changed, this job dies here."
|
- "Update starts in {{ semaphore_update_delay }}s, after this job ends."
|
||||||
- "That is expected — the log is the success signal, not the task status."
|
- "If the image changed, the container is replaced — expected."
|
||||||
- "tail -n 20 {{ semaphore_update_log }}"
|
- "The log is the success signal: tail -n 20 {{ semaphore_update_log }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user