diff --git a/README.md b/README.md index 8059fbe..5ed17c1 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,49 @@ # Ansible Role: Seriatim -Installs the [Seriatim](https://gitea.maximumdirect.net/eric/seriatim) -transcript processor from source. The role supports Ubuntu 26.04 on `amd64` -and `arm64` hosts and is independently usable outside the D&D pipeline role. - -The role manages its build dependencies and a root-owned source checkout, -builds a native static executable, validates it, and safely installs it. - -Role source and issue tracking are hosted in the -[ansible-role-seriatim repository](https://gitea.maximumdirect.net/eric/ansible-role-seriatim) -and its [issue tracker](https://gitea.maximumdirect.net/eric/ansible-role-seriatim/issues). - -## Requirements - -Ansible Core 2.18 or newer is required. Install the pinned collection -dependency before using or validating the role: - -```sh -ansible-galaxy collection install -r collections/requirements.yml -``` - -The consuming play must set `become: true` or provide equivalent root -privileges. The role manages system packages, classic snaps, root-owned source -and state directories, and the installed executable under `/usr/local` by -default. +Installs [Seriatim](https://gitea.maximumdirect.net/eric/seriatim) from source +on Ubuntu 26.04 (`x86_64` or `aarch64`). The role requires gathered facts and +privilege escalation for its default paths and dependency installation. ## Variables -| Variable | Default | Description | -| --- | --- | --- | -| `seriatim_version` | `v1.5.0` | Release identity expected from the installed binary. | -| `seriatim_repository_url` | `https://gitea.maximumdirect.net/eric/seriatim.git` | Source repository URL. | -| `seriatim_source_ref` | `{{ seriatim_version }}` | Git tag, branch, or commit to check out. | -| `seriatim_build_version` | `{{ seriatim_version }}` | Version injected into build metadata. | -| `seriatim_source_dir` | `/usr/local/src/seriatim` | Fully managed source checkout. | -| `seriatim_binary_path` | `/usr/local/bin/seriatim` | Installed executable path. | -| `seriatim_build_state_dir` | `/usr/local/src/seriatim.ansible-build-state` | Sidecar directory for build inputs and output state. | -| `seriatim_manage_build_dependencies` | `true` | Install Git and Go when enabled. | -| `seriatim_go_snap_channel` | `1.26/stable` | Go Snap channel. | -| `seriatim_go_binary` | `/snap/bin/go` | Go executable used to inspect and build. | -| `seriatim_go_build_flags` | `[-trimpath]` | Ordered Go build arguments before `-ldflags`. | -| `seriatim_go_ldflags` | `-s -w -X gitea.maximumdirect.net/eric/seriatim/internal/buildinfo.Version={{ seriatim_build_version }}` | Linker flags passed as one argument. | -| `seriatim_go_package` | `./cmd/seriatim` | Go package built from the checkout. | +```yaml +seriatim_version: v1.5.0 +seriatim_repository_url: https://gitea.maximumdirect.net/eric/seriatim.git +seriatim_source_dir: /usr/local/src/seriatim +seriatim_binary_path: /usr/local/bin/seriatim +seriatim_manage_build_dependencies: true +seriatim_go_snap_channel: 1.26/stable +``` -For development revisions, set both `seriatim_source_ref` and an honest -`seriatim_build_version`. Released deployments should select immutable tags. +When `seriatim_manage_build_dependencies` is `false`, Git and `/snap/bin/go` +must already be available. Otherwise, the role installs Git with APT and the +classic Go Snap from `seriatim_go_snap_channel`. -When `seriatim_manage_build_dependencies` is enabled, the role installs Git -and converges the classic Go Snap to `seriatim_go_snap_channel`. When disabled, -both Git and the configured Go executable must already be available, and Go -must be version 1.25 or newer. +The checkout is authoritative: local source changes are discarded while +selecting `seriatim_version`. The role builds `./cmd/seriatim` directly to +`seriatim_binary_path` using `/snap/bin/go`, `CGO_ENABLED=0`, `GOWORK=off`, +`GOTOOLCHAIN=local`, `-trimpath`, and release linker metadata. It rebuilds when +the checkout changes, the binary is absent, or `seriatim --version` does not +exactly match the selected version. -The checkout is authoritative: tracked changes and all untracked or ignored -files beneath `seriatim_source_dir` are discarded during convergence. The -repository URL may point to a private remote, but authentication must be -provided independently through the managed host's Git configuration or SSH -environment; this role does not manage credentials or accept unknown host -keys. +The source tree is recursively owned by `root:root`. The installed binary is a +regular file owned by `root:root` with mode `0755`. -## Build and installation behavior - -Builds run from the selected checkout with `GOWORK=off`, -`GOTOOLCHAIN=local`, `CGO_ENABLED=0`, the configured ordered build flags, and -the upstream version-injection linker flags. The resulting executable is -staged and must report the exact configured version before it atomically -replaces `seriatim_binary_path`. It is installed as `root:root` mode `0755`. - -The role records readable inputs, their SHA-256 fingerprint, and the installed -binary checksum beneath `seriatim_build_state_dir`. A source commit, compiler -identity, Go path, version, flags, linker flags, package, or fixed environment -change triggers a rebuild. Missing or corrupt state, a modified binary, or an -incorrect installed version also triggers rebuilding. Failed compilation or -staged validation leaves the previous installed binary and successful state -untouched. - -For example, development from a branch must identify both source and output: +## Example ```yaml -seriatim_source_ref: feature/example -seriatim_build_version: v1.5.0-dev +- hosts: seriatim_hosts + become: true + roles: + - role: ansible-role-seriatim ``` -## Static validation +## Validation -```sh -ANSIBLE_COLLECTIONS_PATH="$PWD/.ansible/collections" \ - ../.venv/bin/ansible-playbook -i tests/inventory tests/converge.yml \ - --syntax-check -ANSIBLE_COLLECTIONS_PATH="$PWD/.ansible/collections" \ - ../.venv/bin/ansible-lint . -``` - -## Development and integration validation - -Create the workspace virtual environment described by the control repository -and run the complete local suite from this repository with: +The integration driver installs pinned collection dependencies, runs syntax +checking and ansible-lint, converges and verifies the default release, then +requires an immediate repeat with `changed=0` and `failed=0`: ```sh tests/integration ``` - -The driver installs the pinned collection into the ignored local `.ansible` -directory, runs static checks, and then mutates the local host using privilege -escalation. It manages `/usr/local/src/seriatim`, -`/usr/local/src/seriatim.ansible-build-state`, and -`/usr/local/bin/seriatim`; do not run it where those paths contain data that -must be preserved. It verifies convergence, idempotence, platform guards, -authoritative source cleanup, compiler-version invalidation, and preservation -of a working installation after a failed build. - -The driver uses temporary Go wrappers and removes them on exit. It attempts to -restore the default role state after a failure. Recovery consists only of -rerunning `tests/integration` or the default `tests/converge.yml`; no manual -deletion of managed paths is required. - -## License - -BSD-3-Clause diff --git a/defaults/main.yml b/defaults/main.yml index dc4b197..770f20b 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,16 +1,7 @@ --- seriatim_version: v1.5.0 seriatim_repository_url: https://gitea.maximumdirect.net/eric/seriatim.git -seriatim_source_ref: "{{ seriatim_version }}" -seriatim_build_version: "{{ seriatim_version }}" seriatim_source_dir: /usr/local/src/seriatim seriatim_binary_path: /usr/local/bin/seriatim -seriatim_build_state_dir: /usr/local/src/seriatim.ansible-build-state seriatim_manage_build_dependencies: true seriatim_go_snap_channel: 1.26/stable -seriatim_go_binary: /snap/bin/go -seriatim_go_build_flags: - - -trimpath -seriatim_go_ldflags: >- - -s -w -X gitea.maximumdirect.net/eric/seriatim/internal/buildinfo.Version={{ seriatim_build_version }} -seriatim_go_package: ./cmd/seriatim diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml index 85521a8..0ca9d48 100644 --- a/meta/argument_specs.yml +++ b/meta/argument_specs.yml @@ -6,19 +6,11 @@ argument_specs: seriatim_version: type: str required: true - description: Release identity expected from the installed binary. + description: Git revision and version injected into the executable. seriatim_repository_url: type: str required: true description: Git repository containing Seriatim source. - seriatim_source_ref: - type: str - required: true - description: Git tag, branch, or commit to check out. - seriatim_build_version: - type: str - required: true - description: Version value injected into build metadata. seriatim_source_dir: type: path required: true @@ -27,10 +19,6 @@ argument_specs: type: path required: true description: Installed executable path. - seriatim_build_state_dir: - type: path - required: true - description: Sidecar directory holding successful build state. seriatim_manage_build_dependencies: type: bool required: true @@ -39,20 +27,3 @@ argument_specs: type: str required: true description: Go Snap channel to install. - seriatim_go_binary: - type: path - required: true - description: Go executable used for discovery and builds. - seriatim_go_build_flags: - type: list - elements: str - required: true - description: Ordered arguments passed to go build before ldflags. - seriatim_go_ldflags: - type: str - required: true - description: Linker flags passed to go build as one argument. - seriatim_go_package: - type: str - required: true - description: Go package built from the source checkout. diff --git a/tasks/build.yml b/tasks/build.yml deleted file mode 100644 index d29d498..0000000 --- a/tasks/build.yml +++ /dev/null @@ -1,316 +0,0 @@ ---- -- name: Construct canonical Seriatim build inputs - ansible.builtin.set_fact: - seriatim_build_inputs: >- - {{ { - 'source_commit': seriatim_source_commit, - 'go_version': seriatim_go_version_result.stdout, - 'go_binary': seriatim_go_binary, - 'build_version': seriatim_build_version, - 'build_flags': seriatim_go_build_flags, - 'ldflags': seriatim_go_ldflags, - 'package': seriatim_go_package, - 'environment': { - 'GOWORK': 'off', - 'GOTOOLCHAIN': 'local', - 'CGO_ENABLED': '0' - } - } | to_nice_json(indent=2, sort_keys=true) }} - -- name: Calculate desired Seriatim build fingerprint - ansible.builtin.set_fact: - seriatim_desired_fingerprint: "{{ seriatim_build_inputs | hash('sha256') }}" - seriatim_stored_build_inputs: "" - seriatim_stored_fingerprint: "" - seriatim_stored_checksum: "" - -- name: Inspect Seriatim binary parent directory - ansible.builtin.stat: - path: "{{ seriatim_binary_path | dirname }}" - register: seriatim_binary_parent_stat - -- name: Require an existing Seriatim binary parent to be a directory - ansible.builtin.assert: - that: - - not seriatim_binary_parent_stat.stat.exists or seriatim_binary_parent_stat.stat.isdir - fail_msg: >- - Seriatim binary parent exists but is not a directory: - {{ seriatim_binary_path | dirname }}. - -- name: Create missing Seriatim binary parent directory - ansible.builtin.file: - path: "{{ seriatim_binary_path | dirname }}" - state: directory - owner: root - group: root - mode: "0755" - when: not seriatim_binary_parent_stat.stat.exists - -- name: Inspect installed Seriatim binary - ansible.builtin.stat: - path: "{{ seriatim_binary_path }}" - checksum_algorithm: sha256 - register: seriatim_installed_binary_stat - -- name: Require a regular installed Seriatim binary path - ansible.builtin.assert: - that: - - not seriatim_installed_binary_stat.stat.exists or seriatim_installed_binary_stat.stat.isreg - fail_msg: >- - Seriatim binary path exists but is not a regular file: - {{ seriatim_binary_path }}. Move the unexpected object before rerunning the role. - -- name: Inspect Seriatim build state directory - ansible.builtin.stat: - path: "{{ seriatim_build_state_dir }}" - register: seriatim_build_state_dir_stat - -- name: Remove malformed Seriatim build state directory object - ansible.builtin.file: - path: "{{ seriatim_build_state_dir }}" - state: absent - when: - - seriatim_build_state_dir_stat.stat.exists or - (seriatim_build_state_dir_stat.stat.islnk | default(false)) - - not (seriatim_build_state_dir_stat.stat.isdir | default(false)) - -- name: Create missing Seriatim build state directory - ansible.builtin.file: - path: "{{ seriatim_build_state_dir }}" - state: directory - owner: root - group: root - mode: "0755" - when: not (seriatim_build_state_dir_stat.stat.isdir | default(false)) - -- name: Inspect Seriatim build input state - ansible.builtin.stat: - path: "{{ seriatim_build_state_dir }}/build-inputs.json" - register: seriatim_build_inputs_state_stat - -- name: Inspect Seriatim fingerprint state - ansible.builtin.stat: - path: "{{ seriatim_build_state_dir }}/fingerprint.sha256" - register: seriatim_fingerprint_state_stat - -- name: Inspect Seriatim checksum state - ansible.builtin.stat: - path: "{{ seriatim_build_state_dir }}/binary.sha256" - register: seriatim_checksum_state_stat - -- name: Remove malformed Seriatim build state objects - ansible.builtin.file: - path: "{{ item.path }}" - state: absent - loop: - - path: "{{ seriatim_build_state_dir }}/build-inputs.json" - exists: "{{ seriatim_build_inputs_state_stat.stat.exists }}" - is_regular: "{{ seriatim_build_inputs_state_stat.stat.isreg | default(false) }}" - - path: "{{ seriatim_build_state_dir }}/fingerprint.sha256" - exists: "{{ seriatim_fingerprint_state_stat.stat.exists }}" - is_regular: "{{ seriatim_fingerprint_state_stat.stat.isreg | default(false) }}" - - path: "{{ seriatim_build_state_dir }}/binary.sha256" - exists: "{{ seriatim_checksum_state_stat.stat.exists }}" - is_regular: "{{ seriatim_checksum_state_stat.stat.isreg | default(false) }}" - when: - - item.exists | bool - - not item.is_regular | bool - -- name: Read stored Seriatim fingerprint - ansible.builtin.slurp: - src: "{{ seriatim_build_state_dir }}/fingerprint.sha256" - register: seriatim_stored_fingerprint_result - failed_when: false - when: - - seriatim_fingerprint_state_stat.stat.isreg | default(false) - - seriatim_fingerprint_state_stat.stat.readable | default(false) - -- name: Read stored Seriatim build inputs - ansible.builtin.slurp: - src: "{{ seriatim_build_state_dir }}/build-inputs.json" - register: seriatim_stored_build_inputs_result - failed_when: false - when: - - seriatim_build_inputs_state_stat.stat.isreg | default(false) - - seriatim_build_inputs_state_stat.stat.readable | default(false) - -- name: Read stored Seriatim checksum - ansible.builtin.slurp: - src: "{{ seriatim_build_state_dir }}/binary.sha256" - register: seriatim_stored_checksum_result - failed_when: false - when: - - seriatim_checksum_state_stat.stat.isreg | default(false) - - seriatim_checksum_state_stat.stat.readable | default(false) - -- name: Record stored Seriatim state values - ansible.builtin.set_fact: - seriatim_stored_build_inputs: >- - {{ seriatim_stored_build_inputs_result.content | b64decode | trim }} - seriatim_stored_fingerprint: >- - {{ seriatim_stored_fingerprint_result.content | b64decode | trim }} - seriatim_stored_checksum: >- - {{ seriatim_stored_checksum_result.content | b64decode | trim }} - when: - - seriatim_build_inputs_state_stat.stat.isreg | default(false) - - seriatim_build_inputs_state_stat.stat.readable | default(false) - - seriatim_fingerprint_state_stat.stat.isreg | default(false) - - seriatim_fingerprint_state_stat.stat.readable | default(false) - - seriatim_checksum_state_stat.stat.isreg | default(false) - - seriatim_checksum_state_stat.stat.readable | default(false) - - not (seriatim_stored_build_inputs_result.failed | default(true)) - - not (seriatim_stored_fingerprint_result.failed | default(true)) - - not (seriatim_stored_checksum_result.failed | default(true)) - -- name: Read installed Seriatim version - ansible.builtin.command: - argv: - - "{{ seriatim_binary_path }}" - - --version - register: seriatim_installed_version_result - changed_when: false - failed_when: false - when: seriatim_installed_binary_stat.stat.exists - -- name: Decide whether Seriatim requires rebuilding - ansible.builtin.set_fact: - seriatim_build_required: >- - {{ not seriatim_installed_binary_stat.stat.exists - or not seriatim_build_inputs_state_stat.stat.exists - or not seriatim_fingerprint_state_stat.stat.exists - or not seriatim_checksum_state_stat.stat.exists - or seriatim_stored_build_inputs != seriatim_build_inputs - or (seriatim_stored_build_inputs | hash('sha256')) != seriatim_stored_fingerprint - or (seriatim_stored_build_inputs | hash('sha256')) != seriatim_desired_fingerprint - or seriatim_stored_fingerprint != seriatim_desired_fingerprint - or seriatim_stored_checksum != (seriatim_installed_binary_stat.stat.checksum | default('')) - or (seriatim_installed_version_result.rc | default(1)) != 0 - or (seriatim_installed_version_result.stdout | default('')) != 'seriatim version ' ~ seriatim_build_version }} - -- name: Build, validate, and install Seriatim - when: seriatim_build_required - block: - - name: Create temporary Seriatim build directory - ansible.builtin.tempfile: - state: directory - prefix: ansible-seriatim-build- - register: seriatim_build_tempdir - - - name: Build staged Seriatim binary - ansible.builtin.command: - argv: >- - {{ [seriatim_go_binary, 'build'] - + seriatim_go_build_flags - + ['-ldflags', seriatim_go_ldflags, - '-o', seriatim_build_tempdir.path ~ '/seriatim', - seriatim_go_package] }} - chdir: "{{ seriatim_source_dir }}" - environment: - GOWORK: "off" - GOTOOLCHAIN: local - CGO_ENABLED: "0" - changed_when: true - - - name: Validate staged Seriatim binary - ansible.builtin.command: - argv: - - "{{ seriatim_build_tempdir.path }}/seriatim" - - --version - register: seriatim_staged_version_result - changed_when: false - failed_when: >- - seriatim_staged_version_result.rc != 0 or - seriatim_staged_version_result.stdout != 'seriatim version ' ~ seriatim_build_version - - - name: Install validated Seriatim binary - ansible.builtin.copy: - src: "{{ seriatim_build_tempdir.path }}/seriatim" - dest: "{{ seriatim_binary_path }}" - remote_src: true - owner: root - group: root - mode: "0755" - - - name: Validate installed Seriatim binary - ansible.builtin.command: - argv: - - "{{ seriatim_binary_path }}" - - --version - register: seriatim_new_installed_version_result - changed_when: false - failed_when: >- - seriatim_new_installed_version_result.rc != 0 or - seriatim_new_installed_version_result.stdout != 'seriatim version ' ~ seriatim_build_version - - - name: Calculate installed Seriatim checksum - ansible.builtin.stat: - path: "{{ seriatim_binary_path }}" - checksum_algorithm: sha256 - register: seriatim_new_binary_stat - - - name: Create Seriatim build state directory - ansible.builtin.file: - path: "{{ seriatim_build_state_dir }}" - state: directory - owner: root - group: root - mode: "0755" - - - name: Record Seriatim build inputs - ansible.builtin.copy: - content: "{{ seriatim_build_inputs }}\n" - dest: "{{ seriatim_build_state_dir }}/build-inputs.json" - owner: root - group: root - mode: "0644" - - - name: Record Seriatim build fingerprint - ansible.builtin.copy: - content: "{{ seriatim_desired_fingerprint }}\n" - dest: "{{ seriatim_build_state_dir }}/fingerprint.sha256" - owner: root - group: root - mode: "0644" - - - name: Record installed Seriatim checksum - ansible.builtin.copy: - content: "{{ seriatim_new_binary_stat.stat.checksum }}\n" - dest: "{{ seriatim_build_state_dir }}/binary.sha256" - owner: root - group: root - mode: "0644" - - always: - - name: Remove temporary Seriatim build directory - ansible.builtin.file: - path: "{{ seriatim_build_tempdir.path }}" - state: absent - when: seriatim_build_tempdir.path is defined - -- name: Converge installed Seriatim binary metadata - ansible.builtin.file: - path: "{{ seriatim_binary_path }}" - state: file - owner: root - group: root - mode: "0755" - -- name: Converge Seriatim build state directory metadata - ansible.builtin.file: - path: "{{ seriatim_build_state_dir }}" - state: directory - owner: root - group: root - mode: "0755" - -- name: Converge Seriatim build state file metadata - ansible.builtin.file: - path: "{{ seriatim_build_state_dir }}/{{ item }}" - state: file - owner: root - group: root - mode: "0644" - loop: - - build-inputs.json - - fingerprint.sha256 - - binary.sha256 diff --git a/tasks/dependencies.yml b/tasks/dependencies.yml deleted file mode 100644 index 00627d8..0000000 --- a/tasks/dependencies.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -- name: Install Git build dependency - ansible.builtin.apt: - name: git - state: present - update_cache: true - cache_valid_time: 3600 - when: seriatim_manage_build_dependencies - -- name: Install Go build dependency - community.general.snap: - name: go - state: present - classic: true - channel: "{{ seriatim_go_snap_channel }}" - when: seriatim_manage_build_dependencies - -- name: Resolve Git executable - ansible.builtin.command: - argv: - - /usr/bin/which - - git - register: seriatim_git_path_result - changed_when: false - failed_when: seriatim_git_path_result.rc != 0 - -- name: Verify configured Go executable exists - ansible.builtin.stat: - path: "{{ seriatim_go_binary }}" - follow: true - register: seriatim_go_binary_stat - -- name: Require configured Go executable - ansible.builtin.assert: - that: - - seriatim_go_binary_stat.stat.exists - - seriatim_go_binary_stat.stat.isreg - - seriatim_go_binary_stat.stat.executable - fail_msg: "Configured Go executable is unavailable: {{ seriatim_go_binary }}" - -- name: Read Go compiler version - ansible.builtin.command: - argv: - - "{{ seriatim_go_binary }}" - - version - register: seriatim_go_version_result - changed_when: false - -- name: Extract Go compiler version components - ansible.builtin.set_fact: - seriatim_go_version_parts: >- - {{ seriatim_go_version_result.stdout - | regex_findall('go([0-9]+)\.([0-9]+)') - | first - | default([]) }} - -- name: Require supported Go compiler version - ansible.builtin.assert: - that: - - seriatim_go_version_result.stdout is regex('^go version go[0-9]+\.[0-9]+(?:\.[0-9]+)? ') - - seriatim_go_version_parts | length == 2 - - (seriatim_go_version_parts[0] | int > 1) or - (seriatim_go_version_parts[0] | int == 1 and seriatim_go_version_parts[1] | int >= 25) - fail_msg: >- - Seriatim requires Go 1.25 or newer; detected - {{ seriatim_go_version_result.stdout | default('no usable version output') }}. diff --git a/tasks/main.yml b/tasks/main.yml index fe3f3be..680cc0d 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,12 +1,109 @@ --- -- name: Validate Seriatim role inputs - ansible.builtin.import_tasks: validate.yml +- name: Validate the supported platform + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Ubuntu" + - ansible_facts.distribution_version == "26.04" + - ansible_facts.architecture in ["x86_64", "aarch64"] + fail_msg: Seriatim supports Ubuntu 26.04 on x86_64 or aarch64. -- name: Manage Seriatim build dependencies - ansible.builtin.import_tasks: dependencies.yml +- name: Install Git + ansible.builtin.apt: + name: git + state: present + update_cache: true + when: seriatim_manage_build_dependencies -- name: Manage Seriatim source checkout - ansible.builtin.import_tasks: source.yml +- name: Install Go + community.general.snap: + name: go + channel: "{{ seriatim_go_snap_channel }}" + classic: true + state: present + when: seriatim_manage_build_dependencies -- name: Build and install Seriatim - ansible.builtin.import_tasks: build.yml +- name: Create installation parent directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ seriatim_source_dir | dirname }}" + - "{{ seriatim_binary_path | dirname }}" + +- name: Check out Seriatim source + ansible.builtin.git: + repo: "{{ seriatim_repository_url }}" + dest: "{{ seriatim_source_dir }}" + version: "{{ seriatim_version }}" + force: true + register: seriatim_checkout + +- name: Inspect the installed binary + ansible.builtin.stat: + path: "{{ seriatim_binary_path }}" + register: seriatim_binary + +- name: Read the installed Seriatim version + ansible.builtin.command: + argv: + - "{{ seriatim_binary_path }}" + - --version + register: seriatim_installed_version + changed_when: false + failed_when: false + when: seriatim_binary.stat.exists + +- name: Build Seriatim + ansible.builtin.command: + argv: + - /snap/bin/go + - build + - -trimpath + - -ldflags + - "-s -w -X gitea.maximumdirect.net/eric/seriatim/internal/buildinfo.Version={{ seriatim_version }}" + - -o + - "{{ seriatim_binary_path }}" + - ./cmd/seriatim + chdir: "{{ seriatim_source_dir }}" + environment: + CGO_ENABLED: "0" + GOTOOLCHAIN: local + GOWORK: "off" + register: seriatim_build + changed_when: true + when: >- + seriatim_checkout.changed or + not seriatim_binary.stat.exists or + seriatim_installed_version.rc | default(1) != 0 or + seriatim_installed_version.stdout | default('') != 'seriatim version ' ~ seriatim_version + +- name: Converge installed metadata + ansible.builtin.file: + path: "{{ item.path }}" + state: "{{ item.state }}" + owner: root + group: root + mode: "{{ item.mode | default(omit) }}" + recurse: "{{ item.recurse | default(omit) }}" + loop: + - path: "{{ seriatim_source_dir }}" + state: directory + recurse: true + - path: "{{ seriatim_binary_path }}" + state: file + mode: "0755" + +- name: Validate the built Seriatim version # noqa no-handler + ansible.builtin.command: + argv: + - "{{ seriatim_binary_path }}" + - --version + register: seriatim_built_version + changed_when: false + failed_when: >- + seriatim_built_version.rc != 0 or + seriatim_built_version.stdout != 'seriatim version ' ~ seriatim_version + when: seriatim_build.changed diff --git a/tasks/source.yml b/tasks/source.yml deleted file mode 100644 index 0087dea..0000000 --- a/tasks/source.yml +++ /dev/null @@ -1,76 +0,0 @@ ---- -- name: Create Seriatim source parent directory - ansible.builtin.file: - path: "{{ seriatim_source_dir | dirname }}" - state: directory - owner: root - group: root - mode: "0755" - -- name: Check out selected Seriatim source - ansible.builtin.git: - repo: "{{ seriatim_repository_url }}" - dest: "{{ seriatim_source_dir }}" - version: "{{ seriatim_source_ref }}" - update: true - force: true - accept_hostkey: false - -- name: Inspect Seriatim checkout cleanliness - ansible.builtin.command: - argv: - - "{{ seriatim_git_path_result.stdout }}" - - -C - - "{{ seriatim_source_dir }}" - - status - - --porcelain - - --untracked-files=all - - --ignored - register: seriatim_source_status - changed_when: false - -- name: Reset tracked Seriatim source changes - ansible.builtin.command: - argv: - - "{{ seriatim_git_path_result.stdout }}" - - -C - - "{{ seriatim_source_dir }}" - - reset - - --hard - - HEAD - when: seriatim_source_status.stdout | length > 0 - changed_when: true - -- name: Remove untracked and ignored Seriatim source content - ansible.builtin.command: - argv: - - "{{ seriatim_git_path_result.stdout }}" - - -C - - "{{ seriatim_source_dir }}" - - clean - - -dffx - when: seriatim_source_status.stdout | length > 0 - changed_when: true - -- name: Enforce Seriatim source ownership - ansible.builtin.file: - path: "{{ seriatim_source_dir }}" - state: directory - owner: root - group: root - recurse: true - -- name: Resolve selected Seriatim source commit - ansible.builtin.command: - argv: - - "{{ seriatim_git_path_result.stdout }}" - - -C - - "{{ seriatim_source_dir }}" - - rev-parse - - HEAD - register: seriatim_source_commit_result - changed_when: false - -- name: Record selected Seriatim source commit - ansible.builtin.set_fact: - seriatim_source_commit: "{{ seriatim_source_commit_result.stdout }}" diff --git a/tasks/validate.yml b/tasks/validate.yml deleted file mode 100644 index 6d51333..0000000 --- a/tasks/validate.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -- name: Verify the managed host is supported - ansible.builtin.assert: - that: - - ansible_facts.distribution == 'Ubuntu' - - ansible_facts.distribution_version is version('26.04', '==') - - ansible_facts.architecture in ['x86_64', 'aarch64'] - fail_msg: >- - Seriatim supports Ubuntu 26.04 on x86_64 and aarch64; detected - {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }} - on {{ ansible_facts.architecture }}. - -- name: Verify required Seriatim values are nonempty - ansible.builtin.assert: - that: - - seriatim_version | length > 0 - - seriatim_repository_url | length > 0 - - seriatim_source_ref | length > 0 - - seriatim_build_version | length > 0 - - seriatim_go_snap_channel | length > 0 - - seriatim_go_binary | length > 0 - - seriatim_go_build_flags | length > 0 - - seriatim_go_build_flags | select('equalto', '') | list | length == 0 - - seriatim_go_ldflags | length > 0 - - seriatim_go_package | length > 0 - - seriatim_build_version is not regex('.*\s+.*') - fail_msg: Seriatim repository, revision, build, and Go values must be nonempty, and the build version cannot contain whitespace. - -- name: Verify managed Seriatim paths are safe and distinct - ansible.builtin.assert: - that: - - seriatim_source_dir is match('^/') - - seriatim_binary_path is match('^/') - - seriatim_build_state_dir is match('^/') - - seriatim_source_dir != seriatim_binary_path - - seriatim_source_dir != seriatim_build_state_dir - - seriatim_binary_path != seriatim_build_state_dir - fail_msg: Seriatim source, binary, and state paths must be absolute and pairwise distinct. diff --git a/tests/integration b/tests/integration index 8554e8f..7254bee 100755 --- a/tests/integration +++ b/tests/integration @@ -2,197 +2,26 @@ set -euo pipefail repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -workspace_venv=$(cd "$repository_root/../.venv" && pwd) +workspace_venv="$repository_root/../.venv" test_tmp=$(mktemp -d) +trap 'rm -rf -- "$test_tmp"' EXIT -export ANSIBLE_COLLECTIONS_PATH="$repository_root/.ansible/collections" +export ANSIBLE_COLLECTIONS_PATH="$test_tmp/collections" export ANSIBLE_ROLES_PATH="$(dirname "$repository_root")" -playbook="$workspace_venv/bin/ansible-playbook" inventory="$repository_root/tests/inventory" converge="$repository_root/tests/converge.yml" - -run_converge() { - "$playbook" -i "$inventory" "$converge" "$@" -} - -restore_default() { - run_converge >/dev/null 2>&1 -} - -cleanup() { - exit_status=$? - restore_default || true - sudo rm -rf -- "$test_tmp" - exit "$exit_status" -} -trap cleanup EXIT - -require_clean_recap() { - recap_file=$1 - grep -Eq 'changed=0 .*failed=0' "$recap_file" -} - -require_changed_recap() { - recap_file=$1 - grep -Eq 'changed=[1-9][0-9]* .*failed=0' "$recap_file" -} - -binary_digest() { - sha256sum /usr/local/bin/seriatim | awk '{print $1}' -} - -state_digest() { - sudo sha256sum /usr/local/src/seriatim.ansible-build-state/* | - sha256sum | awk '{print $1}' -} +verify="$repository_root/tests/verify.yml" cd "$repository_root" "$workspace_venv/bin/ansible-galaxy" collection install \ - -p .ansible/collections -r collections/requirements.yml -"$playbook" -i "$inventory" "$converge" --syntax-check -"$workspace_venv/bin/ansible-lint" . + -r collections/requirements.yml \ + -p "$ANSIBLE_COLLECTIONS_PATH" +"$workspace_venv/bin/ansible-playbook" -i "$inventory" --syntax-check "$converge" +"$workspace_venv/bin/ansible-lint" "$converge" "$verify" +"$workspace_venv/bin/ansible-playbook" -i "$inventory" "$converge" +"$workspace_venv/bin/ansible-playbook" -i "$inventory" "$verify" -run_converge -"$playbook" -i "$inventory" tests/verify.yml -run_converge | tee "$test_tmp/default-repeat.log" -require_clean_recap "$test_tmp/default-repeat.log" - -printf '%s\n' 'corrupt build inputs' | - sudo tee /usr/local/src/seriatim.ansible-build-state/build-inputs.json >/dev/null -run_converge | tee "$test_tmp/build-input-repair.log" -require_changed_recap "$test_tmp/build-input-repair.log" -"$playbook" -i "$inventory" tests/verify.yml -run_converge | tee "$test_tmp/build-input-repair-repeat.log" -require_clean_recap "$test_tmp/build-input-repair-repeat.log" - -sudo chown "$(id -u):$(id -g)" /usr/local/bin/seriatim -sudo chmod 0777 /usr/local/bin/seriatim -sudo chown "$(id -u):$(id -g)" /usr/local/src/seriatim.ansible-build-state -sudo chmod 0700 /usr/local/src/seriatim.ansible-build-state -sudo chown "$(id -u):$(id -g)" /usr/local/src/seriatim.ansible-build-state/* -sudo chmod 0600 /usr/local/src/seriatim.ansible-build-state/* -run_converge | tee "$test_tmp/metadata-repair.log" -require_changed_recap "$test_tmp/metadata-repair.log" -"$playbook" -i "$inventory" tests/verify.yml -run_converge | tee "$test_tmp/metadata-repair-repeat.log" -require_clean_recap "$test_tmp/metadata-repair-repeat.log" - -custom_binary="$test_tmp/missing-binary-parent/seriatim" -custom_state="$test_tmp/custom-build-state" -test ! -e "$(dirname "$custom_binary")" -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/custom-path.log" -require_changed_recap "$test_tmp/custom-path.log" -test "$($custom_binary --version)" = 'seriatim version v1.5.0' -test "$(stat -c '%U:%G %a' "$(dirname "$custom_binary")")" = 'root:root 755' -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/custom-path-repeat.log" -require_clean_recap "$test_tmp/custom-path-repeat.log" - -sudo rm -rf -- "$custom_state" -sudo install -o root -g root -m 0644 /dev/null "$custom_state" -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/state-file-repair.log" -require_changed_recap "$test_tmp/state-file-repair.log" -test -d "$custom_state" -test ! -L "$custom_state" -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/state-file-repair-repeat.log" -require_clean_recap "$test_tmp/state-file-repair-repeat.log" - -redirected_state="$test_tmp/redirected-build-state" -sudo rm -rf -- "$custom_state" -sudo install -d -o root -g root -m 0755 "$redirected_state" -sudo ln -s "$redirected_state" "$custom_state" -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/state-link-repair.log" -require_changed_recap "$test_tmp/state-link-repair.log" -test -d "$custom_state" -test ! -L "$custom_state" -test -z "$(find "$redirected_state" -mindepth 1 -maxdepth 1 -print -quit)" -run_converge \ - -e "seriatim_binary_path=$custom_binary" \ - -e "seriatim_build_state_dir=$custom_state" | - tee "$test_tmp/state-link-repair-repeat.log" -require_clean_recap "$test_tmp/state-link-repair-repeat.log" - -baseline_binary=$(binary_digest) -baseline_state=$(state_digest) -for scenario in \ - '{"test_distribution":"Debian","test_distribution_version":"26.04","test_architecture":"x86_64"}' \ - '{"test_distribution":"Ubuntu","test_distribution_version":"24.04","test_architecture":"x86_64"}' \ - '{"test_distribution":"Ubuntu","test_distribution_version":"26.04","test_architecture":"riscv64"}' -do - if "$playbook" -i "$inventory" tests/unsupported.yml -e "$scenario"; then - echo "unsupported platform scenario unexpectedly succeeded: $scenario" >&2 - exit 1 - fi - test "$(binary_digest)" = "$baseline_binary" - test "$(state_digest)" = "$baseline_state" -done - -sudo touch /usr/local/src/seriatim/ansible-untracked-test -sudo sed -i '1s/^/ansible-test-change\n/' /usr/local/src/seriatim/go.mod -run_converge -"$playbook" -i "$inventory" tests/verify.yml - -compiler_wrapper="$test_tmp/go-version-wrapper" -sed 's|@GO@|/snap/bin/go|' >"$compiler_wrapper" <<'WRAPPER' -#!/bin/sh -if [ "${1:-}" = version ]; then - echo 'go version go1.26.99 linux/amd64' - exit 0 -fi -exec @GO@ "$@" -WRAPPER -chmod 0755 "$compiler_wrapper" - -run_converge -e "seriatim_go_binary=$compiler_wrapper" | - tee "$test_tmp/compiler-change.log" -require_changed_recap "$test_tmp/compiler-change.log" -run_converge -e "seriatim_go_binary=$compiler_wrapper" | - tee "$test_tmp/compiler-repeat.log" -require_clean_recap "$test_tmp/compiler-repeat.log" -run_converge | tee "$test_tmp/compiler-restore.log" -require_changed_recap "$test_tmp/compiler-restore.log" -run_converge | tee "$test_tmp/compiler-restored-repeat.log" -require_clean_recap "$test_tmp/compiler-restored-repeat.log" - -failure_wrapper="$test_tmp/go-failure-wrapper" -sed 's|@GO@|/snap/bin/go|' >"$failure_wrapper" <<'WRAPPER' -#!/bin/sh -if [ "${1:-}" = version ]; then - exec @GO@ version -fi -echo 'deliberate integration-test build failure' >&2 -exit 42 -WRAPPER -chmod 0755 "$failure_wrapper" - -baseline_binary=$(binary_digest) -baseline_state=$(state_digest) -if run_converge -e "seriatim_go_binary=$failure_wrapper"; then - echo 'deliberate build failure unexpectedly succeeded' >&2 - exit 1 -fi -test "$(binary_digest)" = "$baseline_binary" -test "$(state_digest)" = "$baseline_state" -test "$(/usr/local/bin/seriatim --version)" = 'seriatim version v1.5.0' - -run_converge | tee "$test_tmp/final-restore.log" -require_clean_recap "$test_tmp/final-restore.log" -"$playbook" -i "$inventory" tests/verify.yml - -trap - EXIT -sudo rm -rf -- "$test_tmp" +repeat_output="$test_tmp/repeat.log" +"$workspace_venv/bin/ansible-playbook" -i "$inventory" "$converge" | tee "$repeat_output" +grep -Eq 'changed=0 .*failed=0' "$repeat_output" diff --git a/tests/verify.yml b/tests/verify.yml index d17ca6f..defaccf 100644 --- a/tests/verify.yml +++ b/tests/verify.yml @@ -1,111 +1,73 @@ --- -- name: Verify Seriatim installation - hosts: seriatim - gather_facts: true +- name: Verify the Seriatim installation + hosts: localhost + connection: local + gather_facts: false become: true vars: seriatim_expected_commit: 0fc92f364316c874d366b00cdc884a33e1900b25 seriatim_expected_version: v1.5.0 - seriatim_verify_source_dir: /usr/local/src/seriatim - seriatim_verify_binary_path: /usr/local/bin/seriatim - seriatim_verify_state_dir: /usr/local/src/seriatim.ansible-build-state + seriatim_source_dir: /usr/local/src/seriatim + seriatim_binary_path: /usr/local/bin/seriatim tasks: - - name: Inspect Seriatim source directory + - name: Inspect the source directory ansible.builtin.stat: - path: "{{ seriatim_verify_source_dir }}" - register: seriatim_verify_source_stat + path: "{{ seriatim_source_dir }}" + register: seriatim_source - - name: Inspect installed Seriatim binary - ansible.builtin.stat: - path: "{{ seriatim_verify_binary_path }}" - checksum_algorithm: sha256 - register: seriatim_verify_binary_stat - - - name: Inspect Seriatim build state directory - ansible.builtin.stat: - path: "{{ seriatim_verify_state_dir }}" - register: seriatim_verify_state_stat - - - name: Inspect Seriatim build state files - ansible.builtin.stat: - path: "{{ seriatim_verify_state_dir }}/{{ item }}" - loop: - - build-inputs.json - - fingerprint.sha256 - - binary.sha256 - register: seriatim_verify_state_file_stats - - - name: Resolve installed Seriatim source commit + - name: Find incorrectly owned source entries ansible.builtin.command: argv: - - /usr/bin/git + - find + - "{{ seriatim_source_dir }}" + - ( + - -not + - -user + - root + - -or + - -not + - -group + - root + - ) + - -print + - -quit + register: seriatim_source_ownership + changed_when: false + + - name: Read the selected source commit + ansible.builtin.command: + argv: + - git - -C - - "{{ seriatim_verify_source_dir }}" + - "{{ seriatim_source_dir }}" - rev-parse - HEAD - register: seriatim_verify_commit + register: seriatim_source_commit changed_when: false - - name: Inspect installed Seriatim source cleanliness - ansible.builtin.command: - argv: - - /usr/bin/git - - -C - - "{{ seriatim_verify_source_dir }}" - - status - - --porcelain - - --untracked-files=all - - --ignored - register: seriatim_verify_source_status - changed_when: false + - name: Inspect the installed binary + ansible.builtin.stat: + path: "{{ seriatim_binary_path }}" + register: seriatim_binary - - name: Read installed Seriatim version + - name: Read the installed version ansible.builtin.command: argv: - - "{{ seriatim_verify_binary_path }}" + - "{{ seriatim_binary_path }}" - --version - register: seriatim_verify_version + register: seriatim_version_output changed_when: false - - name: Read stored Seriatim build inputs - ansible.builtin.slurp: - src: "{{ seriatim_verify_state_dir }}/build-inputs.json" - register: seriatim_verify_inputs - - - name: Read stored Seriatim fingerprint - ansible.builtin.slurp: - src: "{{ seriatim_verify_state_dir }}/fingerprint.sha256" - register: seriatim_verify_fingerprint - - - name: Read stored Seriatim checksum - ansible.builtin.slurp: - src: "{{ seriatim_verify_state_dir }}/binary.sha256" - register: seriatim_verify_checksum - - - name: Require valid Seriatim installation state + - name: Assert the installation contract ansible.builtin.assert: that: - - ansible_facts.distribution == 'Ubuntu' - - ansible_facts.distribution_version is version('26.04', '==') - - ansible_facts.architecture in ['x86_64', 'aarch64'] - - seriatim_verify_source_stat.stat.isdir - - seriatim_verify_source_stat.stat.pw_name == 'root' - - seriatim_verify_source_stat.stat.gr_name == 'root' - - seriatim_verify_binary_stat.stat.isreg - - seriatim_verify_binary_stat.stat.pw_name == 'root' - - seriatim_verify_binary_stat.stat.gr_name == 'root' - - seriatim_verify_binary_stat.stat.mode == '0755' - - seriatim_verify_state_stat.stat.isdir - - seriatim_verify_state_stat.stat.pw_name == 'root' - - seriatim_verify_state_stat.stat.gr_name == 'root' - - seriatim_verify_state_stat.stat.mode == '0755' - - seriatim_verify_state_file_stats.results | map(attribute='stat.isreg') | unique | list == [true] - - seriatim_verify_state_file_stats.results | map(attribute='stat.pw_name') | unique | list == ['root'] - - seriatim_verify_state_file_stats.results | map(attribute='stat.gr_name') | unique | list == ['root'] - - seriatim_verify_state_file_stats.results | map(attribute='stat.mode') | unique | list == ['0644'] - - seriatim_verify_commit.stdout == seriatim_expected_commit - - seriatim_verify_source_status.stdout == '' - - seriatim_verify_version.stdout == 'seriatim version ' ~ seriatim_expected_version - - (seriatim_verify_inputs.content | b64decode | trim | hash('sha256')) == (seriatim_verify_fingerprint.content | b64decode | trim) - - seriatim_verify_binary_stat.stat.checksum == (seriatim_verify_checksum.content | b64decode | trim) - fail_msg: Installed Seriatim source, binary, or build state is inconsistent. + - seriatim_source.stat.isdir + - seriatim_source.stat.pw_name == "root" + - seriatim_source.stat.gr_name == "root" + - seriatim_source_ownership.stdout == "" + - seriatim_source_commit.stdout == seriatim_expected_commit + - seriatim_binary.stat.isreg + - seriatim_binary.stat.pw_name == "root" + - seriatim_binary.stat.gr_name == "root" + - seriatim_binary.stat.mode == "0755" + - seriatim_version_output.stdout == "seriatim version " ~ seriatim_expected_version