From 90d7f45eaa5e82c23451ac0fcde3d59484e2c1c8 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 9 Sep 2026 14:36:55 +0000 Subject: [PATCH] Install and configure Scriptorium --- README.md | 79 ++++++++++++++++++++++++++- ansible.cfg | 2 + collections/requirements.yml | 4 ++ defaults/main.yml | 19 +++++++ meta/argument_specs.yml | 59 ++++++++++++++++++++ meta/main.yml | 18 +++++++ tasks/main.yml | 101 +++++++++++++++++++++++++++++++++++ templates/config.yml.j2 | 38 +++++++++++++ tests/converge.yml | 6 +++ tests/integration | 56 +++++++++++++++++++ tests/inventory | 1 + 11 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 ansible.cfg create mode 100644 collections/requirements.yml create mode 100644 defaults/main.yml create mode 100644 meta/argument_specs.yml create mode 100644 meta/main.yml create mode 100644 tasks/main.yml create mode 100644 templates/config.yml.j2 create mode 100644 tests/converge.yml create mode 100755 tests/integration create mode 100644 tests/inventory diff --git a/README.md b/README.md index 9936a7a..12af421 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,78 @@ -# ansible-role-scriptorium +# Ansible Role: Scriptorium -Ansible role to install the Scriptorium application. \ No newline at end of file +Installs and configures [Scriptorium](https://gitea.maximumdirect.net/eric/scriptorium) +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 +scriptorium_version: v0.13.0 +scriptorium_repository_url: https://gitea.maximumdirect.net/eric/scriptorium.git +scriptorium_source_dir: /usr/local/src/scriptorium +scriptorium_binary_path: /usr/local/bin/scriptorium +scriptorium_manage_build_dependencies: true +scriptorium_go_snap_channel: 1.26/stable +scriptorium_go_binary: /snap/bin/go +scriptorium_config_path: /usr/local/etc/scriptorium/config.yml +scriptorium_prompt_dir: null +scriptorium_profile_dir: null +scriptorium_schema_dir: null +scriptorium_server_addr: null +scriptorium_server_artifact_root: null +scriptorium_server_max_request_bytes: null +scriptorium_server_max_artifact_bytes: null +scriptorium_server_max_response_bytes: null +scriptorium_defaults_render_format: null +scriptorium_backends: null +``` + +When `scriptorium_manage_build_dependencies` is `false`, Git and the executable +selected by `scriptorium_go_binary` must already be available. Otherwise, the +role installs Git with APT and the classic Go Snap from +`scriptorium_go_snap_channel`. It checks out `scriptorium_version` +authoritatively, builds `./cmd/scriptorium` directly to +`scriptorium_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`. + +Every application configuration variable defaults to `null`, so it and any +empty parent section are omitted and Scriptorium retains ownership of its +built-in defaults. When no values are selected, the role writes `{}` to +`scriptorium_config_path` as `root:root` mode `0644`. Non-null falsey values +are retained. `scriptorium_backends` 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 +scriptorium_backends: "{{ scriptorium_local_backends | combine(scriptorium_remote_backends) }}" +``` + +Backend entries may name a credential environment variable through +`api_key_env`; credential values themselves belong outside this role. The role +does not install prompt, profile, or schema content and does not manage a +service. + +## Example + +```yaml +- hosts: scriptorium_hosts + roles: + - role: ansible-role-scriptorium +``` + +## Validation + +The repository-local integration suite installs its pinned collection, runs +syntax checking and production-profile lint, and applies the default role. It +uses an offline native profile inspection with the managed configuration, then +requires clean repeated convergence. It mutates only the managed source, +binary, configuration, and a private temporary directory, then restores the +default deployment during cleanup. + +```sh +tests/integration +``` + +Ansible role to install the Scriptorium application. 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..3097d5c --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,19 @@ +--- +scriptorium_version: v0.13.0 +scriptorium_repository_url: https://gitea.maximumdirect.net/eric/scriptorium.git +scriptorium_source_dir: /usr/local/src/scriptorium +scriptorium_binary_path: /usr/local/bin/scriptorium +scriptorium_manage_build_dependencies: true +scriptorium_go_snap_channel: 1.26/stable +scriptorium_go_binary: /snap/bin/go +scriptorium_config_path: /usr/local/etc/scriptorium/config.yml +scriptorium_prompt_dir: null +scriptorium_profile_dir: null +scriptorium_schema_dir: null +scriptorium_server_addr: null +scriptorium_server_artifact_root: null +scriptorium_server_max_request_bytes: null +scriptorium_server_max_artifact_bytes: null +scriptorium_server_max_response_bytes: null +scriptorium_defaults_render_format: null +scriptorium_backends: null diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 0000000..77cdd75 --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,59 @@ +--- +argument_specs: + main: + short_description: Install and configure Scriptorium from source + options: + scriptorium_version: + type: str + required: true + scriptorium_repository_url: + type: str + required: true + scriptorium_source_dir: + type: path + required: true + scriptorium_binary_path: + type: path + required: true + scriptorium_manage_build_dependencies: + type: bool + required: true + scriptorium_go_snap_channel: + type: str + required: true + scriptorium_go_binary: + type: path + required: true + scriptorium_config_path: + type: path + required: true + scriptorium_prompt_dir: + type: path + required: false + scriptorium_profile_dir: + type: path + required: false + scriptorium_schema_dir: + type: path + required: false + scriptorium_server_addr: + type: str + required: false + scriptorium_server_artifact_root: + type: str + required: false + scriptorium_server_max_request_bytes: + type: int + required: false + scriptorium_server_max_artifact_bytes: + type: int + required: false + scriptorium_server_max_response_bytes: + type: int + required: false + scriptorium_defaults_render_format: + type: str + required: false + scriptorium_backends: + type: dict + required: false diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..129cb41 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,18 @@ +--- +galaxy_info: + role_name: scriptorium + namespace: rakestraw + author: Eric Rakestraw + description: Install and configure Scriptorium from source + license: CC0-1.0 + min_ansible_version: "2.18" + issue_tracker_url: https://gitea.maximumdirect.net/eric/ansible-role-scriptorium/issues + platforms: + - name: Ubuntu + versions: + - resolute + galaxy_tags: + - scriptorium + - promptkit + - processing +dependencies: [] diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..0fbfc7b --- /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: Scriptorium 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: scriptorium_manage_build_dependencies + +- name: Install Go + become: true + community.general.snap: + name: go + channel: "{{ scriptorium_go_snap_channel }}" + classic: true + state: present + when: scriptorium_manage_build_dependencies + +- name: Create installation parent directories + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ scriptorium_source_dir | dirname }}" + - "{{ scriptorium_binary_path | dirname }}" + - "{{ scriptorium_config_path | dirname }}" + +- name: Check out Scriptorium source + become: true + ansible.builtin.git: + repo: "{{ scriptorium_repository_url }}" + dest: "{{ scriptorium_source_dir }}" + version: "{{ scriptorium_version }}" + force: true + register: scriptorium_checkout + +- name: Inspect the installed binary + ansible.builtin.stat: + path: "{{ scriptorium_binary_path }}" + register: scriptorium_binary + +- name: Build Scriptorium + become: true + ansible.builtin.command: + argv: + - "{{ scriptorium_go_binary }}" + - build + - -trimpath + - -ldflags + - -s -w + - -o + - "{{ scriptorium_binary_path }}" + - ./cmd/scriptorium + chdir: "{{ scriptorium_source_dir }}" + environment: + CGO_ENABLED: "0" + GOWORK: "off" + GOTOOLCHAIN: local + changed_when: true + when: >- + scriptorium_checkout.changed or + not scriptorium_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: "{{ scriptorium_source_dir }}" + state: directory + recurse: true + - path: "{{ scriptorium_binary_path }}" + state: file + mode: "0755" + +- name: Write Scriptorium configuration + become: true + ansible.builtin.template: + src: config.yml.j2 + dest: "{{ scriptorium_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..0f603bc --- /dev/null +++ b/templates/config.yml.j2 @@ -0,0 +1,38 @@ +{% if scriptorium_prompt_dir is none and scriptorium_profile_dir is none and scriptorium_schema_dir is none and scriptorium_server_addr is none and scriptorium_server_artifact_root is none and scriptorium_server_max_request_bytes is none and scriptorium_server_max_artifact_bytes is none and scriptorium_server_max_response_bytes is none and scriptorium_defaults_render_format is none and scriptorium_backends is none %} +{} +{% else %} +{% if scriptorium_prompt_dir is not none %} +prompt_dir: {{ scriptorium_prompt_dir | to_json }} +{% endif %} +{% if scriptorium_profile_dir is not none %} +profile_dir: {{ scriptorium_profile_dir | to_json }} +{% endif %} +{% if scriptorium_schema_dir is not none %} +schema_dir: {{ scriptorium_schema_dir | to_json }} +{% endif %} +{% if scriptorium_server_addr is not none or scriptorium_server_artifact_root is not none or scriptorium_server_max_request_bytes is not none or scriptorium_server_max_artifact_bytes is not none or scriptorium_server_max_response_bytes is not none %} +server: +{% if scriptorium_server_addr is not none %} + addr: {{ scriptorium_server_addr | to_json }} +{% endif %} +{% if scriptorium_server_artifact_root is not none %} + artifact_root: {{ scriptorium_server_artifact_root | to_json }} +{% endif %} +{% if scriptorium_server_max_request_bytes is not none %} + max_request_bytes: {{ scriptorium_server_max_request_bytes | to_json }} +{% endif %} +{% if scriptorium_server_max_artifact_bytes is not none %} + max_artifact_bytes: {{ scriptorium_server_max_artifact_bytes | to_json }} +{% endif %} +{% if scriptorium_server_max_response_bytes is not none %} + max_response_bytes: {{ scriptorium_server_max_response_bytes | to_json }} +{% endif %} +{% endif %} +{% if scriptorium_defaults_render_format is not none %} +defaults: + render_format: {{ scriptorium_defaults_render_format | to_json }} +{% endif %} +{% if scriptorium_backends is not none %} +backends: {{ scriptorium_backends | to_json }} +{% endif %} +{% endif %} diff --git a/tests/converge.yml b/tests/converge.yml new file mode 100644 index 0000000..ea1b950 --- /dev/null +++ b/tests/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge Scriptorium role + hosts: localhost + gather_facts: true + roles: + - role: ansible-role-scriptorium diff --git a/tests/integration b/tests/integration new file mode 100755 index 0000000..b27fe2f --- /dev/null +++ b/tests/integration @@ -0,0 +1,56 @@ +#!/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" +ansible_playbook="$workspace_venv/bin/ansible-playbook" +binary_path=/usr/local/bin/scriptorium +config_path=/usr/local/etc/scriptorium/config.yml +profile_dir="$test_tmp/profiles" + +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" +} + +run_converge() { + output_file=$1 + "$ansible_playbook" -i "$inventory" "$converge" | tee "$output_file" +} + +mkdir -p "$profile_dir" +printf '%s\n' \ + 'id: endpoint-profile' \ + 'endpoint: http://127.0.0.1:8000/v1' \ + 'model: offline-fixture' > "$profile_dir/endpoint-profile.yml" + +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" + +baseline_log="$test_tmp/baseline.log" +run_converge "$baseline_log" +"$binary_path" inspect profile --config "$config_path" \ + --profile-dir "$profile_dir" --profile endpoint-profile +baseline_repeat_log="$test_tmp/baseline-repeat.log" +run_converge "$baseline_repeat_log" +require_clean_recap "$baseline_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