Compare commits
2 Commits
df1dd39197
...
085654d017
| Author | SHA1 | Date | |
|---|---|---|---|
|
085654d017
|
|||
|
5fe7b4edeb
|
63
CLAUDE.md
63
CLAUDE.md
@@ -1,63 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
## Session Start
|
|
||||||
|
|
||||||
Check `docs/summaries/` for a handoff file. If one exists, read it and the files it references — not all summaries. State: what you understand the project state to be, what you plan to do, and open questions.
|
|
||||||
|
|
||||||
If no handoff exists, determine session type before proceeding:
|
|
||||||
- **Quick task**: single-session, self-contained work (adding a playbook, fixing a role, configuring a service) → proceed without setup overhead
|
|
||||||
- **Sustained work**: multi-session project or significant design work → ask: what is the goal and what is the target deliverable
|
|
||||||
|
|
||||||
## Identity
|
|
||||||
|
|
||||||
You work with Pat, a Senior Solutions Architect at Red Hat building automation for a HomeLab. Expert-level Ansible knowledge — do not explain Ansible basics.
|
|
||||||
|
|
||||||
## Project
|
|
||||||
|
|
||||||
**Repo:** Ansible playbooks and roles managing a full HomeLab — Proxmox, OPNsense, OpenShift (SNO), AAP, Satellite, Gitea, and services.
|
|
||||||
**Inventory:** `/home/ptoal/Dev/inventories/toallab-inventory/static.yml`
|
|
||||||
**Run locally:** `ansible-navigator run playbooks/<name>.yml --mode stdout`
|
|
||||||
**Run with extra vars:** `ansible-navigator run playbooks/<name>.yml --mode stdout -e key=value`
|
|
||||||
**Lint:** `ansible-navigator lint playbooks/ --mode stdout`
|
|
||||||
**Collections:** `ansible-galaxy collection install -r collections/requirements.yml`
|
|
||||||
**Production:** playbooks run via AAP — do not refer to AWX
|
|
||||||
|
|
||||||
Load `docs/context/project-structure.md` when working on playbooks or roles.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
1. Do not mix unrelated project contexts in one session.
|
|
||||||
2. For sustained work: write state to disk after completing meaningful work. Use templates from `templates/claude-templates.md`. Include: decisions with rationale, exact numbers, file paths, open items.
|
|
||||||
3. For sustained work: before compaction or session end, write to disk — every number, every decision with rationale, every open question, every file path, exact next action.
|
|
||||||
4. For sustained work: when switching work types (development → documentation → review), write a handoff to `docs/summaries/handoff-[date]-[topic].md` and suggest a new session.
|
|
||||||
5. Do not silently resolve open questions. Mark them OPEN or ASSUMED.
|
|
||||||
6. Do not bulk-read documents. Process one at a time: read, summarize to disk, release from context before reading next. For the detailed protocol, read `docs/context/processing-protocol.md`.
|
|
||||||
7. Sub-agent returns must be structured, not free-form prose. Use output contracts from `templates/claude-templates.md`.
|
|
||||||
|
|
||||||
## Where Things Live
|
|
||||||
|
|
||||||
- `templates/claude-templates.md` — summary, handoff, decision, analysis, task, output contract templates (read on demand)
|
|
||||||
- `docs/summaries/` — active session state (latest handoff + decision records + source summaries)
|
|
||||||
- `docs/context/` — reusable domain knowledge, loaded only when relevant
|
|
||||||
- `project-structure.md` — playbook inventory, roles, collections, infrastructure map
|
|
||||||
- `processing-protocol.md` — full document processing steps
|
|
||||||
- `archive-rules.md` — summary lifecycle and file archival rules
|
|
||||||
- `subagent-rules.md` — when to use subagents vs. main agent
|
|
||||||
- `.claude/agents/` — specialized subagents (ansible-idempotency-reviewer — use before adding tasks or before production runs)
|
|
||||||
- `playbooks/` — main Ansible playbooks
|
|
||||||
- `roles/` — custom and external Ansible roles
|
|
||||||
- `collections/` — `requirements.yml` only; installed collections in `collections/ansible_collections/`
|
|
||||||
- `docs/archive/` — processed raw files. Do not read unless explicitly told.
|
|
||||||
- `output/deliverables/` — final outputs
|
|
||||||
|
|
||||||
For cross-project user preferences, recurring constraints, or tool preferences: use Claude Code's native memory system, not `docs/summaries/`.
|
|
||||||
|
|
||||||
## Error Recovery
|
|
||||||
|
|
||||||
If context degrades or auto-compact fires unexpectedly: write current state to `docs/summaries/recovery-[date].md`, tell the user what may have been lost, suggest a fresh session.
|
|
||||||
|
|
||||||
## Before Delivering Output
|
|
||||||
|
|
||||||
Verify: exact numbers preserved, open questions marked OPEN, output matches what was requested (not assumed), no Ansible idempotency regressions introduced.
|
|
||||||
|
|
||||||
All Ansible files (playbooks, task files, templates, vars) must end with a trailing newline.
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
# Inventory - override with -i or ANSIBLE_INVENTORY env var
|
# Inventory - override with -i or ANSIBLE_INVENTORY env var
|
||||||
inventory = /home/ptoal/Dev/inventories/toallab-inventory/static.yml
|
inventory = /home/ptoal/Dev/inventories/toallab-inventory/static.yml
|
||||||
|
jinja2_native = True
|
||||||
|
|
||||||
|
|
||||||
# Role and collection paths
|
# Role and collection paths
|
||||||
roles_path = roles
|
roles_path = roles
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
# Session Handoff: OpenClaw Deployment + VM Role Refactor
|
|
||||||
**Date:** 2026-03-29
|
|
||||||
**Session Focus:** Extract SNO VM creation into its own role; build new OpenClaw playbook with Signal channel and security stack
|
|
||||||
**Context Usage at Handoff:** ~60%
|
|
||||||
|
|
||||||
## What Was Accomplished
|
|
||||||
|
|
||||||
1. **Refactored SNO VM deployment into `proxmox_vm` role** → `roles/proxmox_vm/`
|
|
||||||
2. **Removed `create_vm.yml` from `sno_deploy` role** → `roles/sno_deploy/tasks/create_vm.yml` deleted
|
|
||||||
3. **Updated `deploy_openshift.yml` Play 1** to use `role: proxmox_vm` directly
|
|
||||||
4. **Created `roles/openclaw/`** — full role for OpenClaw installation and Signal channel
|
|
||||||
5. **Created `playbooks/deploy_openclaw.yml`** — 3-play pipeline: VM creation → SSH wait → install
|
|
||||||
|
|
||||||
## Files Created or Modified
|
|
||||||
|
|
||||||
| File Path | Action | Description |
|
|
||||||
|-----------|--------|-------------|
|
|
||||||
| `roles/proxmox_vm/tasks/main.yml` | Created | VM creation tasks moved from sno_deploy/tasks/create_vm.yml |
|
|
||||||
| `roles/proxmox_vm/defaults/main.yml` | Created | Proxmox connection + VM spec defaults |
|
|
||||||
| `roles/proxmox_vm/meta/main.yml` | Created | Role metadata |
|
|
||||||
| `roles/sno_deploy/tasks/create_vm.yml` | Deleted | Moved to proxmox_vm role |
|
|
||||||
| `roles/sno_deploy/defaults/main.yml` | Modified | Removed `sno_pvc_disk_gb` (VM-only, now in proxmox_vm) |
|
|
||||||
| `roles/sno_deploy/meta/argument_specs.yml` | Modified | Removed VM-creation-only entries |
|
|
||||||
| `playbooks/deploy_openshift.yml` | Modified | Play 1 now uses `role: proxmox_vm` |
|
|
||||||
| `roles/openclaw/defaults/main.yml` | Created | Role-scoped defaults only (no proxmox vars) |
|
|
||||||
| `roles/openclaw/meta/main.yml` | Created | Role metadata |
|
|
||||||
| `roles/openclaw/handlers/main.yml` | Created | Reload systemd + restart openclaw |
|
|
||||||
| `roles/openclaw/tasks/main.yml` | Created | Orchestrates security → install → signal |
|
|
||||||
| `roles/openclaw/tasks/security.yml` | Created | UFW + rootless Podman |
|
|
||||||
| `roles/openclaw/tasks/install.yml` | Created | User + Node.js + OpenClaw binary + systemd service |
|
|
||||||
| `roles/openclaw/tasks/signal.yml` | Created | signal-cli install + registration reminder |
|
|
||||||
| `roles/openclaw/templates/openclaw-config.yaml.j2` | Created | OpenClaw config (model provider + Signal channel) |
|
|
||||||
| `roles/openclaw/templates/openclaw.service.j2` | Created | Hardened systemd unit |
|
|
||||||
| `playbooks/deploy_openclaw.yml` | Created | Full deployment playbook |
|
|
||||||
|
|
||||||
## Decisions Made This Session
|
|
||||||
|
|
||||||
- **DR-1: `proxmox_vm` role keeps `sno_*` variable names** BECAUSE renaming would break existing host_vars and SNO playbook — STATUS: confirmed
|
|
||||||
- **DR-2: `proxmox_vm` defaults duplicated in `sno_deploy`** BECAUSE Play 4 (install.yml) runs in a separate play and cannot inherit defaults from Play 1's role — STATUS: confirmed
|
|
||||||
- **DR-3: No Tailscale** BECAUSE OPNsense firewall provides perimeter security; UFW on VM is defense-in-depth only — STATUS: confirmed
|
|
||||||
- **DR-4: Rootless Podman instead of Docker CE** for agent sandbox isolation — `podman-docker` shim provides docker CLI compatibility; `DOCKER_HOST` points to user Podman socket — STATUS: confirmed
|
|
||||||
- **DR-5: `openclaw` user is non-system (`system: false`)** BECAUSE rootless Podman requires `/etc/subuid`+`/etc/subgid` entries, which Ubuntu only creates for non-system users — STATUS: confirmed
|
|
||||||
- **DR-6: VM spec vars live in playbook Play 1 `vars:` block** (not in `openclaw` role defaults) BECAUSE they're only used in VM creation, not in the role itself — STATUS: confirmed
|
|
||||||
|
|
||||||
## Key Numbers
|
|
||||||
|
|
||||||
- OpenClaw gateway port: **18789**
|
|
||||||
- signal-cli version: **0.13.15** (pinned in `openclaw_signal_cli_version` default — verify this is current)
|
|
||||||
- Node.js version: **24** (`openclaw_node_version`)
|
|
||||||
- OpenClaw VM defaults: **2 vCPU, 4096 MB RAM, 40 GB disk**
|
|
||||||
- UFW: allow **22/tcp** (SSH) + **18789/tcp** (gateway); deny all else inbound
|
|
||||||
|
|
||||||
## Conditional Logic Established
|
|
||||||
|
|
||||||
- IF `openclaw_signal_enabled: true` THEN signal.yml runs AND Signal block appears in config template
|
|
||||||
- IF `openclaw_vm_ip == 'dhcp'` THEN DHCP cloud-init task runs, ELSE static IP task runs (requires `openclaw_vm_gateway` and `openclaw_vm_nameserver`)
|
|
||||||
- IF disk already imported (scsi0 present in VM config) THEN `qm importdisk` and disk attach tasks are skipped (idempotency guard)
|
|
||||||
|
|
||||||
## Exact State of Work in Progress
|
|
||||||
|
|
||||||
- `openclaw` role: complete and syntax-checked (no errors)
|
|
||||||
- `deploy_openclaw.yml`: syntax-checked — passes with expected warnings (inventory host not yet defined)
|
|
||||||
- Signal registration: **cannot be automated** — requires interactive QR scan or SMS captcha. Tasks print instructions; user must run manually post-deploy.
|
|
||||||
|
|
||||||
## Open Questions Requiring User Input
|
|
||||||
|
|
||||||
- [ ] What inventory hostname/group for the OpenClaw VM? Currently hardcoded to `openclaw.toal.ca` in playbook `hosts:` — confirm or change
|
|
||||||
- [ ] What `openclaw_vm_vnet` should be used? Defaulted to `lan` — confirm VNet name in Proxmox
|
|
||||||
- [ ] Static IP or DHCP for the OpenClaw VM? (`openclaw_vm_ip` default is `dhcp`)
|
|
||||||
- [ ] Which phone number to use for Signal? Dedicated bot number recommended (registration de-authenticates the main Signal app on that number)
|
|
||||||
- [ ] Confirm `signal-cli` version **0.13.15** is the desired version — check https://github.com/AsamK/signal-cli/releases
|
|
||||||
|
|
||||||
## Assumptions That Need Validation
|
|
||||||
|
|
||||||
- ASSUMED: OpenClaw config file format is YAML at `$OPENCLAW_STATE_DIR/config.yaml` — validate against actual OpenClaw docs/source; the config template (`openclaw-config.yaml.j2`) may need field name corrections
|
|
||||||
- ASSUMED: `DOCKER_HOST=unix:/run/user/<uid>/podman/podman.sock` is sufficient for OpenClaw to use Podman for sandboxes — validate that OpenClaw respects `DOCKER_HOST`
|
|
||||||
- ASSUMED: `openclaw` npm package name is correct — verify at https://www.npmjs.com/package/openclaw
|
|
||||||
- ASSUMED: Ubuntu 24.04 Noble cloud image at `https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img` — stable URL, but verify
|
|
||||||
|
|
||||||
## What NOT to Re-Read
|
|
||||||
|
|
||||||
- `roles/sno_deploy/tasks/install.yml` — already reviewed this session; no changes made
|
|
||||||
- `roles/sno_deploy/tasks/create_vm.yml` — deleted; content now in `roles/proxmox_vm/tasks/main.yml`
|
|
||||||
|
|
||||||
## Files to Load Next Session
|
|
||||||
|
|
||||||
- `playbooks/deploy_openclaw.yml` — needed to review/run the playbook
|
|
||||||
- `roles/openclaw/tasks/install.yml` — needed if adjusting OpenClaw install steps
|
|
||||||
- `roles/openclaw/templates/openclaw-config.yaml.j2` — needed if config format needs correction
|
|
||||||
- `roles/openclaw/tasks/signal.yml` — needed if adjusting Signal setup
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
---
|
|
||||||
# Deploy OpenClaw AI Gateway on a Proxmox VM
|
|
||||||
#
|
|
||||||
# OpenClaw: https://docs.openclaw.ai
|
|
||||||
# Ansible install docs: https://docs.openclaw.ai/install/ansible
|
|
||||||
# Signal channel docs: https://docs.openclaw.ai/channels/signal
|
|
||||||
#
|
|
||||||
# Prerequisites:
|
|
||||||
# Inventory host: openclaw.toal.ca (in group 'openclaw')
|
|
||||||
# host_vars required:
|
|
||||||
# openclaw_vm_ssh_public_key — SSH public key injected via cloud-init
|
|
||||||
# openclaw_vm_ip — static IP or 'dhcp'
|
|
||||||
# openclaw_vm_gateway — required for static IP
|
|
||||||
# openclaw_vm_vnet — Proxmox SDN VNet (e.g. lan)
|
|
||||||
#
|
|
||||||
# Vault secrets (1Password):
|
|
||||||
# vault_proxmox_token_secret — Proxmox API token
|
|
||||||
# vault_openclaw_api_key — Model provider API key (Anthropic, OpenAI, etc.)
|
|
||||||
# vault_openclaw_signal_phone — Signal account phone number (E.164, if Signal enabled)
|
|
||||||
#
|
|
||||||
# Security architecture:
|
|
||||||
# - OPNsense firewall provides perimeter security
|
|
||||||
# - UFW on VM: allow SSH (22) + gateway (18789); deny everything else inbound
|
|
||||||
# - Docker CE for agent sandbox isolation
|
|
||||||
# - Systemd hardening: NoNewPrivileges, PrivateTmp, ProtectSystem
|
|
||||||
#
|
|
||||||
# Signal channel MANUAL STEP required after deploy:
|
|
||||||
# sudo -i -u openclaw
|
|
||||||
# signal-cli link -n "OpenClaw" # scan QR with Signal app
|
|
||||||
# openclaw pairing approve signal
|
|
||||||
#
|
|
||||||
# Play order:
|
|
||||||
# Play 1: openclaw_create_vm — Create Ubuntu VM in Proxmox (cloud-init)
|
|
||||||
# Play 2: openclaw_wait — Wait for SSH to become available
|
|
||||||
# Play 3: openclaw_install — Install OpenClaw, security stack, Signal channel
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ansible-navigator run playbooks/deploy_openclaw.yml
|
|
||||||
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_create_vm
|
|
||||||
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install
|
|
||||||
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install,openclaw_signal
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Play 1: Create Ubuntu VM in Proxmox using cloud-init
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Create OpenClaw VM in Proxmox
|
|
||||||
hosts: openclaw.toal.ca
|
|
||||||
gather_facts: false
|
|
||||||
connection: local
|
|
||||||
tags: openclaw_create_vm
|
|
||||||
|
|
||||||
vars:
|
|
||||||
# Proxmox connection — override in host_vars if needed
|
|
||||||
proxmox_node: pve1
|
|
||||||
proxmox_api_user: ansible@pam
|
|
||||||
proxmox_api_token_id: ansible
|
|
||||||
proxmox_api_token_secret: "{{ vault_proxmox_token_secret }}"
|
|
||||||
proxmox_validate_certs: false
|
|
||||||
proxmox_storage: local-lvm
|
|
||||||
proxmox_iso_dir: /var/lib/vz/template/iso
|
|
||||||
# VM spec — override in host_vars for the openclaw inventory host
|
|
||||||
openclaw_vm_name: openclaw
|
|
||||||
openclaw_vm_id: 0
|
|
||||||
openclaw_vm_cpu: 2
|
|
||||||
openclaw_vm_memory_mb: 4096
|
|
||||||
openclaw_vm_disk_gb: 40
|
|
||||||
openclaw_vm_vnet: lan
|
|
||||||
openclaw_vm_user: ubuntu
|
|
||||||
openclaw_vm_ssh_public_key: "" # required — set in host_vars
|
|
||||||
openclaw_vm_ip: dhcp # set to x.x.x.x for static
|
|
||||||
openclaw_vm_prefix: 24
|
|
||||||
openclaw_vm_gateway: ""
|
|
||||||
openclaw_vm_nameserver: ""
|
|
||||||
openclaw_vm_cloud_image_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
|
|
||||||
openclaw_vm_cloud_image_filename: noble-server-cloudimg-amd64.img
|
|
||||||
# Computed
|
|
||||||
__openclaw_proxmox_api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
|
|
||||||
__openclaw_proxmox_api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Download Ubuntu 24.04 cloud image to Proxmox host
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "{{ openclaw_vm_cloud_image_url }}"
|
|
||||||
dest: "{{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }}"
|
|
||||||
mode: "0644"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
|
|
||||||
- name: Create VM definition
|
|
||||||
community.proxmox.proxmox_kvm:
|
|
||||||
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
||||||
api_user: "{{ proxmox_api_user }}"
|
|
||||||
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
||||||
api_token_id: "{{ proxmox_api_token_id }}"
|
|
||||||
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
||||||
validate_certs: "{{ proxmox_validate_certs }}"
|
|
||||||
node: "{{ proxmox_node }}"
|
|
||||||
vmid: "{{ openclaw_vm_id | default(omit, true) }}"
|
|
||||||
name: "{{ openclaw_vm_name }}"
|
|
||||||
cores: "{{ openclaw_vm_cpu }}"
|
|
||||||
memory: "{{ openclaw_vm_memory_mb }}"
|
|
||||||
cpu: host
|
|
||||||
machine: q35
|
|
||||||
bios: ovmf
|
|
||||||
efidisk0:
|
|
||||||
storage: "{{ proxmox_storage }}"
|
|
||||||
format: raw
|
|
||||||
efitype: 4m
|
|
||||||
pre_enrolled_keys: false
|
|
||||||
scsihw: virtio-scsi-single
|
|
||||||
net:
|
|
||||||
net0: "virtio,bridge={{ openclaw_vm_vnet }}"
|
|
||||||
boot: "order=scsi0"
|
|
||||||
onboot: true
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Retrieve VM info
|
|
||||||
community.proxmox.proxmox_vm_info:
|
|
||||||
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
||||||
api_user: "{{ proxmox_api_user }}"
|
|
||||||
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
||||||
api_token_id: "{{ proxmox_api_token_id }}"
|
|
||||||
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
||||||
validate_certs: "{{ proxmox_validate_certs }}"
|
|
||||||
node: "{{ proxmox_node }}"
|
|
||||||
name: "{{ openclaw_vm_name }}"
|
|
||||||
type: qemu
|
|
||||||
config: current
|
|
||||||
register: __openclaw_vm_info
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
- name: Set VM ID fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
openclaw_vm_id: "{{ __openclaw_vm_info.proxmox_vms[0].vmid }}"
|
|
||||||
cacheable: true
|
|
||||||
|
|
||||||
- name: Check if disk is already imported (scsi0 present in config)
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
__openclaw_disk_imported: "{{ __openclaw_vm_info.proxmox_vms[0].config.scsi0 is defined }}"
|
|
||||||
|
|
||||||
- name: Import cloud image as primary disk
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: >-
|
|
||||||
qm importdisk {{ openclaw_vm_id }}
|
|
||||||
{{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }}
|
|
||||||
{{ proxmox_storage }} --format raw
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: not __openclaw_disk_imported | bool
|
|
||||||
|
|
||||||
- name: Attach imported disk as scsi0
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "qm set {{ openclaw_vm_id }} --scsi0 {{ proxmox_storage }}:vm-{{ openclaw_vm_id }}-disk-0,iothread=1,cache=writeback"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: not __openclaw_disk_imported | bool
|
|
||||||
|
|
||||||
- name: Resize disk to configured size
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "qm disk resize {{ openclaw_vm_id }} scsi0 {{ openclaw_vm_disk_gb }}G"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: not __openclaw_disk_imported | bool
|
|
||||||
|
|
||||||
- name: Add cloud-init drive
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "qm set {{ openclaw_vm_id }} --ide2 {{ proxmox_storage }}:cloudinit"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: not __openclaw_disk_imported | bool
|
|
||||||
|
|
||||||
- name: Write SSH public key to temp file on Proxmox host
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ openclaw_vm_ssh_public_key }}"
|
|
||||||
dest: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub"
|
|
||||||
mode: "0600"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
no_log: false
|
|
||||||
|
|
||||||
- name: Configure cloud-init user and SSH key
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: >-
|
|
||||||
qm set {{ openclaw_vm_id }}
|
|
||||||
--ciuser {{ openclaw_vm_user }}
|
|
||||||
--sshkeys /tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
- name: Configure cloud-init network (static)
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: >-
|
|
||||||
qm set {{ openclaw_vm_id }}
|
|
||||||
--ipconfig0 ip={{ openclaw_vm_ip }}/{{ openclaw_vm_prefix }},gw={{ openclaw_vm_gateway }}
|
|
||||||
--nameserver {{ openclaw_vm_nameserver }}
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: openclaw_vm_ip != 'dhcp'
|
|
||||||
|
|
||||||
- name: Configure cloud-init network (DHCP)
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "qm set {{ openclaw_vm_id }} --ipconfig0 ip=dhcp"
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
changed_when: true
|
|
||||||
when: openclaw_vm_ip == 'dhcp'
|
|
||||||
|
|
||||||
- name: Start VM
|
|
||||||
community.proxmox.proxmox_kvm:
|
|
||||||
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
||||||
api_user: "{{ proxmox_api_user }}"
|
|
||||||
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
||||||
api_token_id: "{{ proxmox_api_token_id }}"
|
|
||||||
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
||||||
validate_certs: "{{ proxmox_validate_certs }}"
|
|
||||||
node: "{{ proxmox_node }}"
|
|
||||||
name: "{{ openclaw_vm_name }}"
|
|
||||||
state: started
|
|
||||||
|
|
||||||
- name: Remove temporary SSH key file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub"
|
|
||||||
state: absent
|
|
||||||
delegate_to: proxmox_host
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Play 2: Wait for VM to become reachable
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Wait for OpenClaw VM SSH
|
|
||||||
hosts: openclaw.toal.ca
|
|
||||||
gather_facts: false
|
|
||||||
tags: openclaw_create_vm
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Wait for SSH port
|
|
||||||
ansible.builtin.wait_for_connection:
|
|
||||||
timeout: 300
|
|
||||||
sleep: 10
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Play 3: Install OpenClaw, security stack, and Signal channel
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Install and configure OpenClaw
|
|
||||||
hosts: openclaw.toal.ca
|
|
||||||
gather_facts: true
|
|
||||||
become: true
|
|
||||||
tags: openclaw_install
|
|
||||||
|
|
||||||
roles:
|
|
||||||
- role: openclaw
|
|
||||||
@@ -333,8 +333,57 @@
|
|||||||
- never
|
- never
|
||||||
- sno_deploy_service_accounts
|
- sno_deploy_service_accounts
|
||||||
|
|
||||||
roles:
|
tasks:
|
||||||
- role: ocp_service_account
|
# ---- supabase-deployer ----
|
||||||
|
- name: Provision supabase-deployer ServiceAccount
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: ocp_service_account
|
||||||
|
vars:
|
||||||
|
ocp_service_account_name: supabase-deployer
|
||||||
|
ocp_service_account_namespace: supabase
|
||||||
|
ocp_service_account_cluster_role_rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["namespaces"]
|
||||||
|
verbs: ["get", "list", "create", "patch", "delete"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets", "configmaps", "services", "serviceaccounts",
|
||||||
|
"persistentvolumeclaims", "pods", "endpoints"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments", "statefulsets", "replicasets"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||||
|
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "escalate", "bind"]
|
||||||
|
- apiGroups: ["security.openshift.io"]
|
||||||
|
resources: ["securitycontextconstraints"]
|
||||||
|
verbs: ["get", "list", "use"]
|
||||||
|
- apiGroups: ["route.openshift.io"]
|
||||||
|
resources: ["routes", "routes/custom-host"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["cert-manager.io"]
|
||||||
|
resources: ["certificates", "clusterissuers", "issuers"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["networking.k8s.io"]
|
||||||
|
resources: ["ingresses"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["storage.k8s.io"]
|
||||||
|
resources: ["storageclasses"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
|
||||||
|
- name: Store supabase-deployer token in Vault
|
||||||
|
community.hashi_vault.vault_kv2_write:
|
||||||
|
path: toallab/supabase-deployer
|
||||||
|
engine_mount_point: kv
|
||||||
|
data:
|
||||||
|
token: "{{ __ocp_service_account_token }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
# ---- aap-deployer ----
|
||||||
|
- name: Provision aap-deployer ServiceAccount
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: ocp_service_account
|
||||||
|
vars:
|
||||||
ocp_service_account_name: aap-deployer
|
ocp_service_account_name: aap-deployer
|
||||||
ocp_service_account_namespace: aap
|
ocp_service_account_namespace: aap
|
||||||
ocp_service_account_cluster_role_rules:
|
ocp_service_account_cluster_role_rules:
|
||||||
@@ -359,3 +408,11 @@
|
|||||||
- apiGroups: ["aap.ansible.com"]
|
- apiGroups: ["aap.ansible.com"]
|
||||||
resources: ["ansibleautomationplatforms"]
|
resources: ["ansibleautomationplatforms"]
|
||||||
verbs: ["get", "list", "create", "patch", "watch"]
|
verbs: ["get", "list", "create", "patch", "watch"]
|
||||||
|
|
||||||
|
- name: Store aap-deployer token in Vault
|
||||||
|
community.hashi_vault.vault_kv2_write:
|
||||||
|
path: toallab/aap-deployer
|
||||||
|
engine_mount_point: kv
|
||||||
|
data:
|
||||||
|
token: "{{ __ocp_service_account_token }}"
|
||||||
|
no_log: true
|
||||||
|
|||||||
44
playbooks/deploy_supabase.yml
Normal file
44
playbooks/deploy_supabase.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
# Deploy Supabase on OpenShift (SNO) via the supabase-community Helm chart.
|
||||||
|
#
|
||||||
|
# Chart: https://github.com/supabase-community/supabase-kubernetes
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - cert-manager installed (deploy_openshift.yml --tags sno_deploy_certmanager)
|
||||||
|
# - NFS StorageClass available (deploy_openshift.yml --tags sno_deploy_nfs)
|
||||||
|
# - supabase-deployer ServiceAccount provisioned and token saved to 1Password:
|
||||||
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||||
|
# - vault_supabase_deployer_token set in host_vars (see hashivault_secrets.yml)
|
||||||
|
#
|
||||||
|
# All Supabase secrets (JWT, DB password, etc.) are auto-generated on first run
|
||||||
|
# and stored in Vault at supabase_vault_path. No manual secret setup required.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout
|
||||||
|
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_chart_version=0.0.9
|
||||||
|
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_db_storage_size=20Gi
|
||||||
|
|
||||||
|
- name: Deploy Supabase on OpenShift
|
||||||
|
hosts: sno.openshift.toal.ca
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
environment:
|
||||||
|
K8S_AUTH_HOST: "{{ supabase_k8s_api_url }}"
|
||||||
|
K8S_AUTH_API_KEY: "{{ vault_supabase_deployer_token }}"
|
||||||
|
K8S_AUTH_VERIFY_SSL: "false"
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Verify supabase-deployer token is available
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- vault_supabase_deployer_token is defined
|
||||||
|
- vault_supabase_deployer_token | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
vault_supabase_deployer_token is not set.
|
||||||
|
Provision the ServiceAccount with:
|
||||||
|
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||||
|
Then save the displayed token to 1Password and reference it as vault_supabase_deployer_token.
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: supabase
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
# OpenClaw service user
|
|
||||||
openclaw_user: openclaw
|
|
||||||
openclaw_group: openclaw
|
|
||||||
openclaw_home: /opt/openclaw
|
|
||||||
openclaw_state_dir: /opt/openclaw/.openclaw
|
|
||||||
openclaw_node_version: "24"
|
|
||||||
|
|
||||||
# Model provider
|
|
||||||
openclaw_model_provider: anthropic
|
|
||||||
openclaw_api_key: "{{ vault_openclaw_api_key }}"
|
|
||||||
|
|
||||||
# Signal channel
|
|
||||||
openclaw_signal_enabled: false
|
|
||||||
openclaw_signal_account: "{{ vault_openclaw_signal_phone | default('') }}"
|
|
||||||
openclaw_signal_cli_version: "0.13.15"
|
|
||||||
openclaw_signal_cli_path: /usr/local/bin/signal-cli
|
|
||||||
openclaw_signal_dm_policy: pairing
|
|
||||||
openclaw_signal_allow_from: [] # list of E.164 numbers permitted to DM
|
|
||||||
|
|
||||||
# Firewall
|
|
||||||
openclaw_ssh_port: 22
|
|
||||||
openclaw_gateway_port: 18789
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Reload systemd
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
|
|
||||||
- name: Restart openclaw
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: openclaw
|
|
||||||
state: restarted
|
|
||||||
listen: Restart openclaw
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
galaxy_info:
|
|
||||||
author: ptoal
|
|
||||||
description: Install and configure OpenClaw AI gateway on Ubuntu
|
|
||||||
license: MIT
|
|
||||||
min_ansible_version: "2.16"
|
|
||||||
platforms:
|
|
||||||
- name: Ubuntu
|
|
||||||
versions:
|
|
||||||
- noble
|
|
||||||
galaxy_tags:
|
|
||||||
- openclaw
|
|
||||||
- ai
|
|
||||||
- signal
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
---
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# System user and directories
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Create openclaw group
|
|
||||||
ansible.builtin.group:
|
|
||||||
name: "{{ openclaw_group }}"
|
|
||||||
system: false
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create openclaw user
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ openclaw_user }}"
|
|
||||||
group: "{{ openclaw_group }}"
|
|
||||||
home: "{{ openclaw_home }}"
|
|
||||||
shell: /sbin/nologin
|
|
||||||
system: false # must be non-system: subuid/subgid entries required for rootless Podman
|
|
||||||
create_home: true
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Get openclaw user UID
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "id -u {{ openclaw_user }}"
|
|
||||||
register: __openclaw_uid_result
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Set openclaw UID fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
__openclaw_uid: "{{ __openclaw_uid_result.stdout }}"
|
|
||||||
|
|
||||||
- name: Enable lingering for openclaw user
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "loginctl enable-linger {{ openclaw_user }}"
|
|
||||||
register: __openclaw_linger
|
|
||||||
changed_when: __openclaw_linger.rc == 0
|
|
||||||
|
|
||||||
- name: Enable rootless Podman socket for openclaw user
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: podman.socket
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
scope: user
|
|
||||||
become: true
|
|
||||||
become_user: "{{ openclaw_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ __openclaw_uid }}"
|
|
||||||
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ __openclaw_uid }}/bus"
|
|
||||||
|
|
||||||
- name: Create OpenClaw state directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ openclaw_state_dir }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ openclaw_user }}"
|
|
||||||
group: "{{ openclaw_group }}"
|
|
||||||
mode: "0750"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Node.js
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Add NodeSource apt signing key
|
|
||||||
ansible.builtin.apt_key:
|
|
||||||
url: "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Add NodeSource apt repository
|
|
||||||
ansible.builtin.apt_repository:
|
|
||||||
repo: "deb https://deb.nodesource.com/node_{{ openclaw_node_version }}.x nodistro main"
|
|
||||||
state: present
|
|
||||||
filename: nodesource
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: nodejs
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Install pnpm globally
|
|
||||||
community.general.npm:
|
|
||||||
name: pnpm
|
|
||||||
global: true
|
|
||||||
state: present
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# OpenClaw binary
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Install OpenClaw via npm
|
|
||||||
community.general.npm:
|
|
||||||
name: openclaw
|
|
||||||
global: true
|
|
||||||
state: "{{ 'latest' if openclaw_version == 'latest' else 'present' }}"
|
|
||||||
notify: Restart openclaw
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Configuration
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Template OpenClaw config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: openclaw-config.yaml.j2
|
|
||||||
dest: "{{ openclaw_state_dir }}/config.yaml"
|
|
||||||
owner: "{{ openclaw_user }}"
|
|
||||||
group: "{{ openclaw_group }}"
|
|
||||||
mode: "0640"
|
|
||||||
notify: Restart openclaw
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Systemd service with hardening
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Template openclaw systemd service
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: openclaw.service.j2
|
|
||||||
dest: /etc/systemd/system/openclaw.service
|
|
||||||
mode: "0644"
|
|
||||||
notify:
|
|
||||||
- Reload systemd
|
|
||||||
- Restart openclaw
|
|
||||||
|
|
||||||
- name: Enable and start openclaw service
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: openclaw
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Configure security (UFW, Tailscale, Docker)
|
|
||||||
ansible.builtin.include_tasks: security.yml
|
|
||||||
|
|
||||||
- name: Install OpenClaw
|
|
||||||
ansible.builtin.include_tasks: install.yml
|
|
||||||
|
|
||||||
- name: Configure Signal channel
|
|
||||||
ansible.builtin.include_tasks: signal.yml
|
|
||||||
when: openclaw_signal_enabled | bool
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# UFW firewall — defense-in-depth behind OPNsense perimeter
|
|
||||||
# Allows SSH and the OpenClaw gateway port; blocks everything else inbound
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Install UFW
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: ufw
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Set UFW default policies
|
|
||||||
community.general.ufw:
|
|
||||||
direction: "{{ item.direction }}"
|
|
||||||
policy: "{{ item.policy }}"
|
|
||||||
loop:
|
|
||||||
- { direction: incoming, policy: deny }
|
|
||||||
- { direction: outgoing, policy: allow }
|
|
||||||
- { direction: routed, policy: deny }
|
|
||||||
|
|
||||||
- name: Allow SSH
|
|
||||||
community.general.ufw:
|
|
||||||
rule: allow
|
|
||||||
port: "{{ openclaw_ssh_port | string }}"
|
|
||||||
proto: tcp
|
|
||||||
|
|
||||||
- name: Allow OpenClaw gateway port
|
|
||||||
community.general.ufw:
|
|
||||||
rule: allow
|
|
||||||
port: "{{ openclaw_gateway_port | string }}"
|
|
||||||
proto: tcp
|
|
||||||
|
|
||||||
- name: Enable UFW
|
|
||||||
community.general.ufw:
|
|
||||||
state: enabled
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Rootless Podman — used exclusively for agent sandbox isolation
|
|
||||||
# Runs as the openclaw user; no root daemon, no exposed sockets
|
|
||||||
# podman-docker provides a docker-compatible CLI shim for OpenClaw tooling
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
- name: Install Podman and dependencies
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- podman
|
|
||||||
- podman-docker
|
|
||||||
- uidmap
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# signal-cli — Java-based CLI bridge required by OpenClaw's Signal channel.
|
|
||||||
# Docs: https://docs.openclaw.ai/channels/signal
|
|
||||||
#
|
|
||||||
# MANUAL STEP REQUIRED after first deploy:
|
|
||||||
# Option A (link existing account):
|
|
||||||
# sudo -i -u openclaw
|
|
||||||
# signal-cli link -n "OpenClaw" # scan QR code with Signal app
|
|
||||||
#
|
|
||||||
# Option B (register dedicated number):
|
|
||||||
# sudo -i -u openclaw
|
|
||||||
# signal-cli -a {{ openclaw_signal_account }} register --captcha <token>
|
|
||||||
# signal-cli -a {{ openclaw_signal_account }} verify <sms-code>
|
|
||||||
#
|
|
||||||
# Then approve DM access:
|
|
||||||
# openclaw pairing approve signal
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
- name: Install Java runtime (required by signal-cli)
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: default-jre-headless
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Create signal-cli install directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /opt/signal-cli
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Download signal-cli archive
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: "https://github.com/AsamK/signal-cli/releases/download/v{{ openclaw_signal_cli_version }}/signal-cli-{{ openclaw_signal_cli_version }}-Linux.tar.gz"
|
|
||||||
dest: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}.tar.gz"
|
|
||||||
mode: "0644"
|
|
||||||
register: __openclaw_signal_cli_download
|
|
||||||
|
|
||||||
- name: Extract signal-cli
|
|
||||||
ansible.builtin.unarchive:
|
|
||||||
src: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}.tar.gz"
|
|
||||||
dest: /opt/signal-cli
|
|
||||||
remote_src: true
|
|
||||||
creates: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}/bin/signal-cli"
|
|
||||||
|
|
||||||
- name: Symlink signal-cli to PATH
|
|
||||||
ansible.builtin.file:
|
|
||||||
src: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}/bin/signal-cli"
|
|
||||||
dest: "{{ openclaw_signal_cli_path }}"
|
|
||||||
state: link
|
|
||||||
|
|
||||||
- name: Set ownership of signal-cli data directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ openclaw_home }}/.local/share/signal-cli"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ openclaw_user }}"
|
|
||||||
group: "{{ openclaw_group }}"
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Display Signal registration reminder
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg:
|
|
||||||
- "*** MANUAL STEP REQUIRED: Signal account not yet registered ***"
|
|
||||||
- "Switch to the openclaw user and register signal-cli:"
|
|
||||||
- " sudo -i -u {{ openclaw_user }}"
|
|
||||||
- " # Option A — link existing account (recommended):"
|
|
||||||
- " signal-cli link -n 'OpenClaw' # scan QR with Signal app"
|
|
||||||
- " # Option B — register a dedicated number:"
|
|
||||||
- " signal-cli -a {{ openclaw_signal_account }} register --captcha <token>"
|
|
||||||
- " signal-cli -a {{ openclaw_signal_account }} verify <sms-code>"
|
|
||||||
- "After registration, approve pairing:"
|
|
||||||
- " openclaw pairing approve signal"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# OpenClaw configuration — managed by Ansible, do not edit manually
|
|
||||||
# Ref: https://docs.openclaw.ai
|
|
||||||
|
|
||||||
gateway:
|
|
||||||
port: 18789
|
|
||||||
# Gateway binds localhost only; Tailscale is the remote access path
|
|
||||||
|
|
||||||
providers:
|
|
||||||
- type: {{ openclaw_model_provider }}
|
|
||||||
apiKey: "{{ openclaw_api_key }}"
|
|
||||||
|
|
||||||
{% if openclaw_signal_enabled | bool %}
|
|
||||||
channels:
|
|
||||||
signal:
|
|
||||||
account: "{{ openclaw_signal_account }}"
|
|
||||||
cliPath: "{{ openclaw_signal_cli_path }}"
|
|
||||||
dmPolicy: {{ openclaw_signal_dm_policy }}
|
|
||||||
{% if openclaw_signal_allow_from | length > 0 %}
|
|
||||||
allowFrom:
|
|
||||||
{% for number in openclaw_signal_allow_from %}
|
|
||||||
- "{{ number }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=OpenClaw AI Gateway
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User={{ openclaw_user }}
|
|
||||||
Group={{ openclaw_group }}
|
|
||||||
WorkingDirectory={{ openclaw_home }}
|
|
||||||
|
|
||||||
Environment=OPENCLAW_STATE_DIR={{ openclaw_state_dir }}
|
|
||||||
Environment=OPENCLAW_CONFIG_PATH={{ openclaw_state_dir }}/config.yaml
|
|
||||||
Environment=DOCKER_HOST=unix:/run/user/{{ __openclaw_uid }}/podman/podman.sock
|
|
||||||
Environment=XDG_RUNTIME_DIR=/run/user/{{ __openclaw_uid }}
|
|
||||||
|
|
||||||
ExecStart=/usr/bin/openclaw gateway run
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
# Hardening
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ReadWritePaths={{ openclaw_state_dir }} {{ openclaw_home }}
|
|
||||||
ProtectHome=read-only
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
72
roles/supabase/defaults/main.yml
Normal file
72
roles/supabase/defaults/main.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
# --- Debugging ---
|
||||||
|
# Set to false to disable no_log and expose secret values in task output.
|
||||||
|
# Never set false in production or shared environments.
|
||||||
|
supabase_no_log: true
|
||||||
|
|
||||||
|
# --- Vault ---
|
||||||
|
# KV2 path where Supabase secrets are stored and retrieved.
|
||||||
|
# Run with: op run --env-file=~/.ansible.zshenv -- ansible-navigator ...
|
||||||
|
# so that VAULT_ADDR / VAULT_TOKEN are injected from 1Password.
|
||||||
|
supabase_vault_mount: kv
|
||||||
|
supabase_vault_path: supabase
|
||||||
|
|
||||||
|
# --- Helm release ---
|
||||||
|
supabase_release_name: supabase
|
||||||
|
supabase_namespace: supabase
|
||||||
|
supabase_chart_repo_url: https://supabase-community.github.io/supabase-kubernetes
|
||||||
|
supabase_chart_version: "" # blank = latest
|
||||||
|
|
||||||
|
# --- External access ---
|
||||||
|
# Studio and API gateway are both served through Kong on this hostname.
|
||||||
|
# No generic default — must be set in host_vars or group_vars.
|
||||||
|
supabase_studio_host: ""
|
||||||
|
supabase_tls_cluster_issuer: ""
|
||||||
|
|
||||||
|
# --- Storage ---
|
||||||
|
# Empty string uses the cluster's default StorageClass.
|
||||||
|
supabase_storage_class: ""
|
||||||
|
supabase_db_storage_size: 10Gi
|
||||||
|
supabase_functions_storage_size: 1Gi
|
||||||
|
supabase_imgproxy_storage_size: 1Gi
|
||||||
|
supabase_minio_storage_size: 20Gi
|
||||||
|
|
||||||
|
# --- Database ---
|
||||||
|
supabase_db_name: postgres
|
||||||
|
|
||||||
|
# --- Studio branding ---
|
||||||
|
supabase_org_name: ""
|
||||||
|
supabase_project_name: ""
|
||||||
|
|
||||||
|
# --- SMTP (set supabase_smtp_enabled: true to configure email auth) ---
|
||||||
|
supabase_smtp_enabled: false
|
||||||
|
supabase_smtp_host: ""
|
||||||
|
supabase_smtp_port: 587
|
||||||
|
supabase_smtp_admin_email: ""
|
||||||
|
supabase_smtp_sender_name: Supabase
|
||||||
|
|
||||||
|
# --- Internal component name overrides ---
|
||||||
|
# These become predictable Kubernetes resource names (service, SA, etc.)
|
||||||
|
# Must match deployment.*.fullnameOverride values passed to the chart
|
||||||
|
supabase_db_fullname: supabase-db
|
||||||
|
supabase_kong_fullname: supabase-kong
|
||||||
|
supabase_functions_fullname: supabase-functions
|
||||||
|
supabase_vector_fullname: supabase-vector
|
||||||
|
|
||||||
|
# --- Docker Hub pull secret (optional) ---
|
||||||
|
# Set to avoid anonymous rate limiting on docker.io images (kong, timberio/vector, etc.)
|
||||||
|
# vault_supabase_dockerhub_token must be set in host_vars when enabled.
|
||||||
|
supabase_dockerhub_enabled: false
|
||||||
|
supabase_dockerhub_username: ""
|
||||||
|
|
||||||
|
# --- External DB access ---
|
||||||
|
# External hostname (or IP) clients use to reach the postgres NodePort.
|
||||||
|
# Must be set in host_vars; no safe generic default.
|
||||||
|
supabase_db_external_host: ""
|
||||||
|
# NodePort assigned for external postgres access (30000-32767).
|
||||||
|
supabase_db_nodeport: 30432
|
||||||
|
|
||||||
|
# --- Wait ---
|
||||||
|
# helm wait is disabled — image pulls from docker.io can take longer than any
|
||||||
|
# reasonable helm timeout. Pod readiness is checked separately below.
|
||||||
|
supabase_wait_timeout: 600
|
||||||
133
roles/supabase/meta/argument_specs.yml
Normal file
133
roles/supabase/meta/argument_specs.yml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy Supabase on OpenShift via the supabase-community Helm chart
|
||||||
|
description:
|
||||||
|
- Deploys the full Supabase stack (Studio, Kong, Auth, REST, Realtime, Storage,
|
||||||
|
Meta, Analytics, Functions, ImgProxy, Vector) to an OpenShift cluster.
|
||||||
|
- All secrets are created as Kubernetes Secrets and referenced via secretRef
|
||||||
|
so secret material never appears in Helm values.
|
||||||
|
- OpenShift SCCs are granted before the Helm install so pods start with
|
||||||
|
correct permissions (anyuid for postgres, privileged for vector).
|
||||||
|
- An OCP Route with edge TLS termination is created, backed by a cert-manager
|
||||||
|
Certificate against the configured ClusterIssuer.
|
||||||
|
- >-
|
||||||
|
Vault variables (vault_supabase_*) must be defined in host_vars before running.
|
||||||
|
Generate JWT values at https://supabase.com/docs/guides/self-hosting/docker
|
||||||
|
Generate secrets with openssl rand -base64 64
|
||||||
|
options:
|
||||||
|
supabase_release_name:
|
||||||
|
description: Helm release name.
|
||||||
|
type: str
|
||||||
|
default: supabase
|
||||||
|
supabase_namespace:
|
||||||
|
description: Kubernetes namespace for all Supabase resources.
|
||||||
|
type: str
|
||||||
|
default: supabase
|
||||||
|
supabase_chart_repo_url:
|
||||||
|
description: URL of the supabase-community Helm chart repository.
|
||||||
|
type: str
|
||||||
|
default: https://supabase-community.github.io/supabase-kubernetes
|
||||||
|
supabase_chart_version:
|
||||||
|
description: Chart version to install. Empty string means latest.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_studio_host:
|
||||||
|
description: >-
|
||||||
|
FQDN for the Studio dashboard and Kong API gateway Route.
|
||||||
|
Defaults to supabase.apps.<cluster>.<domain>.
|
||||||
|
type: str
|
||||||
|
supabase_tls_cluster_issuer:
|
||||||
|
description: cert-manager ClusterIssuer name for TLS certificate provisioning.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_storage_class:
|
||||||
|
description: StorageClass name for all PersistentVolumeClaims (must support ReadWriteOnce).
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_db_storage_size:
|
||||||
|
description: Size of the PostgreSQL PVC.
|
||||||
|
type: str
|
||||||
|
default: 10Gi
|
||||||
|
supabase_functions_storage_size:
|
||||||
|
description: Size of the Edge Functions PVC.
|
||||||
|
type: str
|
||||||
|
default: 1Gi
|
||||||
|
supabase_imgproxy_storage_size:
|
||||||
|
description: Size of the ImgProxy PVC.
|
||||||
|
type: str
|
||||||
|
default: 1Gi
|
||||||
|
supabase_minio_storage_size:
|
||||||
|
description: Size of the MinIO (object storage) PVC.
|
||||||
|
type: str
|
||||||
|
default: 20Gi
|
||||||
|
supabase_db_name:
|
||||||
|
description: PostgreSQL database name.
|
||||||
|
type: str
|
||||||
|
default: postgres
|
||||||
|
supabase_org_name:
|
||||||
|
description: Default organisation name shown in Supabase Studio.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_project_name:
|
||||||
|
description: Default project name shown in Supabase Studio.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_smtp_enabled:
|
||||||
|
description: >-
|
||||||
|
Enable SMTP for auth email. When false, auto-confirm is enabled so
|
||||||
|
sign-ups do not require email verification.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
supabase_smtp_host:
|
||||||
|
description: SMTP server hostname.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_smtp_port:
|
||||||
|
description: SMTP server port.
|
||||||
|
type: int
|
||||||
|
default: 587
|
||||||
|
supabase_smtp_admin_email:
|
||||||
|
description: Admin email address used as From header.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_smtp_sender_name:
|
||||||
|
description: Display name used in outgoing email.
|
||||||
|
type: str
|
||||||
|
default: Supabase
|
||||||
|
supabase_db_fullname:
|
||||||
|
description: >-
|
||||||
|
fullnameOverride for the db component. Determines the ServiceAccount
|
||||||
|
name that receives the anyuid SCC — must match deployment.db.fullnameOverride
|
||||||
|
passed to the chart.
|
||||||
|
type: str
|
||||||
|
default: supabase-db
|
||||||
|
supabase_kong_fullname:
|
||||||
|
description: >-
|
||||||
|
fullnameOverride for the kong component. Determines the Service name
|
||||||
|
that the OCP Route targets.
|
||||||
|
type: str
|
||||||
|
default: supabase-kong
|
||||||
|
supabase_vector_fullname:
|
||||||
|
description: >-
|
||||||
|
fullnameOverride for the vector component. Determines the ServiceAccount
|
||||||
|
name that receives the privileged SCC.
|
||||||
|
type: str
|
||||||
|
default: supabase-vector
|
||||||
|
supabase_db_external_host:
|
||||||
|
description: >-
|
||||||
|
External hostname or IP used in the postgres_url written to Vault and
|
||||||
|
by clients connecting via the NodePort Service. Must be set in host_vars.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
supabase_db_nodeport:
|
||||||
|
description: >-
|
||||||
|
NodePort assigned for external PostgreSQL access (30000-32767).
|
||||||
|
Used both when creating the NodePort Service and when constructing the
|
||||||
|
postgres_url written to Vault.
|
||||||
|
type: int
|
||||||
|
default: 30432
|
||||||
|
supabase_wait_timeout:
|
||||||
|
description: Seconds to wait for deployments to become ready.
|
||||||
|
type: int
|
||||||
|
default: 600
|
||||||
13
roles/supabase/meta/main.yml
Normal file
13
roles/supabase/meta/main.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
role_name: supabase
|
||||||
|
author: ptoal
|
||||||
|
description: Deploy Supabase on OpenShift via the supabase-community Helm chart
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.14"
|
||||||
|
platforms:
|
||||||
|
- name: EL
|
||||||
|
versions:
|
||||||
|
- "9"
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
506
roles/supabase/tasks/main.yml
Normal file
506
roles/supabase/tasks/main.yml
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
---
|
||||||
|
# Deploy Supabase on OpenShift via the supabase-community Helm chart.
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - KUBECONFIG set in environment (or ocp_kubeconfig host var)
|
||||||
|
# - cert-manager installed with a ClusterIssuer matching supabase_tls_cluster_issuer
|
||||||
|
# - A ReadWriteOnce-capable StorageClass set via supabase_storage_class
|
||||||
|
# - Vault variables defined (see meta/argument_specs.yml for the full list)
|
||||||
|
#
|
||||||
|
# Architecture:
|
||||||
|
# - Kong API gateway is the single ingress point (Route: supabase_studio_host)
|
||||||
|
# - Studio dashboard is served through Kong
|
||||||
|
# - DB runs as root — anyuid SCC granted to the db ServiceAccount
|
||||||
|
# - Vector reads /var/log/pods — privileged SCC granted to vector SA
|
||||||
|
# - All secrets are created as K8s Secrets and referenced via secretRef
|
||||||
|
#
|
||||||
|
# OpenShift SCC notes:
|
||||||
|
# - supabase-db ServiceAccount → anyuid SCC (postgres runs as root)
|
||||||
|
# - supabase-vector ServiceAccount → privileged SCC (hostPath to /var/log/pods)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 0: Resolve secrets from Vault (generate and store if missing)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Resolve Supabase secrets from Vault
|
||||||
|
ansible.builtin.include_tasks: vault_secrets.yml
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 1: Namespace
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create Supabase namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ supabase_namespace }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 2: SCC grants (ClusterRoleBindings before helm so pods start clean)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create ClusterRole granting anyuid SCC (supabase db)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: supabase-db-anyuid-scc
|
||||||
|
rules:
|
||||||
|
- apiGroups: [security.openshift.io]
|
||||||
|
resources: [securitycontextconstraints]
|
||||||
|
verbs: [use]
|
||||||
|
resourceNames: [anyuid]
|
||||||
|
|
||||||
|
- name: Bind anyuid SCC to supabase db ServiceAccount
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: supabase-db-anyuid-scc
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: "{{ supabase_db_fullname }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: supabase-db-anyuid-scc
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
|
||||||
|
- name: Create ClusterRole granting anyuid SCC (supabase kong)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: supabase-kong-anyuid-scc
|
||||||
|
rules:
|
||||||
|
- apiGroups: [security.openshift.io]
|
||||||
|
resources: [securitycontextconstraints]
|
||||||
|
verbs: [use]
|
||||||
|
resourceNames: [anyuid]
|
||||||
|
|
||||||
|
- name: Bind anyuid SCC to supabase kong ServiceAccount
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: supabase-kong-anyuid-scc
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: "{{ supabase_kong_fullname }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: supabase-kong-anyuid-scc
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
|
||||||
|
- name: Create ClusterRole granting anyuid SCC (supabase functions)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: supabase-functions-anyuid-scc
|
||||||
|
rules:
|
||||||
|
- apiGroups: [security.openshift.io]
|
||||||
|
resources: [securitycontextconstraints]
|
||||||
|
verbs: [use]
|
||||||
|
resourceNames: [anyuid]
|
||||||
|
|
||||||
|
- name: Bind anyuid SCC to supabase functions ServiceAccount
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: supabase-functions-anyuid-scc
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: "{{ supabase_functions_fullname }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: supabase-functions-anyuid-scc
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
|
||||||
|
- name: Create ClusterRole granting privileged SCC (supabase vector)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: supabase-vector-privileged-scc
|
||||||
|
rules:
|
||||||
|
- apiGroups: [security.openshift.io]
|
||||||
|
resources: [securitycontextconstraints]
|
||||||
|
verbs: [use]
|
||||||
|
resourceNames: [privileged]
|
||||||
|
|
||||||
|
- name: Bind privileged SCC to supabase vector ServiceAccount
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: supabase-vector-privileged-scc
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: "{{ supabase_vector_fullname }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: supabase-vector-privileged-scc
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 3: Kubernetes Secrets (no_log — referenced by helm via secretRef)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create JWT secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_jwt }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
anonKey: "{{ __supabase_anon_key }}"
|
||||||
|
serviceKey: "{{ __supabase_service_key }}"
|
||||||
|
secret: "{{ __supabase_jwt_secret }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create DB secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_db }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
password: "{{ __supabase_db_password }}"
|
||||||
|
database: "{{ supabase_db_name }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create dashboard secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_dashboard }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
username: "{{ __supabase_dashboard_username }}"
|
||||||
|
password: "{{ __supabase_dashboard_password }}"
|
||||||
|
openAiApiKey: "{{ __supabase_openai_api_key }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create analytics secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_analytics }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
publicAccessToken: "{{ __supabase_analytics_public_token }}"
|
||||||
|
privateAccessToken: "{{ __supabase_analytics_private_token }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create realtime secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_realtime }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
secretKeyBase: "{{ __supabase_realtime_secret_key_base }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create meta secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_meta }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
cryptoKey: "{{ __supabase_meta_crypto_key }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Create SMTP secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ __supabase_secret_smtp }}"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
username: "{{ vault_supabase_smtp_username | default('') }}" # set in host_vars
|
||||||
|
password: "{{ vault_supabase_smtp_password | default('') }}" # set in host_vars
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 4: Optional Docker Hub pull secret
|
||||||
|
# Avoids anonymous rate limiting for docker.io images (kong, vector, etc.)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create Docker Hub pull secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: supabase-dockerhub
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
type: kubernetes.io/dockerconfigjson
|
||||||
|
data:
|
||||||
|
.dockerconfigjson: >-
|
||||||
|
{{ {'auths': {'https://index.docker.io/v1/': {
|
||||||
|
'username': supabase_dockerhub_username,
|
||||||
|
'password': vault_supabase_dockerhub_token,
|
||||||
|
'auth': (supabase_dockerhub_username + ':' + vault_supabase_dockerhub_token) | b64encode
|
||||||
|
}}} | to_json | b64encode }}
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: supabase_dockerhub_enabled | bool
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 5: Helm install / upgrade
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Deploy Supabase via Helm
|
||||||
|
kubernetes.core.helm:
|
||||||
|
release_name: "{{ supabase_release_name }}"
|
||||||
|
chart_ref: supabase
|
||||||
|
chart_repo_url: "{{ supabase_chart_repo_url }}"
|
||||||
|
chart_version: "{{ supabase_chart_version | default(omit, true) }}"
|
||||||
|
release_namespace: "{{ supabase_namespace }}"
|
||||||
|
create_namespace: false
|
||||||
|
wait: false
|
||||||
|
values:
|
||||||
|
# Use predictable names so SCC bindings and Route target can reference them
|
||||||
|
deployment:
|
||||||
|
db:
|
||||||
|
fullnameOverride: "{{ supabase_db_fullname }}"
|
||||||
|
kong:
|
||||||
|
fullnameOverride: "{{ supabase_kong_fullname }}"
|
||||||
|
functions:
|
||||||
|
fullnameOverride: "{{ supabase_functions_fullname }}"
|
||||||
|
vector:
|
||||||
|
fullnameOverride: "{{ supabase_vector_fullname }}"
|
||||||
|
|
||||||
|
# Secrets via secretRef — keep secret material out of Helm values
|
||||||
|
secret:
|
||||||
|
jwt:
|
||||||
|
secretRef: "{{ __supabase_secret_jwt }}"
|
||||||
|
db:
|
||||||
|
secretRef: "{{ __supabase_secret_db }}"
|
||||||
|
dashboard:
|
||||||
|
secretRef: "{{ __supabase_secret_dashboard }}"
|
||||||
|
analytics:
|
||||||
|
secretRef: "{{ __supabase_secret_analytics }}"
|
||||||
|
realtime:
|
||||||
|
secretRef: "{{ __supabase_secret_realtime }}"
|
||||||
|
meta:
|
||||||
|
secretRef: "{{ __supabase_secret_meta }}"
|
||||||
|
smtp:
|
||||||
|
secretRef: "{{ __supabase_secret_smtp }}"
|
||||||
|
|
||||||
|
# Storage — all PVCs use the configured StorageClass
|
||||||
|
persistence:
|
||||||
|
db:
|
||||||
|
storageClassName: "{{ supabase_storage_class }}"
|
||||||
|
size: "{{ supabase_db_storage_size }}"
|
||||||
|
functions:
|
||||||
|
storageClassName: "{{ supabase_storage_class }}"
|
||||||
|
size: "{{ supabase_functions_storage_size }}"
|
||||||
|
imgproxy:
|
||||||
|
storageClassName: "{{ supabase_storage_class }}"
|
||||||
|
size: "{{ supabase_imgproxy_storage_size }}"
|
||||||
|
minio:
|
||||||
|
storageClassName: "{{ supabase_storage_class }}"
|
||||||
|
size: "{{ supabase_minio_storage_size }}"
|
||||||
|
|
||||||
|
# Explicit docker.io registry prefix on all docker.io images.
|
||||||
|
# OpenShift ImageContentSourcePolicy rewrites bare image refs (e.g. kong/kong)
|
||||||
|
# to the configured mirror; supplying the full docker.io/ hostname forces the
|
||||||
|
# pull against the real registry and lets the pull secret take effect.
|
||||||
|
image:
|
||||||
|
analytics:
|
||||||
|
repository: docker.io/supabase/logflare
|
||||||
|
auth:
|
||||||
|
repository: docker.io/supabase/gotrue
|
||||||
|
db:
|
||||||
|
repository: docker.io/supabase/postgres
|
||||||
|
functions:
|
||||||
|
repository: docker.io/supabase/edge-runtime
|
||||||
|
imgproxy:
|
||||||
|
repository: docker.io/darthsim/imgproxy
|
||||||
|
kong:
|
||||||
|
repository: docker.io/kong/kong
|
||||||
|
meta:
|
||||||
|
repository: docker.io/supabase/postgres-meta
|
||||||
|
realtime:
|
||||||
|
repository: docker.io/supabase/realtime
|
||||||
|
rest:
|
||||||
|
repository: docker.io/postgrest/postgrest
|
||||||
|
storage:
|
||||||
|
repository: docker.io/supabase/storage-api
|
||||||
|
studio:
|
||||||
|
repository: docker.io/supabase/studio
|
||||||
|
vector:
|
||||||
|
repository: docker.io/timberio/vector
|
||||||
|
|
||||||
|
# Docker Hub pull secret (only wired in when enabled)
|
||||||
|
imagePullSecrets: "{{ [{'name': 'supabase-dockerhub'}] if supabase_dockerhub_enabled | bool else [] }}"
|
||||||
|
|
||||||
|
# Disable chart's nginx ingress — we create an OCP Route below
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Studio public URL (used by auth callbacks and the browser)
|
||||||
|
environment:
|
||||||
|
studio:
|
||||||
|
SUPABASE_PUBLIC_URL: "https://{{ supabase_studio_host }}"
|
||||||
|
STUDIO_DEFAULT_ORGANIZATION: "{{ supabase_org_name }}"
|
||||||
|
STUDIO_DEFAULT_PROJECT: "{{ supabase_project_name }}"
|
||||||
|
NEXT_PUBLIC_ENABLE_LOGS: "true"
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
auth:
|
||||||
|
environment:
|
||||||
|
GOTRUE_SMTP_HOST: "{{ supabase_smtp_host }}"
|
||||||
|
GOTRUE_SMTP_PORT: "{{ supabase_smtp_port | string }}"
|
||||||
|
GOTRUE_SMTP_ADMIN_EMAIL: "{{ supabase_smtp_admin_email }}"
|
||||||
|
GOTRUE_SMTP_SENDER_NAME: "{{ supabase_smtp_sender_name }}"
|
||||||
|
GOTRUE_MAILER_AUTOCONFIRM: "{{ 'false' if supabase_smtp_enabled else 'true' }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 6: OpenShift Route (edge TLS, cert-manager certificate)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create TLS Certificate for Supabase
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: supabase-tls
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
spec:
|
||||||
|
secretName: supabase-tls
|
||||||
|
issuerRef:
|
||||||
|
name: "{{ supabase_tls_cluster_issuer }}"
|
||||||
|
kind: ClusterIssuer
|
||||||
|
dnsNames:
|
||||||
|
- "{{ supabase_studio_host }}"
|
||||||
|
|
||||||
|
- name: Create OpenShift Route for Supabase (Kong gateway)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: route.openshift.io/v1
|
||||||
|
kind: Route
|
||||||
|
metadata:
|
||||||
|
name: supabase
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "{{ supabase_tls_cluster_issuer }}"
|
||||||
|
spec:
|
||||||
|
host: "{{ supabase_studio_host }}"
|
||||||
|
to:
|
||||||
|
kind: Service
|
||||||
|
name: "{{ supabase_kong_fullname }}"
|
||||||
|
weight: 100
|
||||||
|
port:
|
||||||
|
targetPort: http
|
||||||
|
tls:
|
||||||
|
termination: edge
|
||||||
|
insecureEdgeTerminationPolicy: Redirect
|
||||||
|
certificate: ""
|
||||||
|
key: ""
|
||||||
|
caCertificate: ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 7: NodePort Service for external PostgreSQL access
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create NodePort Service for external PostgreSQL access
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: "{{ supabase_db_fullname }}-external"
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: "{{ supabase_db_fullname }}"
|
||||||
|
app.kubernetes.io/instance: "{{ supabase_release_name }}"
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: 5432
|
||||||
|
targetPort: 5432
|
||||||
|
nodePort: "{{ supabase_db_nodeport | int }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 9: Wait for Kong deployment to be healthy
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Wait for Kong deployment to be ready
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: "{{ supabase_namespace }}"
|
||||||
|
name: "{{ supabase_kong_fullname }}"
|
||||||
|
register: __supabase_kong_deploy
|
||||||
|
until: >-
|
||||||
|
__supabase_kong_deploy.resources | length > 0 and
|
||||||
|
(__supabase_kong_deploy.resources[0].status.readyReplicas | default(0)) >= 1
|
||||||
|
retries: "{{ __supabase_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 10: Summary
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Display Supabase deployment summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "Supabase deployment complete!"
|
||||||
|
- " Namespace : {{ supabase_namespace }}"
|
||||||
|
- " Studio URL : https://{{ supabase_studio_host }}"
|
||||||
|
- " Kong service : {{ supabase_kong_fullname }}:8000"
|
||||||
|
- " DB PVC : {{ supabase_db_storage_size }} ({{ supabase_storage_class }})"
|
||||||
|
- " SMTP enabled : {{ supabase_smtp_enabled }}"
|
||||||
|
- ""
|
||||||
|
- " Login with vault_supabase_dashboard_username / vault_supabase_dashboard_password"
|
||||||
161
roles/supabase/tasks/vault_secrets.yml
Normal file
161
roles/supabase/tasks/vault_secrets.yml
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
# Read Supabase secrets from Vault; generate and store any that are missing.
|
||||||
|
#
|
||||||
|
# All __supabase_* facts set here are consumed by the K8s secret tasks in main.yml.
|
||||||
|
#
|
||||||
|
# JWT note: if jwt_secret is absent OR either JWT token is absent, all three are
|
||||||
|
# regenerated together — a partial JWT state (e.g. tokens signed by a different
|
||||||
|
# secret) would break auth across all services.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Read existing secrets (may fail if path does not exist yet)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Read supabase secrets from Vault
|
||||||
|
community.hashi_vault.vault_kv2_get:
|
||||||
|
path: "{{ supabase_vault_path }}"
|
||||||
|
engine_mount_point: "{{ supabase_vault_mount }}"
|
||||||
|
register: __supabase_vault_read
|
||||||
|
failed_when: false
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Set existing vault data fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__sv: "{{ __supabase_vault_read.secret | default({}) }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Resolve non-JWT secrets: use existing or generate random values
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Resolve non-JWT secrets (generate any that are missing)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_db_password: >-
|
||||||
|
{{ __sv.db_password
|
||||||
|
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||||
|
__supabase_dashboard_username: >-
|
||||||
|
{{ __sv.dashboard_username | default('supabase') }}
|
||||||
|
__supabase_dashboard_password: >-
|
||||||
|
{{ __sv.dashboard_password
|
||||||
|
| default(lookup('community.general.random_string', length=24, special=false)) }}
|
||||||
|
__supabase_analytics_public_token: >-
|
||||||
|
{{ __sv.analytics_public_token
|
||||||
|
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||||
|
__supabase_analytics_private_token: >-
|
||||||
|
{{ __sv.analytics_private_token
|
||||||
|
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||||
|
__supabase_realtime_secret_key_base: >-
|
||||||
|
{{ __sv.realtime_secret_key_base
|
||||||
|
| default(lookup('community.general.random_string', length=64, special=false)) }}
|
||||||
|
__supabase_meta_crypto_key: >-
|
||||||
|
{{ __sv.meta_crypto_key
|
||||||
|
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||||
|
__supabase_openai_api_key: >-
|
||||||
|
{{ __sv.openai_api_key | default('') }}
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# JWT secret + signed tokens (must be generated as a coherent set)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Check whether JWT values need to be (re)generated
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_needs_jwt_gen: >-
|
||||||
|
{{ not (__sv.jwt_secret | default('') | string | length > 0)
|
||||||
|
or not (__sv.anon_key | default('') | string | length > 0)
|
||||||
|
or not (__sv.service_key | default('') | string | length > 0) }}
|
||||||
|
|
||||||
|
- name: Generate JWT signing secret
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_jwt_secret: >-
|
||||||
|
{{ lookup('community.general.random_string', length=64, special=false) }}
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: __supabase_needs_jwt_gen | bool
|
||||||
|
|
||||||
|
- name: Use existing JWT signing secret
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_jwt_secret: "{{ __sv.jwt_secret }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: not __supabase_needs_jwt_gen | bool
|
||||||
|
|
||||||
|
- name: Generate anon and service_role JWT tokens
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- python3
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
import hmac, hashlib, base64, json, time, sys
|
||||||
|
|
||||||
|
def b64url(data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode()
|
||||||
|
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
|
||||||
|
|
||||||
|
def make_jwt(secret, role):
|
||||||
|
now = int(time.time())
|
||||||
|
header = b64url(json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')))
|
||||||
|
payload = b64url(json.dumps(
|
||||||
|
{'role': role, 'iss': 'supabase', 'iat': now, 'exp': now + 157680000},
|
||||||
|
separators=(',', ':')
|
||||||
|
))
|
||||||
|
msg = '{}.{}'.format(header, payload)
|
||||||
|
sig = b64url(hmac.new(secret.encode(), msg.encode(), hashlib.sha256).digest())
|
||||||
|
return '{}.{}'.format(msg, sig)
|
||||||
|
|
||||||
|
secret = sys.argv[1]
|
||||||
|
print(make_jwt(secret, 'anon'))
|
||||||
|
print(make_jwt(secret, 'service_role'))
|
||||||
|
- "{{ __supabase_jwt_secret }}"
|
||||||
|
register: __supabase_jwt_output
|
||||||
|
changed_when: false
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: __supabase_needs_jwt_gen | bool
|
||||||
|
|
||||||
|
- name: Set generated JWT token facts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_anon_key: "{{ __supabase_jwt_output.stdout_lines[0] }}"
|
||||||
|
__supabase_service_key: "{{ __supabase_jwt_output.stdout_lines[1] }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: __supabase_needs_jwt_gen | bool
|
||||||
|
|
||||||
|
- name: Use existing JWT tokens from Vault
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__supabase_anon_key: "{{ __sv.anon_key }}"
|
||||||
|
__supabase_service_key: "{{ __sv.service_key }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
when: not __supabase_needs_jwt_gen | bool
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Write back to Vault (always — ensures all fields are present and
|
||||||
|
# any newly generated values are persisted before K8s secrets are written)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Write supabase secrets to Vault
|
||||||
|
community.hashi_vault.vault_kv2_write:
|
||||||
|
path: "{{ supabase_vault_path }}"
|
||||||
|
engine_mount_point: "{{ supabase_vault_mount }}"
|
||||||
|
data:
|
||||||
|
jwt_secret: "{{ __supabase_jwt_secret }}"
|
||||||
|
anon_key: "{{ __supabase_anon_key }}"
|
||||||
|
service_key: "{{ __supabase_service_key }}"
|
||||||
|
db_password: "{{ __supabase_db_password }}"
|
||||||
|
dashboard_username: "{{ __supabase_dashboard_username }}"
|
||||||
|
dashboard_password: "{{ __supabase_dashboard_password }}"
|
||||||
|
analytics_public_token: "{{ __supabase_analytics_public_token }}"
|
||||||
|
analytics_private_token: "{{ __supabase_analytics_private_token }}"
|
||||||
|
realtime_secret_key_base: "{{ __supabase_realtime_secret_key_base }}"
|
||||||
|
meta_crypto_key: "{{ __supabase_meta_crypto_key }}"
|
||||||
|
openai_api_key: "{{ __supabase_openai_api_key }}"
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Write postgres_url to oys/dev/supabase in Vault
|
||||||
|
community.hashi_vault.vault_kv2_write:
|
||||||
|
path: oys/dev/supabase
|
||||||
|
engine_mount_point: "{{ supabase_vault_mount }}"
|
||||||
|
data:
|
||||||
|
postgres_url: >-
|
||||||
|
postgresql://postgres:{{ __supabase_db_password }}@{{ supabase_db_external_host }}:{{ supabase_db_nodeport }}/{{ supabase_db_name }}
|
||||||
|
no_log: "{{ supabase_no_log }}"
|
||||||
|
|
||||||
|
- name: Report vault secret status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ 'Generated and stored new Supabase secrets in Vault'
|
||||||
|
if __supabase_needs_jwt_gen | bool
|
||||||
|
else 'Using existing Supabase secrets from Vault' }}
|
||||||
13
roles/supabase/vars/main.yml
Normal file
13
roles/supabase/vars/main.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
# Computed internal vars — not intended for override
|
||||||
|
|
||||||
|
__supabase_wait_retries: "{{ (supabase_wait_timeout | int / 10) | int }}"
|
||||||
|
|
||||||
|
# Kubernetes secret names created before helm install; referenced via secretRef
|
||||||
|
__supabase_secret_jwt: supabase-jwt
|
||||||
|
__supabase_secret_db: supabase-db
|
||||||
|
__supabase_secret_dashboard: supabase-dashboard
|
||||||
|
__supabase_secret_analytics: supabase-analytics
|
||||||
|
__supabase_secret_realtime: supabase-realtime
|
||||||
|
__supabase_secret_meta: supabase-meta
|
||||||
|
__supabase_secret_smtp: supabase-smtp
|
||||||
Reference in New Issue
Block a user