From 26499f56021f35d6a8dbf81348053ca73bf80af6 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 14 Apr 2026 18:20:38 +0800 Subject: [PATCH] Add docs.svc.plus deployment playbook --- deploy_docs_svc_plus.yml | 58 ++++++ inventory.ini | 7 +- roles/vhosts/docs_service/defaults/main.yml | 35 ++++ roles/vhosts/docs_service/tasks/main.yml | 185 ++++++++++++++++++ .../docs_service/templates/Caddyfile.j2 | 10 + .../templates/docker-compose.yml.j2 | 10 + .../docs_service/templates/env.runtime.j2 | 6 + 7 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 deploy_docs_svc_plus.yml create mode 100644 roles/vhosts/docs_service/defaults/main.yml create mode 100644 roles/vhosts/docs_service/tasks/main.yml create mode 100644 roles/vhosts/docs_service/templates/Caddyfile.j2 create mode 100644 roles/vhosts/docs_service/templates/docker-compose.yml.j2 create mode 100644 roles/vhosts/docs_service/templates/env.runtime.j2 diff --git a/deploy_docs_svc_plus.yml b/deploy_docs_svc_plus.yml new file mode 100644 index 0000000..0d8dd46 --- /dev/null +++ b/deploy_docs_svc_plus.yml @@ -0,0 +1,58 @@ +- name: Deploy managed docs.svc.plus service + hosts: "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}" + gather_facts: true + become: true + vars: + docs_service_image_ref: >- + {{ + (lookup('ansible.builtin.env', 'DOCS_IMAGE_REF') | default('', true) | trim) + or + ( + (lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO') | default('ghcr.io/x-evor/docs', true)) + ~ ':' + ~ (lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG') | default('latest', true)) + ) + }} + docs_service_image_repo: >- + {{ lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO') + | default('ghcr.io/x-evor/docs', true) }} + docs_service_image_tag: >- + {{ lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG') + | default('latest', true) }} + docs_service_pull_image: >- + {{ lookup('ansible.builtin.env', 'DOCS_PULL_IMAGE') + | default(true, true) | bool }} + docs_service_knowledge_repo_path_host: >- + {{ lookup('ansible.builtin.env', 'DOCS_KNOWLEDGE_REPO_PATH_HOST') + | default('', true) }} + docs_service_internal_service_token: >- + {{ + lookup('ansible.builtin.env', 'DOCS_INTERNAL_SERVICE_TOKEN') + | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) + }} + docs_service_reload_interval: >- + {{ lookup('ansible.builtin.env', 'DOCS_RELOAD_INTERVAL') + | default('5m', true) }} + docs_service_container_port: >- + {{ lookup('ansible.builtin.env', 'DOCS_SERVICE_PORT') + | default('8084', true) }} + docs_service_host_port: >- + {{ lookup('ansible.builtin.env', 'DOCS_HOST_PORT') + | default('18086', true) }} + roles: + - roles/vhosts/docker + - roles/vhosts/caddy + - roles/vhosts/docs_service + +- name: Sync docs DNS records when requested + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Reconcile Cloudflare DNS for docs target host + when: docs_service_sync_dns | default(false) + ansible.builtin.include_role: + name: cloudflare_svc_plus_dns + vars: + cloudflare_dns_source_hosts: + - "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}" diff --git a/inventory.ini b/inventory.ini index 4c073cf..d42e274 100644 --- a/inventory.ini +++ b/inventory.ini @@ -4,8 +4,8 @@ cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root firewall_manage_ufw=false service_domains=cn-front.svc.plus [jp_xhttp_contabo_host] -# services: api.svc.plus, console.svc.plus, accounts.svc.plus, acp-server.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, openclaw.svc.plus, postgresql.svc.plus -jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,accounts.svc.plus,acp-server.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,openclaw.svc.plus,postgresql.svc.plus xray_exporter_node_id_custom=jp-xhttp-contabo.svc.plus +# services: api.svc.plus, console.svc.plus, docs.svc.plus, accounts.svc.plus, acp-server.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, openclaw.svc.plus, postgresql.svc.plus +jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,docs.svc.plus,accounts.svc.plus,acp-server.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,openclaw.svc.plus,postgresql.svc.plus xray_exporter_node_id_custom=jp-xhttp-contabo.svc.plus [tky_proxy_host] # services: tky-proxy.svc.plus @@ -37,6 +37,9 @@ jp-xhttp-contabo.svc.plus [accounts] jp-xhttp-contabo.svc.plus +[docs] +jp-xhttp-contabo.svc.plus + [apisix] jp-xhttp-contabo.svc.plus diff --git a/roles/vhosts/docs_service/defaults/main.yml b/roles/vhosts/docs_service/defaults/main.yml new file mode 100644 index 0000000..f46d312 --- /dev/null +++ b/roles/vhosts/docs_service/defaults/main.yml @@ -0,0 +1,35 @@ +--- +docs_service_base_dir: "{{ lookup('ansible.builtin.env', 'DOCS_BASE_DIR') | default('/opt/docs-svc-plus', true) }}" +docs_service_compose_file: "{{ docs_service_base_dir }}/docker-compose.yml" +docs_service_runtime_env_file: "{{ docs_service_base_dir }}/.env.runtime" +docs_service_project_name: "{{ lookup('ansible.builtin.env', 'DOCS_PROJECT_NAME') | default('docs-svc-plus', true) }}" +docs_service_server_name: docs +docs_service_release_id: "{{ lookup('env', 'RELEASE_ID') | default(lookup('pipe', 'git -C ' ~ playbook_dir ~ ' rev-parse --short HEAD'), true) }}" +docs_service_hostname: "{{ inventory_hostname | default(ansible_facts['hostname']) | default('unknown-host', true) }}" +docs_service_canonical_domain: "{{ lookup('ansible.builtin.env', 'DOCS_CANONICAL_DOMAIN') | default('docs.svc.plus', true) }}" +docs_service_served_domains: "{{ lookup('ansible.builtin.env', 'DOCS_SERVED_DOMAINS') | default(docs_service_canonical_domain, true) }}" +docs_service_domain_slug: "{{ docs_service_canonical_domain | replace('.', '-') }}" +docs_service_caddy_conf_dir: /etc/caddy/conf.d +docs_service_caddy_fragment_name: "{{ docs_service_server_name }}-{{ docs_service_release_id }}-{{ docs_service_hostname }}-{{ docs_service_domain_slug }}.caddy" +docs_service_caddy_fragment_path: "{{ docs_service_caddy_conf_dir }}/{{ docs_service_caddy_fragment_name }}" +docs_service_manage_caddy: true + +docs_service_image_ref: "{{ docs_service_image_repo }}:{{ docs_service_image_tag }}" +docs_service_image_repo: ghcr.io/x-evor/docs +docs_service_image_tag: latest +docs_service_pull_image: true +docs_service_registry: "{{ lookup('ansible.builtin.env', 'DOCS_REGISTRY') | default('ghcr.io', true) }}" +docs_service_registry_username: "{{ lookup('ansible.builtin.env', 'GHCR_USERNAME') | default('', true) }}" +docs_service_registry_password: "{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD') | default(lookup('ansible.builtin.env', 'GHCR_TOKEN') | default('', true), true) }}" + +docs_service_container_port: "8084" +docs_service_host_port: "18086" +docs_service_runtime_port: "{{ docs_service_container_port }}" + +docs_service_knowledge_repo_path_host: "{{ lookup('ansible.builtin.env', 'DOCS_KNOWLEDGE_REPO_PATH_HOST') | default('', true) }}" +docs_service_knowledge_repo_path_container: /knowledge + +docs_service_internal_service_token: "{{ lookup('ansible.builtin.env', 'DOCS_INTERNAL_SERVICE_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) }}" +docs_service_reload_interval: "{{ lookup('ansible.builtin.env', 'DOCS_RELOAD_INTERVAL') | default('5m', true) }}" + +docs_service_healthcheck_url: "http://127.0.0.1:{{ docs_service_host_port }}/healthz" diff --git a/roles/vhosts/docs_service/tasks/main.yml b/roles/vhosts/docs_service/tasks/main.yml new file mode 100644 index 0000000..8610da6 --- /dev/null +++ b/roles/vhosts/docs_service/tasks/main.yml @@ -0,0 +1,185 @@ +--- +- name: Ensure docs service base directory exists + ansible.builtin.file: + path: "{{ docs_service_base_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Assert docs deploy uses prebuilt registry image + ansible.builtin.assert: + that: + - docs_service_image_ref | trim | length > 0 + - "'/' in (docs_service_image_ref | trim)" + - "':' in (docs_service_image_ref | trim)" + fail_msg: >- + DOCS deploy requires a prebuilt image reference via DOCS_IMAGE_REF or + DOCS_IMAGE_REPO + DOCS_IMAGE_TAG. This role never builds images on the + target host. + +- name: Assert docs knowledge repo path is configured + ansible.builtin.assert: + that: + - docs_service_knowledge_repo_path_host | trim | length > 0 + fail_msg: >- + DOCS_KNOWLEDGE_REPO_PATH_HOST must be exported before running this playbook. + +- name: Assert docs internal service token is configured + ansible.builtin.assert: + that: + - docs_service_internal_service_token | trim | length > 0 + fail_msg: >- + DOCS_INTERNAL_SERVICE_TOKEN or INTERNAL_SERVICE_TOKEN must be exported + before running this playbook. + +- name: Check docs knowledge repo path exists on target host + ansible.builtin.stat: + path: "{{ docs_service_knowledge_repo_path_host }}" + register: docs_service_knowledge_repo_stat + +- name: Assert docs knowledge repo path exists on target host + ansible.builtin.assert: + that: + - docs_service_knowledge_repo_stat.stat.exists + - docs_service_knowledge_repo_stat.stat.isdir + fail_msg: >- + The configured docs knowledge repo path {{ docs_service_knowledge_repo_path_host }} + does not exist or is not a directory on the target host. + +- name: Log into container registry for docs service + ansible.builtin.shell: | + set -euo pipefail + printf '%s' '{{ docs_service_registry_password }}' | docker login {{ docs_service_registry }} -u '{{ docs_service_registry_username }}' --password-stdin + args: + executable: /bin/bash + no_log: true + when: + - docs_service_registry_username | length > 0 + - docs_service_registry_password | length > 0 + +- name: Render docs compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ docs_service_compose_file }}" + owner: root + group: root + mode: "0644" + +- name: Render docs runtime env file + ansible.builtin.template: + src: env.runtime.j2 + dest: "{{ docs_service_runtime_env_file }}" + owner: root + group: root + mode: "0600" + no_log: true + +- name: Validate docs compose file + ansible.builtin.command: >- + docker compose + --project-name {{ docs_service_project_name }} + -f {{ docs_service_compose_file }} + --env-file {{ docs_service_runtime_env_file }} + config + args: + chdir: "{{ docs_service_base_dir }}" + changed_when: false + +- name: Pull docs service image + ansible.builtin.command: >- + docker compose + --project-name {{ docs_service_project_name }} + -f {{ docs_service_compose_file }} + --env-file {{ docs_service_runtime_env_file }} + pull docs + args: + chdir: "{{ docs_service_base_dir }}" + when: docs_service_pull_image | bool + +- name: Start docs container + ansible.builtin.command: >- + docker compose + --project-name {{ docs_service_project_name }} + -f {{ docs_service_compose_file }} + --env-file {{ docs_service_runtime_env_file }} + up -d --remove-orphans docs + args: + chdir: "{{ docs_service_base_dir }}" + +- name: Ensure Caddy fragment directory exists + ansible.builtin.file: + path: "{{ docs_service_caddy_conf_dir }}" + state: directory + owner: root + group: root + mode: "0755" + when: docs_service_manage_caddy | bool + +- name: Render docs Caddy fragment + ansible.builtin.template: + src: Caddyfile.j2 + dest: "{{ docs_service_caddy_fragment_path }}" + owner: root + group: root + mode: "0644" + when: docs_service_manage_caddy | bool + +- name: Remove obsolete docs Caddy fragments + ansible.builtin.shell: | + set -euo pipefail + shopt -s nullglob + current="{{ docs_service_caddy_fragment_path }}" + changed=0 + for candidate in {{ docs_service_caddy_conf_dir }}/docs*.caddy {{ docs_service_caddy_conf_dir }}/docs*.caddy.bak*; do + if [ "$candidate" != "$current" ]; then + rm -f "$candidate" + changed=1 + fi + done + if [ "$changed" -eq 1 ]; then + echo changed + fi + args: + executable: /bin/bash + register: docs_service_caddy_cleanup + changed_when: docs_service_caddy_cleanup.stdout | trim != "" + when: docs_service_manage_caddy | bool + +- name: Validate Caddy config after updating docs fragment + ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile + changed_when: false + when: + - docs_service_manage_caddy | bool + - not ansible_check_mode + +- name: Reload Caddy after updating docs fragment + ansible.builtin.service: + name: caddy + state: reloaded + when: + - docs_service_manage_caddy | bool + - not ansible_check_mode + +- name: Check docs health endpoint + ansible.builtin.uri: + url: "{{ docs_service_healthcheck_url }}" + method: GET + status_code: 200 + register: docs_service_healthcheck + retries: 10 + delay: 3 + until: docs_service_healthcheck.status == 200 + changed_when: false + when: not ansible_check_mode + +- name: Show docs compose status + ansible.builtin.command: >- + docker compose + --project-name {{ docs_service_project_name }} + -f {{ docs_service_compose_file }} + --env-file {{ docs_service_runtime_env_file }} + ps + args: + chdir: "{{ docs_service_base_dir }}" + changed_when: false diff --git a/roles/vhosts/docs_service/templates/Caddyfile.j2 b/roles/vhosts/docs_service/templates/Caddyfile.j2 new file mode 100644 index 0000000..e5f18d4 --- /dev/null +++ b/roles/vhosts/docs_service/templates/Caddyfile.j2 @@ -0,0 +1,10 @@ +{{ '{$SERVED_DOMAINS}' }} { + encode zstd gzip + + reverse_proxy 127.0.0.1:{{ docs_service_host_port }} { + header_up Host {host} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} diff --git a/roles/vhosts/docs_service/templates/docker-compose.yml.j2 b/roles/vhosts/docs_service/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..e757f3a --- /dev/null +++ b/roles/vhosts/docs_service/templates/docker-compose.yml.j2 @@ -0,0 +1,10 @@ +services: + docs: + image: ${IMAGE:?set IMAGE in .env.runtime} + restart: unless-stopped + env_file: + - .env.runtime + ports: + - "127.0.0.1:{{ docs_service_host_port }}:{{ docs_service_container_port }}" + volumes: + - "{{ docs_service_knowledge_repo_path_host }}:{{ docs_service_knowledge_repo_path_container }}" diff --git a/roles/vhosts/docs_service/templates/env.runtime.j2 b/roles/vhosts/docs_service/templates/env.runtime.j2 new file mode 100644 index 0000000..1759f74 --- /dev/null +++ b/roles/vhosts/docs_service/templates/env.runtime.j2 @@ -0,0 +1,6 @@ +IMAGE={{ docs_service_image_ref }} +SERVED_DOMAINS={{ docs_service_served_domains }} +KNOWLEDGE_REPO_PATH={{ docs_service_knowledge_repo_path_container }} +DOCS_SERVICE_PORT={{ docs_service_container_port }} +INTERNAL_SERVICE_TOKEN={{ docs_service_internal_service_token }} +DOCS_RELOAD_INTERVAL={{ docs_service_reload_interval }}