diff --git a/.DS_Store b/.DS_Store index 2c90403..98c09f2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/docker-compose/docker-compose-bettershift.yml b/docker-compose/docker-compose-bettershift.yml index 78fad28..e99eea0 100644 --- a/docker-compose/docker-compose-bettershift.yml +++ b/docker-compose/docker-compose-bettershift.yml @@ -1,9 +1,11 @@ +# docker-compose/docker-compose-bettershift.yml + name: bettershift services: bettershift: container_name: bettershift - image: ghcr.io/pantelx/bettershift:v2.2.1 + image: ghcr.io/pantelx/bettershift:${BETTERSHIFT_VERSION:-v2.2.1} restart: unless-stopped ports: diff --git a/update_bettershift.yml b/update_bettershift.yml index 04eaf42..8813002 100644 --- a/update_bettershift.yml +++ b/update_bettershift.yml @@ -1,85 +1,130 @@ # update_bettershift.yml +--- - name: Update BetterShift hosts: pve2_vm gather_facts: false + become: true vars: compose_local_dir: "{{ playbook_dir }}/docker-compose" compose_remote_base: "/home/{{ ansible_user }}/.ansible-compose/docker-compose" - bettershift_project: bettershift - bettershift_compose_filename: "docker-compose-bettershift.yml" + bettershift_compose_filename: docker-compose-bettershift.yml bettershift_service: bettershift bettershift_port: 3067 - bettershift_data_dir: "/data/compose/bettershift/data" + bettershift_data_dir: /data/compose/bettershift/data + bettershift_version: v3.3.0 + + environment: + BETTERSHIFT_VERSION: "{{ bettershift_version }}" tasks: - - name: Ensure remote compose directory exists - ansible.builtin.file: - path: "{{ compose_remote_base }}" - state: directory - mode: "0755" + - name: Check existing database and env file + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ bettershift_data_dir }}/sqlite.db" + - "{{ compose_remote_base }}/.env.bettershift" + register: bettershift_files - - name: Ensure BetterShift data directory exists - ansible.builtin.file: - path: "{{ bettershift_data_dir }}" - state: directory - mode: "0755" + - name: Refuse missing or empty files + ansible.builtin.assert: + that: + - item.stat.isreg | default(false) + - item.stat.size | default(0) > 0 + fail_msg: "Missing or empty file: {{ item.item }}" + loop: "{{ bettershift_files.results }}" + loop_control: + label: "{{ item.item }}" - - name: Upload BetterShift compose file to remote host + - name: Check database before changing deployment + ansible.builtin.command: + argv: + - python3 + - -c + - | + import pathlib, sqlite3, sys + path = pathlib.Path(sys.argv[1]) + with sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) as src: + with sqlite3.connect(':memory:') as db: + src.backup(db) + integrity = db.execute('PRAGMA integrity_check').fetchall() + errors = db.execute('PRAGMA foreign_key_check').fetchall() + print('Integrity:', integrity, 'Foreign key violations:', errors) + sys.exit(0 if integrity == [('ok',)] and not errors else 1) + - "{{ bettershift_data_dir }}/sqlite.db" + changed_when: false + + - name: Upload BetterShift compose file ansible.builtin.copy: src: "{{ compose_local_dir }}/{{ bettershift_compose_filename }}" dest: "{{ compose_remote_base }}/{{ bettershift_compose_filename }}" - mode: "0644" + mode: '0644' + backup: true - - name: Check BetterShift env file exists on remote host - ansible.builtin.stat: - path: "{{ compose_remote_base }}/.env.bettershift" - register: bettershift_env_file + - name: Pull image without starting it + community.docker.docker_compose_v2_pull: + project_name: "{{ bettershift_project }}" + project_src: "{{ compose_remote_base }}" + files: ["{{ bettershift_compose_filename }}"] - - name: Fail if BetterShift env file is missing - ansible.builtin.fail: - msg: >- - Missing {{ compose_remote_base }}/.env.bettershift on remote host. - Create it manually because it contains secrets. - when: not bettershift_env_file.stat.exists + - name: Create private backup directory + ansible.builtin.tempfile: + state: directory + path: /data/compose/bettershift + prefix: backup-before-upgrade- + register: bettershift_backup - - name: Pull latest BetterShift image + - name: Show backup location + ansible.builtin.debug: + msg: "Backup: {{ bettershift_backup.path }}" + + - name: Stop service and create a verified backup + block: + - name: Stop BetterShift + ansible.builtin.command: + argv: [docker, stop, --timeout, '60', bettershift] + changed_when: true + + - name: Copy data and compare files + ansible.builtin.shell: | + set -eu + test "$(docker inspect --format '{% raw %}{{.State.Running}}{% endraw %}' bettershift)" = false + cp -a -- "$DATA_DIR" "$BACKUP_DIR/data" + diff -qr -- "$DATA_DIR" "$BACKUP_DIR/data" + environment: + DATA_DIR: "{{ bettershift_data_dir }}" + BACKUP_DIR: "{{ bettershift_backup.path }}" + changed_when: true + + rescue: + - name: Restart original container if backup fails + ansible.builtin.command: + argv: [docker, start, bettershift] + changed_when: true + + - name: Abort update + ansible.builtin.fail: + msg: Backup failed. Original container restarted; update aborted. + + - name: Recreate BetterShift once community.docker.docker_compose_v2: project_name: "{{ bettershift_project }}" project_src: "{{ compose_remote_base }}" - files: - - "{{ bettershift_compose_filename }}" - pull: always - - - name: Recreate BetterShift service - community.docker.docker_compose_v2: - project_name: "{{ bettershift_project }}" - project_src: "{{ compose_remote_base }}" - files: - - "{{ bettershift_compose_filename }}" - services: - - "{{ bettershift_service }}" + files: ["{{ bettershift_compose_filename }}"] + services: ["{{ bettershift_service }}"] state: present + pull: never recreate: always - - name: Wait for BetterShift port - ansible.builtin.wait_for: - host: 127.0.0.1 - port: "{{ bettershift_port }}" - timeout: 60 - - name: Check BetterShift HTTP endpoint ansible.builtin.uri: url: "http://127.0.0.1:{{ bettershift_port }}/" - status_code: - - 200 - - 307 - - 308 + status_code: [200, 307, 308] follow_redirects: none register: bettershift_http retries: 30 delay: 3 - until: bettershift_http.status in [200, 307, 308] + until: bettershift_http.status | default(0) in [200, 307, 308] changed_when: false