diff --git a/README.md b/README.md index 2c55006..5aac508 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,78 @@ -# ansible-role-notarius +# Ansible Role: Notarius -Ansible role to install the Notarius application. \ No newline at end of file +Installs and configures [Notarius](https://gitea.maximumdirect.net/eric/notarius) +from source on Ubuntu 26.04 (`x86_64` or `aarch64`). The role requires gathered +facts and uses privilege escalation only for dependency installation and +root-owned application state. + +## Variables + +```yaml +notarius_version: v0.6.0 +notarius_repository_url: https://gitea.maximumdirect.net/eric/notarius.git +notarius_source_dir: /usr/local/src/notarius +notarius_binary_path: /usr/local/bin/notarius +notarius_manage_build_dependencies: true +notarius_go_snap_channel: 1.26/stable +notarius_go_binary: /snap/bin/go +notarius_config_path: /usr/local/etc/notarius/config.yml +notarius_config_version: 4 +notarius_promptkit_profile_dir: null +notarius_promptkit_profile_file: null +notarius_promptkit_local_backend_endpoint: null +notarius_promptkit_local_backend_concurrency_limit: null +notarius_pipelines: null +notarius_concurrency_total_llm: null +notarius_concurrency_stage_workers_extract: null +notarius_output_directory: null +notarius_cache_chunk_plans_mode: null +notarius_cache_chunk_plans_directory: null +notarius_cache_checkpoints_enabled: null +notarius_cache_checkpoints_directory: null +notarius_debug_directory: null +``` + +When `notarius_manage_build_dependencies` is `false`, Git and the executable +selected by `notarius_go_binary` must already be available. Otherwise, the role +installs Git with APT and the classic Go Snap from `notarius_go_snap_channel`. +It checks out `notarius_version` authoritatively, builds `./cmd/notarius` +directly to `notarius_binary_path`, and rebuilds only when that checkout changes +or the binary is absent. The source tree is recursively owned by `root:root`; +the binary is `root:root` mode `0755`. + +Configuration variables map directly to Notarius's versioned YAML fields. +Every optional value defaults to `null`, so it and any empty parent section are +omitted and Notarius retains ownership of its built-in defaults. The role always +emits `notarius_config_version` as `version` and writes the file as `root:root` +mode `0644`. `notarius_pipelines` is the native mapping supplied by the caller; +the role does not merge or rewrite it. For example, inventory can compose +fragments in the ordinary way: + +```yaml +notarius_pipelines: "{{ notarius_pipeline_group_one | combine(notarius_pipeline_group_two) }}" +``` + +PromptKit profile values identify profile sources or a local backend endpoint; +they never contain credentials. Supply any credential environment variables +outside this role. + +## Example + +```yaml +- hosts: notarius_hosts + roles: + - role: ansible-role-notarius +``` + +## Validation + +The repository-local integration suite installs its pinned collection, runs +syntax checking and production-profile lint, verifies the default release, +ownership, executable output, minimal configuration, native configuration +validation, and immediate idempotence. It mutates only the managed source, +binary, configuration, and a private temporary directory, then leaves the +default release and minimal configuration installed. + +```sh +tests/integration +``` diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..2d4c5f9 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +roles_path = .. diff --git a/collections/requirements.yml b/collections/requirements.yml new file mode 100644 index 0000000..22c02b2 --- /dev/null +++ b/collections/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.general + version: 13.3.0 diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..1d5184f --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,23 @@ +--- +notarius_version: v0.6.0 +notarius_repository_url: https://gitea.maximumdirect.net/eric/notarius.git +notarius_source_dir: /usr/local/src/notarius +notarius_binary_path: /usr/local/bin/notarius +notarius_manage_build_dependencies: true +notarius_go_snap_channel: 1.26/stable +notarius_go_binary: /snap/bin/go +notarius_config_path: /usr/local/etc/notarius/config.yml +notarius_config_version: 4 +notarius_promptkit_profile_dir: null +notarius_promptkit_profile_file: null +notarius_promptkit_local_backend_endpoint: null +notarius_promptkit_local_backend_concurrency_limit: null +notarius_pipelines: null +notarius_concurrency_total_llm: null +notarius_concurrency_stage_workers_extract: null +notarius_output_directory: null +notarius_cache_chunk_plans_mode: null +notarius_cache_chunk_plans_directory: null +notarius_cache_checkpoints_enabled: null +notarius_cache_checkpoints_directory: null +notarius_debug_directory: null diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 0000000..5b66c1c --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,71 @@ +--- +argument_specs: + main: + short_description: Install and configure Notarius from source + options: + notarius_version: + type: str + required: true + notarius_repository_url: + type: str + required: true + notarius_source_dir: + type: path + required: true + notarius_binary_path: + type: path + required: true + notarius_manage_build_dependencies: + type: bool + required: true + notarius_go_snap_channel: + type: str + required: true + notarius_go_binary: + type: path + required: true + notarius_config_path: + type: path + required: true + notarius_config_version: + type: int + required: true + notarius_promptkit_profile_dir: + type: str + required: false + notarius_promptkit_profile_file: + type: str + required: false + notarius_promptkit_local_backend_endpoint: + type: str + required: false + notarius_promptkit_local_backend_concurrency_limit: + type: int + required: false + notarius_pipelines: + type: dict + required: false + notarius_concurrency_total_llm: + type: int + required: false + notarius_concurrency_stage_workers_extract: + type: int + required: false + notarius_output_directory: + type: str + required: false + notarius_cache_chunk_plans_mode: + type: str + required: false + notarius_cache_chunk_plans_directory: + type: str + required: false + notarius_cache_checkpoints_enabled: + type: bool + required: false + notarius_cache_checkpoints_directory: + type: str + required: false + notarius_debug_directory: + type: str + required: false diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..d7f8945 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,18 @@ +--- +galaxy_info: + role_name: notarius + namespace: rakestraw + author: Eric Rakestraw + description: Install and configure Notarius from source + license: BSD-3-Clause + min_ansible_version: "2.18" + issue_tracker_url: https://gitea.maximumdirect.net/eric/ansible-role-notarius/issues + platforms: + - name: Ubuntu + versions: + - resolute + galaxy_tags: + - notarius + - transcript + - processing +dependencies: [] diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..595c8b4 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,101 @@ +--- +- 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: Notarius supports Ubuntu 26.04 on x86_64 or aarch64. + +- name: Install Git + become: true + ansible.builtin.apt: + name: git + state: present + update_cache: true + cache_valid_time: 3600 + when: notarius_manage_build_dependencies + +- name: Install Go + become: true + community.general.snap: + name: go + channel: "{{ notarius_go_snap_channel }}" + classic: true + state: present + when: notarius_manage_build_dependencies + +- name: Create installation parent directories + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ notarius_source_dir | dirname }}" + - "{{ notarius_binary_path | dirname }}" + - "{{ notarius_config_path | dirname }}" + +- name: Check out Notarius source + become: true + ansible.builtin.git: + repo: "{{ notarius_repository_url }}" + dest: "{{ notarius_source_dir }}" + version: "{{ notarius_version }}" + force: true + register: notarius_checkout + +- name: Inspect the installed binary + ansible.builtin.stat: + path: "{{ notarius_binary_path }}" + register: notarius_binary + +- name: Build Notarius + become: true + ansible.builtin.command: + argv: + - "{{ notarius_go_binary }}" + - build + - -trimpath + - -ldflags + - "-X gitea.maximumdirect.net/eric/notarius/internal/buildinfo.Override={{ notarius_version }}" + - -o + - "{{ notarius_binary_path }}" + - ./cmd/notarius + chdir: "{{ notarius_source_dir }}" + environment: + CGO_ENABLED: "0" + GOTOOLCHAIN: local + GOWORK: "off" + changed_when: true + when: >- + notarius_checkout.changed or + not notarius_binary.stat.exists + +- name: Converge installed metadata + become: true + 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: "{{ notarius_source_dir }}" + state: directory + recurse: true + - path: "{{ notarius_binary_path }}" + state: file + mode: "0755" + +- name: Write Notarius configuration + become: true + ansible.builtin.template: + src: config.yml.j2 + dest: "{{ notarius_config_path }}" + owner: root + group: root + mode: "0644" diff --git a/templates/config.yml.j2 b/templates/config.yml.j2 new file mode 100644 index 0000000..0df404e --- /dev/null +++ b/templates/config.yml.j2 @@ -0,0 +1,61 @@ +version: {{ notarius_config_version }} +{% if notarius_promptkit_profile_dir is not none or notarius_promptkit_profile_file is not none or notarius_promptkit_local_backend_endpoint is not none or notarius_promptkit_local_backend_concurrency_limit is not none %} +promptkit: +{% if notarius_promptkit_profile_dir is not none %} + profile_dir: {{ notarius_promptkit_profile_dir | to_json }} +{% endif %} +{% if notarius_promptkit_profile_file is not none %} + profile_file: {{ notarius_promptkit_profile_file | to_json }} +{% endif %} +{% if notarius_promptkit_local_backend_endpoint is not none or notarius_promptkit_local_backend_concurrency_limit is not none %} + local_backend: +{% if notarius_promptkit_local_backend_endpoint is not none %} + endpoint: {{ notarius_promptkit_local_backend_endpoint | to_json }} +{% endif %} +{% if notarius_promptkit_local_backend_concurrency_limit is not none %} + concurrency_limit: {{ notarius_promptkit_local_backend_concurrency_limit | to_json }} +{% endif %} +{% endif %} +{% endif %} +{% if notarius_pipelines is not none %} +pipelines: {{ notarius_pipelines | to_json }} +{% endif %} +{% if notarius_concurrency_total_llm is not none or notarius_concurrency_stage_workers_extract is not none %} +concurrency: +{% if notarius_concurrency_total_llm is not none %} + total_llm: {{ notarius_concurrency_total_llm | to_json }} +{% endif %} +{% if notarius_concurrency_stage_workers_extract is not none %} + stage_workers: + extract: {{ notarius_concurrency_stage_workers_extract | to_json }} +{% endif %} +{% endif %} +{% if notarius_output_directory is not none %} +output: + directory: {{ notarius_output_directory | to_json }} +{% endif %} +{% if notarius_cache_chunk_plans_mode is not none or notarius_cache_chunk_plans_directory is not none or notarius_cache_checkpoints_enabled is not none or notarius_cache_checkpoints_directory is not none %} +cache: +{% if notarius_cache_chunk_plans_mode is not none or notarius_cache_chunk_plans_directory is not none %} + chunk_plans: +{% if notarius_cache_chunk_plans_mode is not none %} + mode: {{ notarius_cache_chunk_plans_mode | to_json }} +{% endif %} +{% if notarius_cache_chunk_plans_directory is not none %} + directory: {{ notarius_cache_chunk_plans_directory | to_json }} +{% endif %} +{% endif %} +{% if notarius_cache_checkpoints_enabled is not none or notarius_cache_checkpoints_directory is not none %} + checkpoints: +{% if notarius_cache_checkpoints_enabled is not none %} + enabled: {{ notarius_cache_checkpoints_enabled | to_json }} +{% endif %} +{% if notarius_cache_checkpoints_directory is not none %} + directory: {{ notarius_cache_checkpoints_directory | to_json }} +{% endif %} +{% endif %} +{% endif %} +{% if notarius_debug_directory is not none %} +debug: + directory: {{ notarius_debug_directory | to_json }} +{% endif %} diff --git a/tests/converge.yml b/tests/converge.yml new file mode 100644 index 0000000..003a20b --- /dev/null +++ b/tests/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge Notarius role + hosts: localhost + gather_facts: true + roles: + - role: ansible-role-notarius diff --git a/tests/integration b/tests/integration new file mode 100755 index 0000000..ba9d872 --- /dev/null +++ b/tests/integration @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +workspace_venv="$repository_root/../.venv" +test_tmp=$(mktemp -d) +chmod 0700 "$test_tmp" + +export ANSIBLE_COLLECTIONS_PATH="$test_tmp/collections" +export ANSIBLE_ROLES_PATH="$(dirname "$repository_root")" + +inventory="$repository_root/tests/inventory" +converge="$repository_root/tests/converge.yml" +verify="$repository_root/tests/verify.yml" +ansible_playbook="$workspace_venv/bin/ansible-playbook" +default_commit=77ae132e5609916da40dcd4fcc749c000a63cbac + +cleanup() { + exit_status=$? + trap - EXIT + "$ansible_playbook" -i "$inventory" "$converge" >/dev/null 2>&1 || true + rm -rf -- "$test_tmp" + exit "$exit_status" +} +trap cleanup EXIT + +require_clean_recap() { + grep -Eq 'changed=0 .*failed=0' "$1" +} + +cd "$repository_root" +"$workspace_venv/bin/ansible-galaxy" collection install \ + -r collections/requirements.yml \ + -p "$ANSIBLE_COLLECTIONS_PATH" +"$ansible_playbook" -i "$inventory" --syntax-check "$converge" +"$workspace_venv/bin/ansible-lint" "$converge" "$verify" + +baseline_log="$test_tmp/baseline.log" +"$ansible_playbook" -i "$inventory" "$converge" | tee "$baseline_log" +"$ansible_playbook" -i "$inventory" \ + -e "notarius_expected_version=v0.6.0" \ + -e "notarius_expected_commit=$default_commit" \ + "$verify" +repeat_log="$test_tmp/repeat.log" +"$ansible_playbook" -i "$inventory" "$converge" | tee "$repeat_log" +require_clean_recap "$repeat_log" diff --git a/tests/inventory b/tests/inventory new file mode 100644 index 0000000..2302eda --- /dev/null +++ b/tests/inventory @@ -0,0 +1 @@ +localhost ansible_connection=local diff --git a/tests/verify.yml b/tests/verify.yml new file mode 100644 index 0000000..c094db9 --- /dev/null +++ b/tests/verify.yml @@ -0,0 +1,83 @@ +--- +- name: Verify Notarius installation + hosts: localhost + gather_facts: false + tasks: + - name: Read the selected source commit + become: true + ansible.builtin.command: + argv: + - git + - -C + - /usr/local/src/notarius + - rev-parse + - HEAD + register: notarius_commit + changed_when: false + + - name: Inspect source ownership + become: true + ansible.builtin.command: + argv: + - find + - /usr/local/src/notarius + - -not + - -user + - root + - -o + - -not + - -group + - root + register: notarius_source_ownership + changed_when: false + + - name: Inspect binary metadata + become: true + ansible.builtin.stat: + path: /usr/local/bin/notarius + register: notarius_binary_metadata + + - name: Inspect configuration metadata + become: true + ansible.builtin.stat: + path: /usr/local/etc/notarius/config.yml + register: notarius_config_metadata + + - name: Read default configuration + become: true + ansible.builtin.slurp: + src: /usr/local/etc/notarius/config.yml + register: notarius_default_config + + - name: Read the installed version + ansible.builtin.command: + argv: + - /usr/local/bin/notarius + - --version + register: notarius_version_output + changed_when: false + + - name: Validate Notarius configuration + ansible.builtin.command: + argv: + - /usr/local/bin/notarius + - config + - validate + - --config + - /usr/local/etc/notarius/config.yml + changed_when: false + + - name: Assert the installation contract + ansible.builtin.assert: + that: + - notarius_commit.stdout == notarius_expected_commit + - notarius_source_ownership.stdout == "" + - notarius_binary_metadata.stat.isreg + - notarius_binary_metadata.stat.pw_name == "root" + - notarius_binary_metadata.stat.gr_name == "root" + - notarius_binary_metadata.stat.mode == "0755" + - notarius_config_metadata.stat.pw_name == "root" + - notarius_config_metadata.stat.gr_name == "root" + - notarius_config_metadata.stat.mode == "0644" + - '(notarius_default_config.content | b64decode | from_yaml) == {"version": 4}' + - notarius_version_output.stdout == "notarius " ~ notarius_expected_version