Commit remaining local changes

This commit is contained in:
Haitao Pan 2026-04-09 15:24:38 +08:00
parent c7ffff2825
commit 9ad2740997
14 changed files with 243 additions and 169 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.env
.artifacts/
.artifacts/acp_codex/xworkmate-go-core
.artifacts/acp_opencode/xworkmate-go-core

View File

@ -1 +1,82 @@
# playbooks
# playbooks
## Traffic Billing Stack
The traffic billing stack now has a single aggregate playbook:
`deploy_traffic_billing_stack.yml`
It orchestrates these existing playbooks in dependency order:
1. `deploy_xray_exporter.yml`
2. `deploy_billing_service.yml`
3. `deploy_accounts_svc_plus.yml`
4. `deploy_console_svc_plus.yml`
5. `deploy_agent_svc_plus.yml`
### Full stack deploy
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
ansible-playbook -i inventory.ini deploy_traffic_billing_stack.yml
```
### Deploy to one target host directly
Use `STACK_TARGET_HOST` to override all service host groups with one inventory host.
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export STACK_TARGET_HOST=jp-xhttp-contabo.svc.plus
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
ansible-playbook -i inventory.ini deploy_traffic_billing_stack.yml
```
### Deploy only selected services
Use `STACK_SERVICES` with a comma-separated list:
- `xray-exporter`
- `billing-service`
- `accounts`
- `console`
- `agent`
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export STACK_TARGET_HOST=jp-xhttp-contabo.svc.plus
export STACK_SERVICES=xray-exporter,billing-service,agent
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
ansible-playbook -i inventory.ini deploy_traffic_billing_stack.yml
```
### Notes
- `accounts` and `console` still use their existing role contracts.
- `console` requires `FRONTEND_IMAGE` because the target host only does pull-only compose deployment.
- `console` now writes a Caddy fragment named like `<server-name>-<release_id>-<hostname>-<domain>.caddy` instead of managing the Caddy service container itself.
- `billing-service` requires `DATABASE_URL`.
- `xray-exporter` and `agent` require `INTERNAL_SERVICE_TOKEN`.
### Deploy console to a specific host and sync DNS
`deploy_console_svc_plus.yml` now accepts two useful overrides:
- `console_service_target_host`: inventory host to deploy to, for example `jp_xhttp_contabo_host`
- `console_service_sync_dns=true`: rebuild and reconcile DNS records for that target host after deployment
Example:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
ansible-playbook -i inventory.ini deploy_console_svc_plus.yml \
-e console_service_target_host=jp_xhttp_contabo_host \
-e console_service_sync_dns=true \
-e FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
```

View File

@ -1,7 +1,20 @@
- name: Deploy managed console.svc.plus service
hosts: "{{ console_service_hosts | default('console') }}"
hosts: "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"
gather_facts: true
become: true
roles:
- roles/vhosts/docker
- roles/vhosts/console_service
- name: Sync console DNS records when requested
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Reconcile Cloudflare DNS for console target host
when: console_service_sync_dns | default(false)
ansible.builtin.include_role:
name: cloudflare_svc_plus_dns
vars:
cloudflare_dns_source_hosts:
- "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"

View File

@ -1,33 +1,30 @@
# Physical hosts
# Vhosts
[cn_front_host]
# services: cn-front.svc.plus
cn-front.svc.plus ansible_host=47.120.61.35
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
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
[tky_proxy_host]
# services: tky-proxy.svc.plus
tky-proxy.svc.plus ansible_host=43.207.194.92
tky-proxy.svc.plus ansible_host=43.207.194.92 ansible_user=admin ansible_ssh_user=admin service_domains=tky-proxy.svc.plus
[jp_k3s_vultr_host]
jp-k3s-vultr.svc.plus ansible_host=167.179.110.129
# services: jp-k3s-vultr.svc.plus
jp-k3s-vultr.svc.plus ansible_host=167.179.110.129 ansible_user=root ansible_ssh_user=root service_domains=jp-k3s-vultr.svc.plus
# Logical service groups
[web]
cn-front.svc.plus
[agent_proxy]
jp-xhttp-contabo.svc.plus
[agent_svc_plus]
tky-proxy.svc.plus
jp-xhttp-contabo.svc.plus
[xray_exporter]
tky-proxy.svc.plus
jp-xhttp-contabo.svc.plus
[billing_service]
@ -45,7 +42,6 @@ jp-xhttp-contabo.svc.plus
[k3s]
jp-k3s-vultr.svc.plus
[all:vars]
ansible_port=22
ansible_user=root

View File

@ -17,6 +17,11 @@
group: root
mode: "0755"
- name: Detect UFW binary for Codex ACP
ansible.builtin.stat:
path: /usr/sbin/ufw
register: acp_codex_ufw_binary
- name: Open HTTP and HTTPS ports when UFW is enabled
ansible.builtin.command: ufw allow 80/tcp
register: acp_codex_ufw_80
@ -24,6 +29,7 @@
failed_when: acp_codex_ufw_80.rc != 0
when:
- acp_codex_enable_ufw | bool
- acp_codex_ufw_binary.stat.exists | default(false)
- name: Open HTTPS port when UFW is enabled
ansible.builtin.command: ufw allow 443/tcp
@ -32,3 +38,4 @@
failed_when: acp_codex_ufw_443.rc != 0
when:
- acp_codex_enable_ufw | bool
- acp_codex_ufw_binary.stat.exists | default(false)

View File

@ -17,6 +17,11 @@
group: root
mode: "0755"
- name: Detect UFW binary for OpenCode ACP
ansible.builtin.stat:
path: /usr/sbin/ufw
register: acp_opencode_ufw_binary
- name: Open HTTP and HTTPS ports when UFW is enabled
ansible.builtin.command: ufw allow 80/tcp
register: acp_opencode_ufw_80
@ -24,6 +29,7 @@
failed_when: acp_opencode_ufw_80.rc != 0
when:
- acp_opencode_enable_ufw | bool
- acp_opencode_ufw_binary.stat.exists | default(false)
- name: Open HTTPS port when UFW is enabled
ansible.builtin.command: ufw allow 443/tcp
@ -32,3 +38,4 @@
failed_when: acp_opencode_ufw_443.rc != 0
when:
- acp_opencode_enable_ufw | bool
- acp_opencode_ufw_binary.stat.exists | default(false)

View File

@ -1,13 +1,24 @@
---
console_service_base_dir: "{{ lookup('ansible.builtin.env', 'CONSOLE_BASE_DIR') | default('/opt/console-svc-plus', true) }}"
console_service_compose_file: "{{ console_service_base_dir }}/docker-compose.yml"
console_service_caddyfile: "{{ console_service_base_dir }}/Caddyfile"
console_service_runtime_env_file: "{{ console_service_base_dir }}/.env.runtime"
console_service_project_name: "{{ lookup('ansible.builtin.env', 'CONSOLE_PROJECT_NAME') | default('console-svc-plus', true) }}"
console_service_server_name: console
console_service_release_id: "{{ lookup('env', 'RELEASE_ID') | default(lookup('pipe', 'git -C ' ~ playbook_dir ~ ' rev-parse --short HEAD'), true) }}"
console_service_hostname: "{{ inventory_hostname | default(ansible_facts['hostname']) | default('unknown-host', true) }}"
console_service_domain: "{{ lookup('ansible.builtin.env', 'CONSOLE_DOMAIN') | default('console.svc.plus', true) }}"
console_service_domain_slug: "{{ console_service_domain | replace('.', '-') }}"
console_service_caddy_conf_dir: /etc/caddy/conf.d
console_service_caddy_fragment_name: "{{ console_service_server_name }}-{{ console_service_release_id }}-{{ console_service_hostname }}-{{ console_service_domain_slug }}.caddy"
console_service_caddy_fragment_path: "{{ console_service_caddy_conf_dir }}/{{ console_service_caddy_fragment_name }}"
console_service_obsolete_caddy_fragment_patterns:
- /etc/caddy/conf.d/console.caddy
- /etc/caddy/conf.d/console-svc-plus.caddy
- /etc/caddy/conf.d/console-*.caddy
console_service_image_repo: "{{ lookup('ansible.builtin.env', 'CONSOLE_IMAGE_REPO') | default('ghcr.io/x-evor/dashboard', true) }}"
console_service_image_tag: "{{ lookup('ansible.builtin.env', 'CONSOLE_IMAGE_TAG') | default('latest', true) }}"
console_service_frontend_image: "{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE') | default('', true) }}"
console_service_frontend_image: "{{ lookup('ansible.builtin.vars', 'FRONTEND_IMAGE', default=lookup('ansible.builtin.env', 'FRONTEND_IMAGE') | default('', true)) }}"
console_service_registry: "{{ lookup('ansible.builtin.env', 'CONSOLE_REGISTRY') | default('ghcr.io', true) }}"
console_service_registry_username: "{{ lookup('ansible.builtin.env', 'GHCR_USERNAME') | default('', true) }}"
@ -65,3 +76,4 @@ console_service_next_public_stripe_price_xcloudflow_paygo: "{{ lookup('ansible.b
console_service_next_public_stripe_price_xcloudflow_subscription: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION') | default('', true) }}"
console_service_pull_images: true
console_service_manage_caddy: true

View File

@ -37,14 +37,6 @@
group: root
mode: "0644"
- name: Render console Caddyfile
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ console_service_caddyfile }}"
owner: root
group: root
mode: "0644"
- name: Render console runtime env file
ansible.builtin.template:
src: env.runtime.j2
@ -71,7 +63,7 @@
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
pull dashboard caddy
pull dashboard frontend-assets
args:
chdir: "{{ console_service_base_dir }}"
when: console_service_pull_images | bool
@ -86,16 +78,64 @@
args:
chdir: "{{ console_service_base_dir }}"
- name: Start console dashboard and caddy containers
- name: Start console dashboard container
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
up -d --remove-orphans dashboard caddy
up -d --remove-orphans dashboard
args:
chdir: "{{ console_service_base_dir }}"
- name: Ensure Caddy fragment directory exists
ansible.builtin.file:
path: "{{ console_service_caddy_conf_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: console_service_manage_caddy | bool
- name: Render console Caddy fragment
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ console_service_caddy_fragment_path }}"
owner: root
group: root
mode: "0644"
when: console_service_manage_caddy | bool
- name: Remove obsolete console Caddy fragments
ansible.builtin.shell: |
set -euo pipefail
shopt -s nullglob
current="{{ console_service_caddy_fragment_path }}"
for candidate in {{ console_service_caddy_conf_dir }}/console*.caddy {{ console_service_caddy_conf_dir }}/console*.caddy.bak*; do
if [ "$candidate" != "$current" ]; then
rm -f "$candidate"
fi
done
args:
executable: /bin/bash
changed_when: true
when: console_service_manage_caddy | bool
- name: Validate Caddy config after updating console fragment
ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile
changed_when: false
when:
- console_service_manage_caddy | bool
- not ansible_check_mode
- name: Reload Caddy after updating console fragment
ansible.builtin.service:
name: caddy
state: reloaded
when:
- console_service_manage_caddy | bool
- not ansible_check_mode
- name: Show console compose status
ansible.builtin.command: >-
docker compose

View File

@ -1,28 +1,7 @@
{$PRIMARY_DOMAIN}, {$SECONDARY_DOMAIN} {
{{ console_service_domain }} {
encode zstd gzip
@secondary host {$SECONDARY_DOMAIN}
redir @secondary https://{$PRIMARY_DOMAIN}{uri} permanent
handle_path /_next/static/* {
root * /srv
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@public_assets {
file {
root /srv/public
try_files {path}
}
}
handle @public_assets {
root * /srv/public
header Cache-Control "public, max-age=3600"
file_server
}
reverse_proxy dashboard:3000 {
reverse_proxy 127.0.0.1:{{ console_service_port }} {
header_up Host {host}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}

View File

@ -22,35 +22,16 @@ services:
environment:
NODE_ENV: production
PORT: {{ console_service_port | quote }}
ports:
- "127.0.0.1:{{ console_service_port }}:{{ console_service_port }}"
volumes:
- frontend_static:/app/dashboard/.next/static:ro
networks:
- frontend
caddy:
image: caddy:2.10-alpine
restart: unless-stopped
depends_on:
- dashboard
ports:
- "80:80"
- "443:443"
environment:
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime}
SECONDARY_DOMAIN: ${SECONDARY_DOMAIN:?set SECONDARY_DOMAIN in .env.runtime}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- frontend_static:/srv:ro
- caddy_data:/data
- caddy_config:/config
networks:
- frontend
networks:
frontend:
driver: bridge
volumes:
frontend_static:
caddy_data:
caddy_config:

View File

@ -1,6 +1,6 @@
FRONTEND_IMAGE={{ console_service_frontend_image }}
PRIMARY_DOMAIN={{ console_service_primary_domain }}
SECONDARY_DOMAIN={{ console_service_secondary_domain }}
PRIMARY_DOMAIN={{ console_service_domain }}
SECONDARY_DOMAIN={{ console_service_domain }}
NODE_ENV={{ console_service_node_env }}
PORT={{ console_service_port }}
RUNTIME_ENV={{ console_service_runtime_env }}

View File

@ -1,6 +1,8 @@
---
# Mail server firewall configuration
firewall_manage_ufw: true
# SSH port (always allow - most important!)
# Note: SSH is allowed FIRST to ensure you don't lock yourself out
# This should typically be customized to your actual SSH port (e.g., 2222)

View File

@ -1,8 +1,16 @@
---
- name: Detect UFW binary
ansible.builtin.stat:
path: /usr/sbin/ufw
register: firewall_ufw_binary
- name: Install UFW
apt:
name: ufw
state: present
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Set default policies
ufw:
@ -11,6 +19,9 @@
loop:
- { policy: 'deny', direction: 'incoming' }
- { policy: 'allow', direction: 'outgoing' }
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Allow essential ports (SSH first!)
ufw:
@ -19,6 +30,9 @@
rule: allow
comment: "{{ item.comment }}"
loop: "{{ essential_ports }}"
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Allow necessary mail ports
ufw:
@ -27,6 +41,9 @@
rule: allow
comment: "{{ item.comment }}"
loop: "{{ mail_ports }}"
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Allow LMTP from private networks
ufw:
@ -36,6 +53,9 @@
from_ip: "{{ item }}"
comment: 'LMTP private'
loop: "{{ lmtp_private_networks }}"
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Deny plaintext ports
ufw:
@ -44,20 +64,32 @@
rule: deny
comment: "{{ item.comment }}"
loop: "{{ denied_ports }}"
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Enable UFW
ufw:
state: enabled
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Get UFW numbered status
command: ufw status numbered
register: ufw_numbered
changed_when: false
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Get UFW status verbose
command: ufw status verbose
register: ufw_status
changed_when: false
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Display UFW status
debug:
@ -65,6 +97,9 @@
🔥 UFW Firewall Status
====================
{{ ufw_status.stdout }}
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Display configured mail rules
debug:
@ -96,3 +131,15 @@
- SSH port {{ ssh_port }} is open - ensure you use key-based authentication
- Default policy: deny all incoming, allow all outgoing
- Only open ports that are absolutely necessary
when:
- firewall_manage_ufw | bool
- firewall_ufw_binary.stat.exists | default(false)
- name: Explain why UFW tasks were skipped
ansible.builtin.debug:
msg: >-
Skipping UFW management on {{ inventory_hostname }} because
firewall_manage_ufw={{ firewall_manage_ufw | bool }}
and ufw_present={{ firewall_ufw_binary.stat.exists | default(false) }}.
when:
- not (firewall_manage_ufw | bool and (firewall_ufw_binary.stat.exists | default(false)))

View File

@ -1,97 +1,5 @@
---
cloudflare_dns_records:
- type: A
name: vps-rag-server.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: vps-accounts.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: vps-preview-accounts.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: docs.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: x-scope-hub.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: x-ops-agent.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: x-cloud-flow.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: accounts-preview.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: acp-server.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: xworkmate-bridge.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: CNAME
name: console-8fa9cd3-contabo.svc.plus
content: jp-xhttp-contabo.svc.plus
ttl: 1
proxied: false
- type: A
name: jp-xhttp-contabo.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: CNAME
name: console.svc.plus
content: console-8fa9cd3-contabo.svc.plus
ttl: 1
proxied: false
- type: CNAME
name: rag-server.svc.plus
content: vps-rag-server.svc.plus
ttl: 1
proxied: false
- type: CNAME
name: accounts.svc.plus
content: vps-accounts.svc.plus
ttl: 1
proxied: false
- type: A
name: vps-api.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: CNAME
name: api.svc.plus
content: vps-api.svc.plus
ttl: 1
proxied: false
- type: CNAME
name: preview-accounts.svc.plus
content: vps-preview-accounts.svc.plus
ttl: 1
proxied: false
- type: CNAME
name: zitadel.svc.plus
content: jp-xhttp-contabo.svc.plus
ttl: 1
proxied: false
cloudflare_dns_default_source_hosts:
- cn_front_host
- jp_xhttp_contabo_host
- tky_proxy_host