From ac6cc1ab3a4b465122c9405f6543500a0de8152b Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Sun, 27 Sep 2026 10:11:47 +0200 Subject: [PATCH] Add a code deploy tag and restart on version mismatch `--tags code` builds, installs the binary, restarts and waits for the new version, skipping firewall, user, config, seeding and Prometheus tasks. A run that failed after installing the binary dropped the pending restart, and the next run saw an unchanged binary and never restarted, leaving the old process running. Compare the running build_info version with the one just built and restart when they differ, then poll until the running process reports the deployed version. Co-Authored-By: Claude Opus 5.5 --- ansible/playbook.yml | 43 +++++++++++++++++++++++++++++++++++-------- docs/deployment.md | 13 +++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/ansible/playbook.yml b/ansible/playbook.yml index cc360d8..cd12867 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -19,6 +19,9 @@ armv6l: arm sshgate_goarch: "{{ sshgate_goarch_map.get(ansible_facts['architecture'], ansible_facts['architecture']) }}" sshgate_build_output: "{{ playbook_dir }}/../sshgate-{{ sshgate_goos }}-{{ sshgate_goarch }}" + sshgate_deploy_version: "{{ sshgate_git_describe.stdout | default('dev', true) | trim }}" + sshgate_metrics_url: "http://{{ sshgate_prometheus_target | default('127.0.0.1:9108') }}/metrics" + sshgate_expected_build_info: 'sshgate_build_info{version="{{ sshgate_deploy_version }}"}' tasks: - name: Install firewalld @@ -52,23 +55,23 @@ changed_when: false register: sshgate_git_describe failed_when: false + tags: [code] - name: Build sshgate locally ansible.builtin.command: cmd: >- go build -trimpath - -ldflags "-s -w -X main.version={{ sshgate_version }}" + -ldflags "-s -w -X main.version={{ sshgate_deploy_version }}" -o {{ sshgate_build_output | quote }} . chdir: "{{ playbook_dir }}/.." environment: CGO_ENABLED: "0" GOOS: "{{ sshgate_goos }}" GOARCH: "{{ sshgate_goarch }}" - vars: - sshgate_version: "{{ sshgate_git_describe.stdout | default('dev', true) | trim }}" delegate_to: localhost become: false changed_when: true + tags: [code] - name: Create sshgate group ansible.builtin.group: @@ -108,6 +111,21 @@ group: root mode: "0755" notify: Restart sshgate + tags: [code] + + # An unchanged binary does not notify the restart handler, so a run that + # failed after installing it (dropping the pending handler) would leave the + # old process running forever. Restart whenever the running version differs. + - name: Check which sshgate version is running + ansible.builtin.uri: + url: "{{ sshgate_metrics_url }}" + return_content: true + register: sshgate_running_metrics + failed_when: false + changed_when: sshgate_expected_build_info not in (sshgate_running_metrics.content | default('')) + notify: Restart sshgate + when: sshgate_metrics_listen | default('127.0.0.1:9108') | length > 0 + tags: [code] - name: Check for existing sshgate config ansible.builtin.stat: @@ -167,14 +185,17 @@ - name: Apply service changes before first start ansible.builtin.meta: flush_handlers + tags: [code] - name: Reset connection after sshgate restart ansible.builtin.meta: reset_connection + tags: [code] - name: Wait for sshgate to accept connections again ansible.builtin.wait_for_connection: delay: 2 timeout: 30 + tags: [code] - name: Enable and start sshgate ansible.builtin.systemd: @@ -221,16 +242,22 @@ - sshgate_metrics_listen | default('127.0.0.1:9108') | length > 0 - sshgate_prometheus_config_file.stat.exists - - name: Verify the sshgate metrics endpoint + # The restart handler is fire-and-forget and a graceful reload hands off to + # the new binary asynchronously, so poll until the running process reports + # the version that was just built. + - name: Verify sshgate is running the deployed version ansible.builtin.uri: - url: "http://{{ sshgate_prometheus_target | default('127.0.0.1:9108') }}/metrics" + url: "{{ sshgate_metrics_url }}" return_content: true status_code: 200 register: sshgate_metrics_response - failed_when: >- - sshgate_metrics_response.status != 200 or - 'sshgate_build_info' not in sshgate_metrics_response.content + until: >- + sshgate_metrics_response.status == 200 and + sshgate_expected_build_info in sshgate_metrics_response.content + retries: 15 + delay: 2 when: sshgate_metrics_listen | default('127.0.0.1:9108') | length > 0 + tags: [code] handlers: - name: Reload systemd diff --git a/docs/deployment.md b/docs/deployment.md index 9441a7b..01426b0 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -41,6 +41,19 @@ ansible-playbook --syntax-check playbook.yml ansible-playbook playbook.yml ``` +For a code-only upgrade, `--tags code` builds the checkout, installs the binary, +restarts the service (gracefully when it is already running), and waits until +the metrics endpoint reports the version that was just built. It skips +firewall, user, config, fingerprint seeding, and Prometheus tasks: + +```bash +ansible-playbook playbook.yml --tags code +``` + +Every run restarts `sshgate` when the running version differs from the build, so +a run that failed after installing the binary is completed by the next run even +though the binary on disk is already current. + The real inventory and group variables files are ignored so deployment-specific host names, fingerprints, and settings are not committed accidentally. Override variables in `group_vars/sshgate.yml`, the inventory, or with `-e` as needed: