Compare commits
4 Commits
df1dd39197
...
update_202
| Author | SHA1 | Date | |
|---|---|---|---|
|
7df35b9dd4
|
|||
|
c164cb2598
|
|||
|
085654d017
|
|||
|
5fe7b4edeb
|
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ansible-dev-container-codespaces",
|
|
||||||
"image": "ghcr.io/ansible/community-ansible-dev-tools:latest",
|
|
||||||
"containerUser": "root",
|
|
||||||
"runArgs": [
|
|
||||||
"--security-opt",
|
|
||||||
"seccomp=unconfined",
|
|
||||||
"--security-opt",
|
|
||||||
"label=disable",
|
|
||||||
"--cap-add=SYS_ADMIN",
|
|
||||||
"--cap-add=SYS_RESOURCE",
|
|
||||||
"--device",
|
|
||||||
"/dev/fuse",
|
|
||||||
"--security-opt",
|
|
||||||
"apparmor=unconfined",
|
|
||||||
"--hostname=ansible-dev-container"
|
|
||||||
],
|
|
||||||
"updateRemoteUserUID": true,
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": ["redhat.ansible","redhat.vscode-redhat-account"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ansible-dev-container-docker",
|
|
||||||
"image": "ghcr.io/ansible/community-ansible-dev-tools:latest",
|
|
||||||
"containerUser": "root",
|
|
||||||
"runArgs": [
|
|
||||||
"--security-opt",
|
|
||||||
"seccomp=unconfined",
|
|
||||||
"--security-opt",
|
|
||||||
"label=disable",
|
|
||||||
"--cap-add=SYS_ADMIN",
|
|
||||||
"--cap-add=SYS_RESOURCE",
|
|
||||||
"--device",
|
|
||||||
"/dev/fuse",
|
|
||||||
"--security-opt",
|
|
||||||
"apparmor=unconfined",
|
|
||||||
"--hostname=ansible-dev-container"
|
|
||||||
],
|
|
||||||
"updateRemoteUserUID": true,
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": ["redhat.ansible","redhat.vscode-redhat-account"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ansible-dev-container-podman",
|
|
||||||
"image": "ghcr.io/ansible/community-ansible-dev-tools:latest",
|
|
||||||
"containerUser": "root",
|
|
||||||
"containerEnv": {
|
|
||||||
"REGISTRY_AUTH_FILE": "/container-auth.json"
|
|
||||||
},
|
|
||||||
"runArgs": [
|
|
||||||
"--cap-add=CAP_MKNOD",
|
|
||||||
"--cap-add=NET_ADMIN",
|
|
||||||
"--cap-add=SYS_ADMIN",
|
|
||||||
"--cap-add=SYS_RESOURCE",
|
|
||||||
"--device",
|
|
||||||
"/dev/fuse",
|
|
||||||
"--security-opt",
|
|
||||||
"seccomp=unconfined",
|
|
||||||
"--security-opt",
|
|
||||||
"label=disable",
|
|
||||||
"--security-opt",
|
|
||||||
"apparmor=unconfined",
|
|
||||||
"--security-opt",
|
|
||||||
"unmask=/sys/fs/cgroup",
|
|
||||||
"--userns=host",
|
|
||||||
"--hostname=ansible-dev-container",
|
|
||||||
"--env-file",
|
|
||||||
".env"
|
|
||||||
],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": ["redhat.ansible","redhat.vscode-redhat-account"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mounts": [
|
|
||||||
"source=${localEnv:XDG_RUNTIME_DIR}/containers/auth.json,target=/container-auth.json,type=bind,consistency=cached",
|
|
||||||
"source=${localEnv:HOME}/Dev/inventories/toallab-inventory,target=/workspaces/inventory,type=bind,consistency=cached",
|
|
||||||
"source=${localEnv:HOME}/Dev/ansible_collections/,target=/workspaces/collections/,type=bind,consistency=cached",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- name: Set up IPA Client
|
- name: Set up IPA Client
|
||||||
hosts: tag_ipa_client
|
hosts: tag_ipa_client
|
||||||
become: yes
|
become: true
|
||||||
collections:
|
collections:
|
||||||
- freeipa.ansible_freeipa
|
- freeipa.ansible_freeipa
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
- name: Packages
|
- name: Packages
|
||||||
hosts: all
|
hosts: all
|
||||||
become: yes
|
become: true
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- name: Host Packages
|
- name: Host Packages
|
||||||
|
|||||||
84
playbooks/deploy_automation_orchestrator.yml
Normal file
84
playbooks/deploy_automation_orchestrator.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
# Deploy Automation Orchestrator on OpenShift via its OLM operator, backed by a
|
||||||
|
# CloudNativePG-provisioned PostgreSQL cluster.
|
||||||
|
#
|
||||||
|
# WARNING: CloudNativePG here is a single-instance, operator-provisioned PostgreSQL
|
||||||
|
# cluster intended for demo/dev use — not production. For production, disable the
|
||||||
|
# cloudnativepg_operator role below and point automation_orchestrator_operator's
|
||||||
|
# postgres_* variables at a customer-managed PostgreSQL instance instead.
|
||||||
|
#
|
||||||
|
# Authenticates via the same aap-deployer ServiceAccount token used by deploy_aap.yml
|
||||||
|
# (same cluster). The token is stored in 1Password as vault_aap_deployer_token.
|
||||||
|
#
|
||||||
|
# Modeled on https://github.com/ansible/aapctl (aap-plus install path).
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - OpenShift cluster deployed (deploy_openshift.yml)
|
||||||
|
# - aap-deployer ServiceAccount provisioned (re-run after RBAC changes):
|
||||||
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||||
|
# - CloudNativePG uses Manual install-plan approval; the role approves its
|
||||||
|
# InstallPlan. Automation Orchestrator installs from redhat-operators and
|
||||||
|
# adopts an existing operator install when its namespace is set in inventory.
|
||||||
|
# - Pre-release AO builds additionally need a custom CatalogSource and an
|
||||||
|
# ImageDigestMirrorSet (see automation_orchestrator_operator defaults).
|
||||||
|
#
|
||||||
|
# Keycloak OIDC (--tags ao_configure_oidc, run after the install):
|
||||||
|
# - Keycloak settings come from the openshift host (keycloak_url, keycloak_realm,
|
||||||
|
# keycloak_admin_user, vault_keycloak_admin_password).
|
||||||
|
# - automation_orchestrator_oidc_group_mappings in host_vars for the aap host.
|
||||||
|
# - The client secret is generated by Keycloak and read back each run (not vaulted).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout
|
||||||
|
# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout --tags ao_configure_oidc
|
||||||
|
|
||||||
|
- name: Install Automation Orchestrator
|
||||||
|
hosts: aap
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
environment:
|
||||||
|
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||||
|
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Verify aap-deployer token is available
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- vault_aap_deployer_token is defined
|
||||||
|
- vault_aap_deployer_token | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
vault_aap_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 as vault_aap_deployer_token.
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: cloudnativepg_operator
|
||||||
|
- role: automation_orchestrator_operator
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configure Keycloak OIDC login (optional, run after the install)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Configure Keycloak OIDC for Automation Orchestrator
|
||||||
|
hosts: aap
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- never
|
||||||
|
- ao_configure_oidc
|
||||||
|
|
||||||
|
vars:
|
||||||
|
__ao_keycloak_host: "{{ hostvars[groups['openshift'][0]] }}"
|
||||||
|
automation_orchestrator_oidc_keycloak_url: "{{ __ao_keycloak_host.keycloak_url }}{{ __ao_keycloak_host.keycloak_context | default('') }}"
|
||||||
|
automation_orchestrator_oidc_keycloak_realm: "{{ __ao_keycloak_host.keycloak_realm }}"
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_user: "{{ __ao_keycloak_host.keycloak_admin_user }}"
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_password: "{{ __ao_keycloak_host.vault_keycloak_admin_password }}"
|
||||||
|
automation_orchestrator_oidc_keycloak_validate_certs: "{{ __ao_keycloak_host.keycloak_validate_certs | default(true) }}"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||||
|
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: automation_orchestrator_oidc
|
||||||
@@ -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
|
|
||||||
@@ -33,6 +33,9 @@
|
|||||||
# Play 7: sno_deploy_lvms — Install LVM Storage for persistent volumes
|
# Play 7: sno_deploy_lvms — Install LVM Storage for persistent volumes
|
||||||
# Play 8: sno_deploy_nfs — Deploy in-cluster NFS provisioner (RWX StorageClass)
|
# Play 8: sno_deploy_nfs — Deploy in-cluster NFS provisioner (RWX StorageClass)
|
||||||
# Play 9: sno_deploy_service_accounts — Provision ServiceAccounts for app deployers
|
# Play 9: sno_deploy_service_accounts — Provision ServiceAccounts for app deployers
|
||||||
|
# Play 10: sno_deploy_alert_tuning — Drop single-control-plane false-positive alerts
|
||||||
|
#
|
||||||
|
# Recurring maintenance (separate playbook, schedule in AAP): maintain_sno_etcd.yml
|
||||||
#
|
#
|
||||||
# AAP deployment is in a separate playbook: deploy_aap.yml
|
# AAP deployment is in a separate playbook: deploy_aap.yml
|
||||||
#
|
#
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_lvms
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_lvms
|
||||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_nfs
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_nfs
|
||||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||||
|
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_alert_tuning
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Play 1: Create SNO VM in Proxmox
|
# Play 1: Create SNO VM in Proxmox
|
||||||
@@ -333,8 +337,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:
|
||||||
@@ -347,15 +400,62 @@
|
|||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["serviceaccounts"]
|
resources: ["serviceaccounts"]
|
||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
|
# patch: Automation Orchestrator backend env (APP_OIDC_ALLOW_PRIVATE_NETWORKS)
|
||||||
- apiGroups: ["apps"]
|
- apiGroups: ["apps"]
|
||||||
resources: ["deployments"]
|
resources: ["deployments"]
|
||||||
|
verbs: ["get", "list", "watch", "patch"]
|
||||||
|
- apiGroups: ["route.openshift.io"]
|
||||||
|
resources: ["routes"]
|
||||||
|
verbs: ["get", "list"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["endpoints"]
|
||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
- apiGroups: ["operators.coreos.com"]
|
- apiGroups: ["operators.coreos.com"]
|
||||||
resources: ["operatorgroups", "subscriptions", "clusterserviceversions"]
|
resources: ["operatorgroups", "subscriptions", "clusterserviceversions",
|
||||||
|
"catalogsources"]
|
||||||
verbs: ["get", "list", "create", "patch", "watch"]
|
verbs: ["get", "list", "create", "patch", "watch"]
|
||||||
|
- apiGroups: ["operators.coreos.com"]
|
||||||
|
resources: ["installplans"]
|
||||||
|
verbs: ["get", "list", "patch", "watch"]
|
||||||
- apiGroups: ["apiextensions.k8s.io"]
|
- apiGroups: ["apiextensions.k8s.io"]
|
||||||
resources: ["customresourcedefinitions"]
|
resources: ["customresourcedefinitions"]
|
||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
- apiGroups: ["aap.ansible.com"]
|
- apiGroups: ["aap.ansible.com"]
|
||||||
resources: ["ansibleautomationplatforms"]
|
resources: ["ansibleautomationplatforms", "automationorchestrators"]
|
||||||
verbs: ["get", "list", "create", "patch", "watch"]
|
verbs: ["get", "list", "create", "patch", "watch"]
|
||||||
|
# CloudNativePG Cluster/Database CRs backing Automation Orchestrator
|
||||||
|
- apiGroups: ["postgresql.cnpg.io"]
|
||||||
|
resources: ["clusters", "databases"]
|
||||||
|
verbs: ["get", "list", "create", "patch", "watch"]
|
||||||
|
# Automation Orchestrator pre-release image mirror (IDMS) + MCP rollout wait
|
||||||
|
- apiGroups: ["config.openshift.io"]
|
||||||
|
resources: ["imagedigestmirrorsets"]
|
||||||
|
verbs: ["get", "list", "create", "patch"]
|
||||||
|
- apiGroups: ["machineconfiguration.openshift.io"]
|
||||||
|
resources: ["machineconfigpools"]
|
||||||
|
verbs: ["get", "list", "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
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Play 10: Drop platform alerts that are false positives on a single
|
||||||
|
# control-plane cluster (AlertRelabelConfig in openshift-monitoring)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Tune platform alerts for single-node OpenShift
|
||||||
|
hosts: sno.openshift.toal.ca
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
tags: sno_deploy_alert_tuning
|
||||||
|
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||||
|
K8S_AUTH_VERIFY_SSL: "false"
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: sno_alert_tuning
|
||||||
|
|||||||
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
|
||||||
23
playbooks/maintain_sno_etcd.yml
Normal file
23
playbooks/maintain_sno_etcd.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
# Defragment etcd on the single-node OpenShift cluster.
|
||||||
|
#
|
||||||
|
# OpenShift disables automatic etcd defragmentation on single-control-plane
|
||||||
|
# clusters, so run this on a schedule (e.g. weekly AAP schedule, off-hours).
|
||||||
|
# It only defragments when fragmentation exceeds the thresholds of the
|
||||||
|
# etcdDatabaseHighFragmentationRatio alert; otherwise it reports and exits.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ansible-navigator run playbooks/maintain_sno_etcd.yml --mode stdout
|
||||||
|
# ansible-navigator run playbooks/maintain_sno_etcd.yml --mode stdout -e sno_etcd_defrag_force=true
|
||||||
|
|
||||||
|
- name: Defragment etcd on single-node OpenShift
|
||||||
|
hosts: sno.openshift.toal.ca
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||||
|
K8S_AUTH_VERIFY_SSL: "false"
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: sno_etcd_defrag
|
||||||
38
roles/automation_orchestrator_oidc/defaults/main.yml
Normal file
38
roles/automation_orchestrator_oidc/defaults/main.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
# --- Automation Orchestrator instance ---
|
||||||
|
# Defaults track the automation_orchestrator_operator role's variables when set.
|
||||||
|
automation_orchestrator_oidc_namespace: "{{ automation_orchestrator_operator_app_namespace | default('automation-orchestrator') }}"
|
||||||
|
automation_orchestrator_oidc_instance_name: "{{ automation_orchestrator_operator_instance_name | default('automation-orchestrator') }}"
|
||||||
|
# Public base URL of the AO UI/API; "" derives it from the instance's Route.
|
||||||
|
automation_orchestrator_oidc_url: ""
|
||||||
|
automation_orchestrator_oidc_admin_user: admin
|
||||||
|
automation_orchestrator_oidc_validate_certs: true
|
||||||
|
|
||||||
|
# --- Keycloak ---
|
||||||
|
automation_orchestrator_oidc_keycloak_url: ""
|
||||||
|
automation_orchestrator_oidc_keycloak_realm: ""
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_user: ""
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_password: ""
|
||||||
|
automation_orchestrator_oidc_keycloak_validate_certs: true
|
||||||
|
automation_orchestrator_oidc_client_id: automation-orchestrator
|
||||||
|
# Create the Keycloak groups named in group_mappings (membership is managed in Keycloak).
|
||||||
|
automation_orchestrator_oidc_create_keycloak_groups: true
|
||||||
|
|
||||||
|
# --- AO identity provider ---
|
||||||
|
automation_orchestrator_oidc_provider_name: Keycloak
|
||||||
|
# Keycloak group (from the "groups" claim) -> AO built-in group (admins, auditors, users).
|
||||||
|
# Users in no mapped group are refused login.
|
||||||
|
automation_orchestrator_oidc_group_mappings: []
|
||||||
|
# - idp_group: ao-admins
|
||||||
|
# group: admins
|
||||||
|
automation_orchestrator_oidc_rp_initiated_logout: true
|
||||||
|
# PATCH the provider even when its config is unchanged (e.g. to push a rotated client secret).
|
||||||
|
automation_orchestrator_oidc_force_update: false
|
||||||
|
|
||||||
|
# The backend rejects OIDC issuers that resolve to private IPs unless this is set, and
|
||||||
|
# the AutomationOrchestrator CR has no field for it — so the role sets the env var on the
|
||||||
|
# backend Deployment directly. Re-run the role if the operator ever reverts it.
|
||||||
|
automation_orchestrator_oidc_allow_private_networks: true
|
||||||
|
|
||||||
|
# --- Debugging ---
|
||||||
|
automation_orchestrator_oidc_no_log: true
|
||||||
99
roles/automation_orchestrator_oidc/meta/argument_specs.yml
Normal file
99
roles/automation_orchestrator_oidc/meta/argument_specs.yml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Configure Keycloak OIDC login for Automation Orchestrator
|
||||||
|
description:
|
||||||
|
- Creates a confidential OIDC client (with a "groups" claim mapper) and the mapped groups
|
||||||
|
in Keycloak, then registers Keycloak as an identity provider in Automation Orchestrator
|
||||||
|
via its REST API, mapping Keycloak groups to AO built-in groups.
|
||||||
|
- The client secret is read back from Keycloak on each run, so it is not stored elsewhere.
|
||||||
|
- >-
|
||||||
|
Sets APP_OIDC_ALLOW_PRIVATE_NETWORKS on the backend Deployment, since the
|
||||||
|
AutomationOrchestrator CR has no field for it and the backend otherwise rejects
|
||||||
|
issuers resolving to private IPs.
|
||||||
|
options:
|
||||||
|
automation_orchestrator_oidc_namespace:
|
||||||
|
description: Namespace of the AutomationOrchestrator instance.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
automation_orchestrator_oidc_instance_name:
|
||||||
|
description: Name of the AutomationOrchestrator CR (prefix of its Route, Deployments and Secrets).
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
automation_orchestrator_oidc_url:
|
||||||
|
description: Public base URL of Automation Orchestrator. Empty derives it from the instance Route.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
automation_orchestrator_oidc_admin_user:
|
||||||
|
description: Local AO admin used to call the API (password read from the initial-admin-password Secret).
|
||||||
|
type: str
|
||||||
|
default: admin
|
||||||
|
automation_orchestrator_oidc_validate_certs:
|
||||||
|
description: Verify TLS when calling the AO API.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
automation_orchestrator_oidc_keycloak_url:
|
||||||
|
description: Keycloak base URL (e.g. https://auth.example.com).
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
automation_orchestrator_oidc_keycloak_realm:
|
||||||
|
description: Keycloak realm holding the client and groups.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_user:
|
||||||
|
description: Keycloak master-realm admin username.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
automation_orchestrator_oidc_keycloak_admin_password:
|
||||||
|
description: Keycloak master-realm admin password.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
no_log: true
|
||||||
|
automation_orchestrator_oidc_keycloak_validate_certs:
|
||||||
|
description: Verify TLS when calling the Keycloak admin API.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
automation_orchestrator_oidc_client_id:
|
||||||
|
description: Keycloak client ID for Automation Orchestrator.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
automation_orchestrator_oidc_create_keycloak_groups:
|
||||||
|
description: Create the Keycloak groups named in group_mappings. Group membership is managed in Keycloak.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
automation_orchestrator_oidc_provider_name:
|
||||||
|
description: Name of the identity provider in Automation Orchestrator (shown on the login page).
|
||||||
|
type: str
|
||||||
|
default: Keycloak
|
||||||
|
automation_orchestrator_oidc_group_mappings:
|
||||||
|
description: >-
|
||||||
|
Keycloak group to AO built-in group mappings. Users in no mapped group are refused login.
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
idp_group:
|
||||||
|
description: Keycloak group name, as emitted in the "groups" claim.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
group:
|
||||||
|
description: AO built-in group.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
choices: [admins, auditors, users]
|
||||||
|
automation_orchestrator_oidc_rp_initiated_logout:
|
||||||
|
description: Log users out of Keycloak when they log out of Automation Orchestrator.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
automation_orchestrator_oidc_force_update:
|
||||||
|
description: PATCH the identity provider even when unchanged (e.g. to push a rotated client secret).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
automation_orchestrator_oidc_allow_private_networks:
|
||||||
|
description: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS=true on the backend Deployment.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
automation_orchestrator_oidc_no_log:
|
||||||
|
description: Set to false to expose secret values in task output. Never set false in shared environments.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
17
roles/automation_orchestrator_oidc/meta/main.yml
Normal file
17
roles/automation_orchestrator_oidc/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: ptoal
|
||||||
|
description: Configure Keycloak OIDC login for Automation Orchestrator
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.16"
|
||||||
|
platforms:
|
||||||
|
- name: GenericLinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- openshift
|
||||||
|
- aap
|
||||||
|
- keycloak
|
||||||
|
- oidc
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
286
roles/automation_orchestrator_oidc/tasks/main.yml
Normal file
286
roles/automation_orchestrator_oidc/tasks/main.yml
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
---
|
||||||
|
# Configure Keycloak OIDC login for Automation Orchestrator.
|
||||||
|
#
|
||||||
|
# 1. Keycloak: mapped groups + confidential client with a "groups" claim mapper;
|
||||||
|
# the client secret is read back rather than stored elsewhere.
|
||||||
|
# 2. Backend Deployment: APP_OIDC_ALLOW_PRIVATE_NETWORKS (no CR field exists for it).
|
||||||
|
# 3. AO REST API: create/update the identity provider with group mappings.
|
||||||
|
#
|
||||||
|
# AO identity providers are runtime configuration in the AO database — the
|
||||||
|
# AutomationOrchestrator CR has no auth fields.
|
||||||
|
|
||||||
|
- name: Validate group mappings
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- automation_orchestrator_oidc_group_mappings | length > 0
|
||||||
|
- >-
|
||||||
|
automation_orchestrator_oidc_group_mappings | map(attribute='group')
|
||||||
|
| difference(__automation_orchestrator_oidc_valid_groups) | length == 0
|
||||||
|
fail_msg: >-
|
||||||
|
automation_orchestrator_oidc_group_mappings must map at least one Keycloak group to an
|
||||||
|
AO built-in group ({{ __automation_orchestrator_oidc_valid_groups | join(', ') }});
|
||||||
|
otherwise no Keycloak user can log in.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 1: Resolve the Automation Orchestrator public URL
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Read Automation Orchestrator Route
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: route.openshift.io/v1
|
||||||
|
kind: Route
|
||||||
|
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_oidc_instance_name }}"
|
||||||
|
register: __automation_orchestrator_oidc_route
|
||||||
|
when: automation_orchestrator_oidc_url | length == 0
|
||||||
|
failed_when: __automation_orchestrator_oidc_route.resources | length == 0
|
||||||
|
|
||||||
|
- name: Set Automation Orchestrator URL
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_oidc_url: >-
|
||||||
|
{{ (automation_orchestrator_oidc_url
|
||||||
|
if automation_orchestrator_oidc_url | length > 0
|
||||||
|
else 'https://' + __automation_orchestrator_oidc_route.resources[0].spec.host)
|
||||||
|
| regex_replace('/$', '') }}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 2: Keycloak client and groups
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Configure Keycloak
|
||||||
|
module_defaults:
|
||||||
|
community.general.keycloak_group: &keycloak_auth
|
||||||
|
auth_client_id: admin-cli
|
||||||
|
auth_keycloak_url: "{{ automation_orchestrator_oidc_keycloak_url }}"
|
||||||
|
auth_realm: master
|
||||||
|
auth_username: "{{ automation_orchestrator_oidc_keycloak_admin_user }}"
|
||||||
|
auth_password: "{{ automation_orchestrator_oidc_keycloak_admin_password }}"
|
||||||
|
validate_certs: "{{ automation_orchestrator_oidc_keycloak_validate_certs }}"
|
||||||
|
community.general.keycloak_clientsecret_info: *keycloak_auth
|
||||||
|
middleware_automation.keycloak.keycloak_client: *keycloak_auth
|
||||||
|
block:
|
||||||
|
- name: Create mapped Keycloak groups
|
||||||
|
community.general.keycloak_group:
|
||||||
|
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||||
|
name: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') | unique }}"
|
||||||
|
when: automation_orchestrator_oidc_create_keycloak_groups | bool
|
||||||
|
|
||||||
|
# No secret is passed: Keycloak generates one on create and keeps it on update.
|
||||||
|
- name: Create Automation Orchestrator OIDC client in Keycloak
|
||||||
|
middleware_automation.keycloak.keycloak_client:
|
||||||
|
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||||
|
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||||
|
name: Automation Orchestrator
|
||||||
|
description: OIDC client for Automation Orchestrator at {{ __automation_orchestrator_oidc_url }}
|
||||||
|
enabled: true
|
||||||
|
protocol: openid-connect
|
||||||
|
public_client: false
|
||||||
|
standard_flow_enabled: true
|
||||||
|
implicit_flow_enabled: false
|
||||||
|
direct_access_grants_enabled: false
|
||||||
|
service_accounts_enabled: false
|
||||||
|
root_url: "{{ __automation_orchestrator_oidc_url }}"
|
||||||
|
redirect_uris:
|
||||||
|
- "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback"
|
||||||
|
web_origins:
|
||||||
|
- "+"
|
||||||
|
attributes:
|
||||||
|
post.logout.redirect.uris: "{{ __automation_orchestrator_oidc_url }}##{{ __automation_orchestrator_oidc_url }}/*"
|
||||||
|
protocol_mappers:
|
||||||
|
- name: groups
|
||||||
|
protocol: openid-connect
|
||||||
|
protocolMapper: oidc-group-membership-mapper
|
||||||
|
config:
|
||||||
|
full.path: "false"
|
||||||
|
id.token.claim: "true"
|
||||||
|
access.token.claim: "true"
|
||||||
|
userinfo.token.claim: "true"
|
||||||
|
claim.name: groups
|
||||||
|
state: present
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
- name: Read Automation Orchestrator client secret from Keycloak
|
||||||
|
community.general.keycloak_clientsecret_info:
|
||||||
|
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||||
|
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||||
|
register: __automation_orchestrator_oidc_clientsecret
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 3: Allow private-network issuers on the backend
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS on backend Deployment
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: patched
|
||||||
|
merge_type: strategic-merge
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_oidc_instance_name }}-backend"
|
||||||
|
definition:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: backend
|
||||||
|
env:
|
||||||
|
- name: APP_OIDC_ALLOW_PRIVATE_NETWORKS
|
||||||
|
value: "{{ automation_orchestrator_oidc_allow_private_networks | string | lower }}"
|
||||||
|
register: __automation_orchestrator_oidc_backend_patch
|
||||||
|
|
||||||
|
- name: Wait for backend rollout
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_oidc_instance_name }}-backend"
|
||||||
|
register: __automation_orchestrator_oidc_backend
|
||||||
|
until: >-
|
||||||
|
__automation_orchestrator_oidc_backend.resources | length > 0 and
|
||||||
|
(__automation_orchestrator_oidc_backend.resources[0].status.observedGeneration | default(0)) >=
|
||||||
|
__automation_orchestrator_oidc_backend.resources[0].metadata.generation and
|
||||||
|
(__automation_orchestrator_oidc_backend.resources[0].status.updatedReplicas | default(0)) ==
|
||||||
|
__automation_orchestrator_oidc_backend.resources[0].spec.replicas and
|
||||||
|
(__automation_orchestrator_oidc_backend.resources[0].status.availableReplicas | default(0)) ==
|
||||||
|
__automation_orchestrator_oidc_backend.resources[0].spec.replicas
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
when: __automation_orchestrator_oidc_backend_patch is changed
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 4: Register Keycloak as an AO identity provider
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Read AO initial admin password
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Secret
|
||||||
|
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_oidc_instance_name }}-initial-admin-password"
|
||||||
|
register: __automation_orchestrator_oidc_admin_secret
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
- name: Log in to the AO API
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/login"
|
||||||
|
method: POST
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
username: "{{ automation_orchestrator_oidc_admin_user }}"
|
||||||
|
password: "{{ __automation_orchestrator_oidc_admin_secret.resources[0].data.password | b64decode }}"
|
||||||
|
validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}"
|
||||||
|
register: __automation_orchestrator_oidc_login
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
- name: Call the AO API
|
||||||
|
module_defaults:
|
||||||
|
ansible.builtin.uri:
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{ __automation_orchestrator_oidc_login.json.access_token }}"
|
||||||
|
validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}"
|
||||||
|
block:
|
||||||
|
- name: List AO groups
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/groups?limit=100"
|
||||||
|
register: __automation_orchestrator_oidc_groups
|
||||||
|
|
||||||
|
- name: List AO identity providers
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers?limit=100"
|
||||||
|
register: __automation_orchestrator_oidc_idps
|
||||||
|
|
||||||
|
- name: Build desired identity provider configuration
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_oidc_existing: >-
|
||||||
|
{{ __automation_orchestrator_oidc_idps.json.resources
|
||||||
|
| selectattr('name', '==', automation_orchestrator_oidc_provider_name) | first | default({}) }}
|
||||||
|
__automation_orchestrator_oidc_config:
|
||||||
|
provider_type: oidc
|
||||||
|
idp_type: custom
|
||||||
|
auto_discovery: true
|
||||||
|
issuer_url: "{{ __automation_orchestrator_oidc_issuer }}"
|
||||||
|
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||||
|
redirect_uri: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback"
|
||||||
|
scopes: openid profile email
|
||||||
|
group_jmespath_expression: groups
|
||||||
|
allow_all_authenticated: false
|
||||||
|
enable_rp_initiated_logout: "{{ automation_orchestrator_oidc_rp_initiated_logout | bool }}"
|
||||||
|
__automation_orchestrator_oidc_group_ids: >-
|
||||||
|
{{ __automation_orchestrator_oidc_groups.json.resources | items2dict(key_name='name', value_name='id') }}
|
||||||
|
__automation_orchestrator_oidc_entries: []
|
||||||
|
|
||||||
|
- name: Build group mapping entries
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_oidc_entries: >-
|
||||||
|
{{ __automation_orchestrator_oidc_entries + [{
|
||||||
|
'idp_group_value': item.idp_group,
|
||||||
|
'mapped_group_id': __automation_orchestrator_oidc_group_ids[item.group]}] }}
|
||||||
|
loop: "{{ automation_orchestrator_oidc_group_mappings }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.idp_group }} -> {{ item.group }}"
|
||||||
|
|
||||||
|
# Compare everything except client_secret (never returned by the API).
|
||||||
|
- name: Determine whether the identity provider needs updating
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_oidc_needs_update: >-
|
||||||
|
{{ automation_orchestrator_oidc_force_update | bool
|
||||||
|
or not (__automation_orchestrator_oidc_existing.enabled | default(false))
|
||||||
|
or (__automation_orchestrator_oidc_existing.configuration | default({}))
|
||||||
|
!= ((__automation_orchestrator_oidc_existing.configuration | default({}))
|
||||||
|
| combine(__automation_orchestrator_oidc_config))
|
||||||
|
or ((__automation_orchestrator_oidc_existing.configuration.group_mapping_entries | default([])
|
||||||
|
| map(attribute='idp_group_value') | zip(__automation_orchestrator_oidc_existing.configuration.group_mapping_entries
|
||||||
|
| default([]) | map(attribute='mapped_group_id')) | map('join', '=') | sort)
|
||||||
|
!= (__automation_orchestrator_oidc_entries | map(attribute='idp_group_value')
|
||||||
|
| zip(__automation_orchestrator_oidc_entries | map(attribute='mapped_group_id'))
|
||||||
|
| map('join', '=') | sort)) }}
|
||||||
|
when: __automation_orchestrator_oidc_existing | length > 0
|
||||||
|
|
||||||
|
- name: Create Keycloak identity provider in AO
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers"
|
||||||
|
method: POST
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
name: "{{ automation_orchestrator_oidc_provider_name }}"
|
||||||
|
description: Keycloak realm {{ automation_orchestrator_oidc_keycloak_realm }}
|
||||||
|
configuration: >-
|
||||||
|
{{ __automation_orchestrator_oidc_config | combine({
|
||||||
|
'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value,
|
||||||
|
'group_mapping_entries': __automation_orchestrator_oidc_entries}) }}
|
||||||
|
status_code: 201
|
||||||
|
changed_when: true
|
||||||
|
when: __automation_orchestrator_oidc_existing | length == 0
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
- name: Update Keycloak identity provider in AO
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers/{{ __automation_orchestrator_oidc_existing.id }}"
|
||||||
|
method: PATCH
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
enabled: true
|
||||||
|
configuration: >-
|
||||||
|
{{ __automation_orchestrator_oidc_config | combine({
|
||||||
|
'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value,
|
||||||
|
'group_mapping_entries': __automation_orchestrator_oidc_entries}) }}
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- __automation_orchestrator_oidc_existing | length > 0
|
||||||
|
- __automation_orchestrator_oidc_needs_update | bool
|
||||||
|
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 5: Summary
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Display Automation Orchestrator OIDC summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "Keycloak OIDC configured for Automation Orchestrator:"
|
||||||
|
- " Login URL : {{ __automation_orchestrator_oidc_url }}"
|
||||||
|
- " Issuer : {{ __automation_orchestrator_oidc_issuer }}"
|
||||||
|
- " Client : {{ automation_orchestrator_oidc_client_id }}"
|
||||||
|
- " Mappings : {{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group')
|
||||||
|
| zip(automation_orchestrator_oidc_group_mappings | map(attribute='group'))
|
||||||
|
| map('join', ' -> ') | join(', ') }}"
|
||||||
|
- ""
|
||||||
|
- "Add users to the mapped Keycloak groups to grant access."
|
||||||
8
roles/automation_orchestrator_oidc/vars/main.yml
Normal file
8
roles/automation_orchestrator_oidc/vars/main.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
# Computed internal variables - do not override
|
||||||
|
__automation_orchestrator_oidc_valid_groups:
|
||||||
|
- admins
|
||||||
|
- auditors
|
||||||
|
- users
|
||||||
|
__automation_orchestrator_oidc_issuer: >-
|
||||||
|
{{ automation_orchestrator_oidc_keycloak_url | regex_replace('/$', '') }}/realms/{{ automation_orchestrator_oidc_keycloak_realm }}
|
||||||
81
roles/automation_orchestrator_operator/defaults/main.yml
Normal file
81
roles/automation_orchestrator_operator/defaults/main.yml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
# --- OLM subscription ---
|
||||||
|
# Automation Orchestrator is published in redhat-operators. To install a pre-release
|
||||||
|
# build instead (as aapctl's scripts/create-catalogsources.sh does), set:
|
||||||
|
# automation_orchestrator_operator_catalog_source_name: cs-automation-orchestrator
|
||||||
|
# automation_orchestrator_operator_catalog_image: quay.io/aap/ansible-automation-platform/automation-orchestrator-operator-index@sha256:...
|
||||||
|
# plus automation_orchestrator_operator_image_digest_mirrors below.
|
||||||
|
#
|
||||||
|
# To adopt an existing install (e.g. from OperatorHub), point operator_namespace at
|
||||||
|
# it: an existing OperatorGroup in that namespace is reused, not duplicated, and an
|
||||||
|
# already-installed CSV skips InstallPlan approval.
|
||||||
|
automation_orchestrator_operator_operator_namespace: automation-orchestrator
|
||||||
|
automation_orchestrator_operator_channel: stable
|
||||||
|
automation_orchestrator_operator_name: automation-orchestrator-operator
|
||||||
|
automation_orchestrator_operator_catalog_source_name: redhat-operators
|
||||||
|
# Index image for a custom CatalogSource; "" uses the existing catalog_source_name as-is.
|
||||||
|
automation_orchestrator_operator_catalog_image: ""
|
||||||
|
# Manual: the role approves the initial InstallPlan only (upgrades stay pending).
|
||||||
|
automation_orchestrator_operator_install_plan_approval: Manual
|
||||||
|
automation_orchestrator_operator_wait_timeout: 600
|
||||||
|
|
||||||
|
# --- Pre-release image mirrors (ImageDigestMirrorSet) ---
|
||||||
|
# Only needed for pre-release builds whose images are not on registry.redhat.io.
|
||||||
|
# Cluster-wide; applying it triggers a MachineConfigPool rollout. Pre-release example:
|
||||||
|
# - source: registry.redhat.io/ansible-automation-platform-27-next
|
||||||
|
# mirrors: [quay.io/aap/ansible-automation-platform-27-next]
|
||||||
|
# - source: registry.redhat.io/ansible-automation-platform-27
|
||||||
|
# mirrors: [quay.io/aap/ansible-automation-platform-27-next]
|
||||||
|
automation_orchestrator_operator_idms_name: idms-aap-27-next
|
||||||
|
automation_orchestrator_operator_image_digest_mirrors: []
|
||||||
|
automation_orchestrator_operator_mcp_wait_timeout: 1800
|
||||||
|
|
||||||
|
# --- AutomationOrchestrator CR ---
|
||||||
|
# Same as the operator namespace by default (aapctl runs OLM + CR in one namespace);
|
||||||
|
# override only if you need to split the cluster-scoped operator from the app instance.
|
||||||
|
automation_orchestrator_operator_app_namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
automation_orchestrator_operator_instance_name: automation-orchestrator
|
||||||
|
|
||||||
|
# --- PostgreSQL (bring-your-own) ---
|
||||||
|
# Defaults match the Secrets/Service created by the cloudnativepg_operator role
|
||||||
|
# (run it first, or point these at a customer-managed PostgreSQL instance instead).
|
||||||
|
automation_orchestrator_operator_postgres_host: orchestrator-postgres-rw
|
||||||
|
automation_orchestrator_operator_postgres_port: 5432
|
||||||
|
automation_orchestrator_operator_postgres_ssl_mode: verify-ca
|
||||||
|
# Name of a pre-existing Secret (key ca.crt). CloudNativePG auto-generates
|
||||||
|
# "<cluster-name>-ca" for its Cluster CR. Only used with sslMode verify-ca/verify-full.
|
||||||
|
automation_orchestrator_operator_postgres_ca_cert_secret: orchestrator-postgres-ca
|
||||||
|
|
||||||
|
# Names of pre-existing Secrets (keys: database, username, password) — created by
|
||||||
|
# cloudnativepg_operator, or your own Secrets when using a customer-managed PostgreSQL.
|
||||||
|
automation_orchestrator_operator_backend_db_secret_name: orchestrator-postgres-secret
|
||||||
|
automation_orchestrator_operator_temporal_db_secret_name: temporal-postgres-secret
|
||||||
|
|
||||||
|
# --- Ingress ---
|
||||||
|
# Route (OpenShift) or Ingress (AKS); the operator manages the resource itself.
|
||||||
|
automation_orchestrator_operator_ingress_type: Route
|
||||||
|
automation_orchestrator_operator_ingress_host: ""
|
||||||
|
automation_orchestrator_operator_ingress_annotations: {}
|
||||||
|
# Only used when ingress_type is Ingress — name of a pre-existing kubernetes.io/tls Secret.
|
||||||
|
automation_orchestrator_operator_ingress_tls_secret_name: ""
|
||||||
|
|
||||||
|
# --- Component workload images (only needed for private registries) ---
|
||||||
|
automation_orchestrator_operator_image_pull_secrets: []
|
||||||
|
|
||||||
|
# --- Per-component overrides (replicas, resources, affinity, tolerations, nodeSelector) ---
|
||||||
|
# Passed through as-is into the matching spec.<component> block; leave {} for operator defaults.
|
||||||
|
automation_orchestrator_operator_backend: {}
|
||||||
|
automation_orchestrator_operator_ui: {}
|
||||||
|
automation_orchestrator_operator_worker: {}
|
||||||
|
automation_orchestrator_operator_background_worker: {}
|
||||||
|
automation_orchestrator_operator_redis: {}
|
||||||
|
automation_orchestrator_operator_temporal_server: {}
|
||||||
|
|
||||||
|
# Escape hatch: recursively merged into the generated spec last, for fields
|
||||||
|
# (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...) not
|
||||||
|
# otherwise exposed as dedicated variables above.
|
||||||
|
automation_orchestrator_operator_extra_spec: {}
|
||||||
|
|
||||||
|
# --- Debugging ---
|
||||||
|
# Set to false to disable no_log and expose secret values in task output.
|
||||||
|
automation_orchestrator_operator_no_log: true
|
||||||
158
roles/automation_orchestrator_operator/meta/argument_specs.yml
Normal file
158
roles/automation_orchestrator_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Install Automation Orchestrator via its OLM operator (AutomationOrchestrator CR)
|
||||||
|
description:
|
||||||
|
- Installs the Automation Orchestrator operator via OLM (redhat-operators by default, or a
|
||||||
|
custom CatalogSource for pre-release builds) and creates a single
|
||||||
|
AutomationOrchestrator CR. Adopts an existing install (OperatorGroup, installed CSV)
|
||||||
|
when operator_namespace points at one.
|
||||||
|
- >-
|
||||||
|
Automation Orchestrator is bring-your-own-PostgreSQL: this role only references
|
||||||
|
existing K8s Secrets (keys: database, username, password) — it does not create the
|
||||||
|
database/user or their credential Secret. Defaults match the cloudnativepg_operator
|
||||||
|
role's output; run that role first, or point postgres_* variables and the secret-name
|
||||||
|
variables at a customer-managed PostgreSQL instance.
|
||||||
|
- Modeled on https://github.com/ansible/aapctl and https://github.com/automation-nexus/automation-orchestrator-operator.
|
||||||
|
options:
|
||||||
|
automation_orchestrator_operator_operator_namespace:
|
||||||
|
description: Namespace for the Automation Orchestrator operator and (by default) its CR.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
automation_orchestrator_operator_channel:
|
||||||
|
description: OLM subscription channel.
|
||||||
|
type: str
|
||||||
|
default: stable
|
||||||
|
automation_orchestrator_operator_name:
|
||||||
|
description: Operator package name in the catalog.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator-operator
|
||||||
|
automation_orchestrator_operator_catalog_source_name:
|
||||||
|
description: CatalogSource to subscribe from (created by this role only when catalog_image is set).
|
||||||
|
type: str
|
||||||
|
default: redhat-operators
|
||||||
|
automation_orchestrator_operator_catalog_image:
|
||||||
|
description: >-
|
||||||
|
Index image for a custom CatalogSource (e.g. a pre-release build). Empty string
|
||||||
|
uses the existing catalog_source_name without creating a CatalogSource.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
automation_orchestrator_operator_install_plan_approval:
|
||||||
|
description: >-
|
||||||
|
Subscription installPlanApproval (Manual or Automatic). With Manual, the role approves
|
||||||
|
only the initial InstallPlan; once a CSV is installed, upgrades are left pending.
|
||||||
|
type: str
|
||||||
|
choices: [Manual, Automatic]
|
||||||
|
default: Manual
|
||||||
|
automation_orchestrator_operator_wait_timeout:
|
||||||
|
description: Seconds to wait for the operator and instance to become ready.
|
||||||
|
type: int
|
||||||
|
default: 600
|
||||||
|
automation_orchestrator_operator_idms_name:
|
||||||
|
description: Name of the cluster-wide ImageDigestMirrorSet for pre-release images.
|
||||||
|
type: str
|
||||||
|
default: idms-aap-27-next
|
||||||
|
automation_orchestrator_operator_image_digest_mirrors:
|
||||||
|
description: >-
|
||||||
|
spec.imageDigestMirrors entries ({source, mirrors}) for the IDMS. Applying a change
|
||||||
|
triggers a MachineConfigPool rollout. Only needed for pre-release builds; [] skips it.
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
automation_orchestrator_operator_mcp_wait_timeout:
|
||||||
|
description: Seconds to wait for the MachineConfigPool rollout after the IDMS changes.
|
||||||
|
type: int
|
||||||
|
default: 1800
|
||||||
|
automation_orchestrator_operator_app_namespace:
|
||||||
|
description: >-
|
||||||
|
Namespace for the AutomationOrchestrator instance. Exactly one CR is allowed per namespace.
|
||||||
|
Defaults to the same namespace as the operator.
|
||||||
|
type: str
|
||||||
|
default: "{operator_namespace}"
|
||||||
|
automation_orchestrator_operator_instance_name:
|
||||||
|
description: Name of the AutomationOrchestrator CR.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
automation_orchestrator_operator_postgres_host:
|
||||||
|
description: PostgreSQL host shared by the backend and Temporal databases.
|
||||||
|
type: str
|
||||||
|
default: orchestrator-postgres-rw
|
||||||
|
automation_orchestrator_operator_postgres_port:
|
||||||
|
description: PostgreSQL port.
|
||||||
|
type: int
|
||||||
|
default: 5432
|
||||||
|
automation_orchestrator_operator_postgres_ssl_mode:
|
||||||
|
description: PostgreSQL SSL mode (disable, allow, prefer, require, verify-ca, verify-full).
|
||||||
|
type: str
|
||||||
|
default: verify-ca
|
||||||
|
automation_orchestrator_operator_postgres_ca_cert_secret:
|
||||||
|
description: >-
|
||||||
|
Name of a pre-existing Secret (key ca.crt) with the PostgreSQL server CA certificate.
|
||||||
|
Required when sslMode is verify-ca or verify-full.
|
||||||
|
type: str
|
||||||
|
default: orchestrator-postgres-ca
|
||||||
|
automation_orchestrator_operator_backend_db_secret_name:
|
||||||
|
description: Name of the pre-existing Secret with the backend database credentials.
|
||||||
|
type: str
|
||||||
|
default: orchestrator-postgres-secret
|
||||||
|
automation_orchestrator_operator_temporal_db_secret_name:
|
||||||
|
description: Name of the pre-existing Secret with the Temporal database credentials.
|
||||||
|
type: str
|
||||||
|
default: temporal-postgres-secret
|
||||||
|
automation_orchestrator_operator_ingress_type:
|
||||||
|
description: External access type (Route on OpenShift, Ingress on AKS, or None).
|
||||||
|
type: str
|
||||||
|
default: Route
|
||||||
|
automation_orchestrator_operator_ingress_host:
|
||||||
|
description: Custom hostname for external access. Leave unset to use the operator's auto-generated hostname.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
automation_orchestrator_operator_ingress_annotations:
|
||||||
|
description: Annotations applied to the generated Route/Ingress resource.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_ingress_tls_secret_name:
|
||||||
|
description: Name of a pre-existing kubernetes.io/tls Secret. Only used when ingress_type is Ingress.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
automation_orchestrator_operator_image_pull_secrets:
|
||||||
|
description: List of {name} references to Secrets used to pull component workload images.
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
automation_orchestrator_operator_backend:
|
||||||
|
description: Passthrough overrides (replicas, resources, affinity, tolerations, nodeSelector) for spec.backend.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_ui:
|
||||||
|
description: Passthrough overrides for spec.ui.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_worker:
|
||||||
|
description: Passthrough overrides for spec.worker.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_background_worker:
|
||||||
|
description: >-
|
||||||
|
Passthrough overrides for spec.backgroundWorker. Note: replicas has no effect —
|
||||||
|
the operator always manages this component via an HPA.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_redis:
|
||||||
|
description: Passthrough overrides for spec.redis.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_temporal_server:
|
||||||
|
description: Passthrough overrides for spec.temporal.server.
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_extra_spec:
|
||||||
|
description: >-
|
||||||
|
Arbitrary dict recursively merged into the generated CR spec last — escape hatch for
|
||||||
|
fields not otherwise exposed (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...).
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
automation_orchestrator_operator_no_log:
|
||||||
|
description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
18
roles/automation_orchestrator_operator/meta/main.yml
Normal file
18
roles/automation_orchestrator_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: ptoal
|
||||||
|
description: Install Automation Orchestrator via its OpenShift/AKS OLM operator
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.16"
|
||||||
|
platforms:
|
||||||
|
- name: GenericLinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- openshift
|
||||||
|
- aap
|
||||||
|
- operator
|
||||||
|
- olm
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
# Apply the ImageDigestMirrorSet that redirects pre-release AAP 2.7 images from
|
||||||
|
# registry.redhat.io to quay.io, then wait for the resulting MachineConfigPool
|
||||||
|
# rollout (matches https://github.com/ansible/aapctl scripts/create-catalogsources.sh).
|
||||||
|
#
|
||||||
|
# The IDMS is cluster-wide: it affects every digest pull from the source repos,
|
||||||
|
# not just Automation Orchestrator. The API may be briefly unavailable during the
|
||||||
|
# rollout on SNO, so the waits tolerate failed reads and keep retrying.
|
||||||
|
|
||||||
|
- name: Read MachineConfigPools before applying IDMS
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: machineconfiguration.openshift.io/v1
|
||||||
|
kind: MachineConfigPool
|
||||||
|
register: __automation_orchestrator_operator_mcp_before
|
||||||
|
|
||||||
|
- name: Create ImageDigestMirrorSet for pre-release images
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: config.openshift.io/v1
|
||||||
|
kind: ImageDigestMirrorSet
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_idms_name }}"
|
||||||
|
spec:
|
||||||
|
imageDigestMirrors: "{{ automation_orchestrator_operator_image_digest_mirrors }}"
|
||||||
|
register: __automation_orchestrator_operator_idms
|
||||||
|
|
||||||
|
# Without this, the Updated wait below could pass on the pools' pre-IDMS state
|
||||||
|
# before the MCO has rendered the new MachineConfig.
|
||||||
|
- name: Wait for MCO to render new MachineConfigs
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: machineconfiguration.openshift.io/v1
|
||||||
|
kind: MachineConfigPool
|
||||||
|
register: __automation_orchestrator_operator_mcp
|
||||||
|
until: >-
|
||||||
|
(__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and
|
||||||
|
(__automation_orchestrator_operator_mcp.resources
|
||||||
|
| map(attribute='spec.configuration.name')
|
||||||
|
| intersect(__automation_orchestrator_operator_mcp_before.resources | map(attribute='spec.configuration.name'))
|
||||||
|
| length) == 0
|
||||||
|
retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
when: __automation_orchestrator_operator_idms is changed
|
||||||
|
|
||||||
|
# Always runs, so a re-run also catches a rollout interrupted on a previous run.
|
||||||
|
- name: Wait for MachineConfigPool rollout to complete
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: machineconfiguration.openshift.io/v1
|
||||||
|
kind: MachineConfigPool
|
||||||
|
register: __automation_orchestrator_operator_mcp
|
||||||
|
until: >-
|
||||||
|
(__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and
|
||||||
|
(__automation_orchestrator_operator_mcp.resources | map(attribute='spec.configuration.name') | list) ==
|
||||||
|
(__automation_orchestrator_operator_mcp.resources | map(attribute='status.configuration.name', default='') | list) and
|
||||||
|
(__automation_orchestrator_operator_mcp.resources
|
||||||
|
| map(attribute='status.conditions', default=[])
|
||||||
|
| map('selectattr', 'type', '==', 'Updated')
|
||||||
|
| map('selectattr', 'status', '==', 'True')
|
||||||
|
| map('list')
|
||||||
|
| reject('equalto', [])
|
||||||
|
| list | length) == (__automation_orchestrator_operator_mcp.resources | length)
|
||||||
|
retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
271
roles/automation_orchestrator_operator/tasks/main.yml
Normal file
271
roles/automation_orchestrator_operator/tasks/main.yml
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
---
|
||||||
|
# Install Automation Orchestrator via its OpenShift OLM operator.
|
||||||
|
#
|
||||||
|
# Automation Orchestrator is bring-your-own-PostgreSQL: this role only creates the
|
||||||
|
# OperatorGroup/Subscription and the AutomationOrchestrator CR — it references
|
||||||
|
# existing K8s Secrets (keys: database/username/password, or host/port/database/
|
||||||
|
# username/password for CNPG's basic-auth secrets) rather than creating them.
|
||||||
|
# Pair with the cloudnativepg_operator role (defaults match its secret names) or
|
||||||
|
# point postgres_* variables at a customer-managed PostgreSQL instance.
|
||||||
|
#
|
||||||
|
# Modeled on https://github.com/ansible/aapctl (internal/install/automationorchestrator)
|
||||||
|
# and https://github.com/automation-nexus/automation-orchestrator-operator.
|
||||||
|
# With Manual install-plan approval (the default) this role approves the initial
|
||||||
|
# InstallPlan itself; later upgrade InstallPlans are left for manual approval.
|
||||||
|
#
|
||||||
|
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||||
|
|
||||||
|
- name: Verify PostgreSQL host is set
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- automation_orchestrator_operator_postgres_host | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
automation_orchestrator_operator_postgres_host must be set. Automation Orchestrator is
|
||||||
|
bring-your-own-PostgreSQL — either run the cloudnativepg_operator role first (its defaults
|
||||||
|
match this role's) or point this at a customer-managed PostgreSQL instance.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 0: Mirror pre-release AAP 2.7 images (IDMS) — triggers an MCP rollout
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Configure pre-release image mirrors
|
||||||
|
ansible.builtin.include_tasks: image_mirrors.yml
|
||||||
|
when: automation_orchestrator_operator_image_digest_mirrors | length > 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 1: Install Automation Orchestrator operator via OLM
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create Automation Orchestrator namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
|
||||||
|
- name: Create Automation Orchestrator app namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||||
|
when: automation_orchestrator_operator_app_namespace != automation_orchestrator_operator_operator_namespace
|
||||||
|
|
||||||
|
- name: Create CatalogSource for Automation Orchestrator
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: operators.coreos.com/v1alpha1
|
||||||
|
kind: CatalogSource
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_catalog_source_name }}"
|
||||||
|
namespace: openshift-marketplace
|
||||||
|
spec:
|
||||||
|
sourceType: grpc
|
||||||
|
image: "{{ automation_orchestrator_operator_catalog_image }}"
|
||||||
|
displayName: Automation Orchestrator
|
||||||
|
when: automation_orchestrator_operator_catalog_image | length > 0
|
||||||
|
|
||||||
|
# OLM allows only one OperatorGroup per namespace; reuse any existing one
|
||||||
|
# (e.g. the auto-named group an OperatorHub install creates).
|
||||||
|
- name: Look up existing OperatorGroups
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1
|
||||||
|
kind: OperatorGroup
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
register: __automation_orchestrator_operator_ogs
|
||||||
|
|
||||||
|
- name: Create OperatorGroup for Automation Orchestrator
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: operators.coreos.com/v1
|
||||||
|
kind: OperatorGroup
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_name }}"
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
spec: {}
|
||||||
|
when: __automation_orchestrator_operator_ogs.resources | length == 0
|
||||||
|
|
||||||
|
- name: Subscribe to Automation Orchestrator operator
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: operators.coreos.com/v1alpha1
|
||||||
|
kind: Subscription
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_name }}"
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
spec:
|
||||||
|
channel: "{{ automation_orchestrator_operator_channel }}"
|
||||||
|
installPlanApproval: "{{ automation_orchestrator_operator_install_plan_approval }}"
|
||||||
|
name: "{{ automation_orchestrator_operator_name }}"
|
||||||
|
source: "{{ automation_orchestrator_operator_catalog_source_name }}"
|
||||||
|
sourceNamespace: openshift-marketplace
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 2: Approve the initial InstallPlan (if needed) and wait for the CSV
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Once a CSV is installed, pending InstallPlans are upgrades — leave those for
|
||||||
|
# manual approval rather than approving them on every run.
|
||||||
|
- name: Wait for OLM to resolve the Subscription
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: Subscription
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_operator_name }}"
|
||||||
|
register: __automation_orchestrator_operator_sub
|
||||||
|
until: >-
|
||||||
|
__automation_orchestrator_operator_sub.resources | length > 0 and
|
||||||
|
((__automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('')) | length > 0 or
|
||||||
|
(__automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0)
|
||||||
|
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Resolve installed CSV name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_operator_csv_name: >-
|
||||||
|
{{ __automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('') }}
|
||||||
|
|
||||||
|
- name: Approve the initial InstallPlan
|
||||||
|
when: __automation_orchestrator_operator_csv_name | length == 0
|
||||||
|
block:
|
||||||
|
- name: Read the InstallPlan
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: InstallPlan
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||||
|
register: __automation_orchestrator_operator_installplan
|
||||||
|
|
||||||
|
- name: Approve the InstallPlan
|
||||||
|
kubernetes.core.k8s_json_patch:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: InstallPlan
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||||
|
patch:
|
||||||
|
- op: add
|
||||||
|
path: /spec/approved
|
||||||
|
value: true
|
||||||
|
when: not (__automation_orchestrator_operator_installplan.resources[0].spec.approved | default(false))
|
||||||
|
|
||||||
|
- name: Resolve CSV name from the InstallPlan
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_operator_csv_name: >-
|
||||||
|
{{ __automation_orchestrator_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }}
|
||||||
|
|
||||||
|
- name: Wait for Automation Orchestrator CSV to succeed
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: ClusterServiceVersion
|
||||||
|
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||||
|
name: "{{ __automation_orchestrator_operator_csv_name }}"
|
||||||
|
register: __automation_orchestrator_operator_csv
|
||||||
|
until: >-
|
||||||
|
__automation_orchestrator_operator_csv.resources | length > 0 and
|
||||||
|
__automation_orchestrator_operator_csv.resources[0].status.phase | default('') == 'Succeeded'
|
||||||
|
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Wait for AutomationOrchestrator CRD to be available
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
name: automationorchestrators.aap.ansible.com
|
||||||
|
register: __automation_orchestrator_operator_crd
|
||||||
|
until: __automation_orchestrator_operator_crd.resources | length > 0
|
||||||
|
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 3: Deploy the AutomationOrchestrator instance
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Build AutomationOrchestrator spec
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__automation_orchestrator_operator_spec: >-
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
'imagePullSecrets': automation_orchestrator_operator_image_pull_secrets,
|
||||||
|
'postgres': {
|
||||||
|
'host': automation_orchestrator_operator_postgres_host,
|
||||||
|
'port': automation_orchestrator_operator_postgres_port,
|
||||||
|
'sslMode': automation_orchestrator_operator_postgres_ssl_mode,
|
||||||
|
'backendDatabase': {'secretRef': {'name': automation_orchestrator_operator_backend_db_secret_name}},
|
||||||
|
'temporalDatabase': {'secretRef': {'name': automation_orchestrator_operator_temporal_db_secret_name}},
|
||||||
|
},
|
||||||
|
'ingress': {
|
||||||
|
'type': automation_orchestrator_operator_ingress_type,
|
||||||
|
'annotations': automation_orchestrator_operator_ingress_annotations,
|
||||||
|
},
|
||||||
|
'backend': automation_orchestrator_operator_backend,
|
||||||
|
'ui': automation_orchestrator_operator_ui,
|
||||||
|
'worker': automation_orchestrator_operator_worker,
|
||||||
|
'backgroundWorker': automation_orchestrator_operator_background_worker,
|
||||||
|
'redis': automation_orchestrator_operator_redis,
|
||||||
|
'temporal': {'server': automation_orchestrator_operator_temporal_server},
|
||||||
|
}
|
||||||
|
| combine(
|
||||||
|
{'postgres': {'caCertSecretRef': {'name': automation_orchestrator_operator_postgres_ca_cert_secret}}} if automation_orchestrator_operator_postgres_ca_cert_secret else {},
|
||||||
|
recursive=True
|
||||||
|
)
|
||||||
|
| combine(
|
||||||
|
{'ingress': {'host': automation_orchestrator_operator_ingress_host}} if automation_orchestrator_operator_ingress_host else {},
|
||||||
|
recursive=True
|
||||||
|
)
|
||||||
|
| combine(
|
||||||
|
{'ingress': {'tlsSecretRef': {'name': automation_orchestrator_operator_ingress_tls_secret_name}}}
|
||||||
|
if (automation_orchestrator_operator_ingress_type == 'Ingress' and automation_orchestrator_operator_ingress_tls_secret_name)
|
||||||
|
else {},
|
||||||
|
recursive=True
|
||||||
|
)
|
||||||
|
| combine(automation_orchestrator_operator_extra_spec, recursive=True)
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Create AutomationOrchestrator instance
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: aap.ansible.com/v1alpha1
|
||||||
|
kind: AutomationOrchestrator
|
||||||
|
metadata:
|
||||||
|
name: "{{ automation_orchestrator_operator_instance_name }}"
|
||||||
|
namespace: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||||
|
spec: "{{ __automation_orchestrator_operator_spec }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 4: Wait for the instance to be ready
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Wait for AutomationOrchestrator to be ready
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: aap.ansible.com/v1alpha1
|
||||||
|
kind: AutomationOrchestrator
|
||||||
|
namespace: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||||
|
name: "{{ automation_orchestrator_operator_instance_name }}"
|
||||||
|
register: __automation_orchestrator_operator_status
|
||||||
|
ignore_errors: true
|
||||||
|
until: >-
|
||||||
|
__automation_orchestrator_operator_status.resources is defined and
|
||||||
|
__automation_orchestrator_operator_status.resources | length > 0 and
|
||||||
|
(__automation_orchestrator_operator_status.resources[0].status.conditions | default([])
|
||||||
|
| selectattr('type', '==', 'Ready')
|
||||||
|
| selectattr('status', '==', 'True') | list | length > 0)
|
||||||
|
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 5: Display summary
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Display Automation Orchestrator deployment summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "Automation Orchestrator deployment complete!"
|
||||||
|
- " Namespace : {{ automation_orchestrator_operator_app_namespace }}"
|
||||||
|
- " Instance : {{ automation_orchestrator_operator_instance_name }}"
|
||||||
|
- " Ingress type: {{ automation_orchestrator_operator_ingress_type }}"
|
||||||
|
- ""
|
||||||
|
- "Find the UI hostname with: oc get routes -n {{ automation_orchestrator_operator_app_namespace }}"
|
||||||
|
- "The initial admin password is auto-generated unless spec.secrets.initialAdminPasswordSecretRef is set:"
|
||||||
|
- " oc get secret {{ automation_orchestrator_operator_instance_name }}-initial-admin-password -n {{ automation_orchestrator_operator_app_namespace }} -o jsonpath='{.data.password}' | base64 -d"
|
||||||
4
roles/automation_orchestrator_operator/vars/main.yml
Normal file
4
roles/automation_orchestrator_operator/vars/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
# Computed internal variables - do not override
|
||||||
|
__automation_orchestrator_operator_wait_retries: "{{ (automation_orchestrator_operator_wait_timeout / 10) | int }}"
|
||||||
|
__automation_orchestrator_operator_mcp_wait_retries: "{{ (automation_orchestrator_operator_mcp_wait_timeout / 10) | int }}"
|
||||||
53
roles/cloudnativepg_operator/defaults/main.yml
Normal file
53
roles/cloudnativepg_operator/defaults/main.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
# --- OLM subscription ---
|
||||||
|
# CloudNativePG is in the certified-operators catalog by default (no custom
|
||||||
|
# CatalogSource needed, unlike automation_orchestrator_operator).
|
||||||
|
cloudnativepg_operator_operator_namespace: cloudnative-pg
|
||||||
|
cloudnativepg_operator_channel: stable-v1
|
||||||
|
cloudnativepg_operator_name: cloudnative-pg
|
||||||
|
cloudnativepg_operator_catalog_source: certified-operators
|
||||||
|
cloudnativepg_operator_wait_timeout: 600
|
||||||
|
|
||||||
|
# --- Cluster CR ---
|
||||||
|
# Namespace for the Cluster, credential Secrets, and Database CRs. Defaults to
|
||||||
|
# the automation_orchestrator_operator app namespace — keep the two in sync if overridden.
|
||||||
|
cloudnativepg_operator_cluster_namespace: automation-orchestrator
|
||||||
|
cloudnativepg_operator_cluster_name: orchestrator-postgres
|
||||||
|
cloudnativepg_operator_owner_user: orchestrator
|
||||||
|
cloudnativepg_operator_instances: 1
|
||||||
|
# A primary PDB on a single instance is permanently at its limit (PodDisruptionBudgetAtLimit)
|
||||||
|
# and blocks node drains; CNPG recommends disabling it for single-instance dev clusters.
|
||||||
|
cloudnativepg_operator_enable_pdb: "{{ cloudnativepg_operator_instances | int > 1 }}"
|
||||||
|
cloudnativepg_operator_image_name: ghcr.io/cloudnative-pg/postgresql:15
|
||||||
|
cloudnativepg_operator_storage_size: 5Gi
|
||||||
|
cloudnativepg_operator_storage_class: ""
|
||||||
|
cloudnativepg_operator_max_connections: 200
|
||||||
|
|
||||||
|
# --- Databases (Database CRs, all owned by cloudnativepg_operator_owner_user) ---
|
||||||
|
cloudnativepg_operator_databases:
|
||||||
|
- name: orchestrator
|
||||||
|
dbname: orchestrator
|
||||||
|
- name: temporal
|
||||||
|
dbname: temporal
|
||||||
|
- name: temporal-visibility
|
||||||
|
dbname: temporal_visibility
|
||||||
|
|
||||||
|
# --- Credential secrets (type kubernetes.io/basic-auth) ---
|
||||||
|
# All share the same generated username/password; only the referenced database differs.
|
||||||
|
# secrets[0] is used as the CNPG bootstrap.initdb secret (creates the owner role + first DB).
|
||||||
|
cloudnativepg_operator_secrets:
|
||||||
|
- name: orchestrator-postgres-secret
|
||||||
|
database: orchestrator
|
||||||
|
- name: temporal-postgres-secret
|
||||||
|
database: temporal
|
||||||
|
- name: temporal-visibility-postgres-secret
|
||||||
|
database: temporal_visibility
|
||||||
|
|
||||||
|
# --- Vault ---
|
||||||
|
# KV2 path/key where the generated postgres password is stored and retrieved.
|
||||||
|
cloudnativepg_operator_vault_mount: kv
|
||||||
|
cloudnativepg_operator_vault_path: cloudnativepg/orchestrator-postgres
|
||||||
|
|
||||||
|
# --- Debugging ---
|
||||||
|
# Set to false to disable no_log and expose secret values in task output.
|
||||||
|
cloudnativepg_operator_no_log: true
|
||||||
94
roles/cloudnativepg_operator/meta/argument_specs.yml
Normal file
94
roles/cloudnativepg_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster + Databases
|
||||||
|
description:
|
||||||
|
- Installs the CloudNativePG operator via OLM (certified-operators catalog, Manual install-plan
|
||||||
|
approval which this role approves itself) and creates a single-instance PostgreSQL Cluster CR
|
||||||
|
with Database CRs for the orchestrator, temporal, and temporal_visibility databases — all owned
|
||||||
|
by one shared PostgreSQL role.
|
||||||
|
- >-
|
||||||
|
WARNING: this is a demo/dev-oriented, non-HA PostgreSQL deployment. For production, point
|
||||||
|
automation_orchestrator_operator at a customer-managed PostgreSQL instance instead.
|
||||||
|
- The generated owner-role password is stored in Vault at cloudnativepg_operator_vault_path
|
||||||
|
and reused on subsequent runs for idempotency.
|
||||||
|
options:
|
||||||
|
cloudnativepg_operator_operator_namespace:
|
||||||
|
description: Namespace for the CloudNativePG operator.
|
||||||
|
type: str
|
||||||
|
default: cloudnative-pg
|
||||||
|
cloudnativepg_operator_channel:
|
||||||
|
description: OLM subscription channel.
|
||||||
|
type: str
|
||||||
|
default: stable-v1
|
||||||
|
cloudnativepg_operator_name:
|
||||||
|
description: Operator package name in the catalog.
|
||||||
|
type: str
|
||||||
|
default: cloudnative-pg
|
||||||
|
cloudnativepg_operator_catalog_source:
|
||||||
|
description: Name of the CatalogSource to subscribe from.
|
||||||
|
type: str
|
||||||
|
default: certified-operators
|
||||||
|
cloudnativepg_operator_wait_timeout:
|
||||||
|
description: Seconds to wait for the operator, CSV, and Database CRs to become ready.
|
||||||
|
type: int
|
||||||
|
default: 600
|
||||||
|
cloudnativepg_operator_cluster_namespace:
|
||||||
|
description: Namespace for the Cluster CR, credential Secrets, and Database CRs.
|
||||||
|
type: str
|
||||||
|
default: automation-orchestrator
|
||||||
|
cloudnativepg_operator_cluster_name:
|
||||||
|
description: Name of the CloudNativePG Cluster CR.
|
||||||
|
type: str
|
||||||
|
default: orchestrator-postgres
|
||||||
|
cloudnativepg_operator_owner_user:
|
||||||
|
description: PostgreSQL role that owns all provisioned databases.
|
||||||
|
type: str
|
||||||
|
default: orchestrator
|
||||||
|
cloudnativepg_operator_instances:
|
||||||
|
description: Number of Postgres instances in the Cluster (1 = no HA).
|
||||||
|
type: int
|
||||||
|
default: 1
|
||||||
|
cloudnativepg_operator_enable_pdb:
|
||||||
|
description: >-
|
||||||
|
Create the CNPG primary PodDisruptionBudget. Defaults to true only when instances > 1;
|
||||||
|
a single-instance PDB is always at its limit and blocks node drains.
|
||||||
|
type: bool
|
||||||
|
cloudnativepg_operator_image_name:
|
||||||
|
description: PostgreSQL container image for the Cluster.
|
||||||
|
type: str
|
||||||
|
default: ghcr.io/cloudnative-pg/postgresql:15
|
||||||
|
cloudnativepg_operator_storage_size:
|
||||||
|
description: Size of the Cluster's PersistentVolumeClaim.
|
||||||
|
type: str
|
||||||
|
default: 5Gi
|
||||||
|
cloudnativepg_operator_storage_class:
|
||||||
|
description: StorageClass for the Cluster's PVC. Empty string uses the cluster default.
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
cloudnativepg_operator_max_connections:
|
||||||
|
description: postgresql.parameters.max_connections. Set to 0 to omit and use the Postgres default.
|
||||||
|
type: int
|
||||||
|
default: 200
|
||||||
|
cloudnativepg_operator_databases:
|
||||||
|
description: List of {name, dbname} Database CRs to create, all owned by cloudnativepg_operator_owner_user.
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
cloudnativepg_operator_secrets:
|
||||||
|
description: >-
|
||||||
|
List of {name, database} credential Secrets to create (type kubernetes.io/basic-auth), all
|
||||||
|
sharing the same generated username/password. secrets[0] is used as the CNPG bootstrap.initdb secret.
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
cloudnativepg_operator_vault_mount:
|
||||||
|
description: Vault KV2 mount point where the generated owner-role password is stored.
|
||||||
|
type: str
|
||||||
|
default: kv
|
||||||
|
cloudnativepg_operator_vault_path:
|
||||||
|
description: Vault KV2 path where the generated owner-role password is stored.
|
||||||
|
type: str
|
||||||
|
default: cloudnativepg/orchestrator-postgres
|
||||||
|
cloudnativepg_operator_no_log:
|
||||||
|
description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
18
roles/cloudnativepg_operator/meta/main.yml
Normal file
18
roles/cloudnativepg_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: ptoal
|
||||||
|
description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster for Automation Orchestrator
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.16"
|
||||||
|
platforms:
|
||||||
|
- name: GenericLinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- openshift
|
||||||
|
- postgresql
|
||||||
|
- cnpg
|
||||||
|
- operator
|
||||||
|
- olm
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
230
roles/cloudnativepg_operator/tasks/main.yml
Normal file
230
roles/cloudnativepg_operator/tasks/main.yml
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
---
|
||||||
|
# Install CloudNativePG via OLM and provision the PostgreSQL Cluster + Database CRs
|
||||||
|
# and credential Secrets that back Automation Orchestrator (backend + Temporal +
|
||||||
|
# Temporal Visibility databases, one shared owner role).
|
||||||
|
#
|
||||||
|
# WARNING: this is a single-instance (no HA), operator-provisioned PostgreSQL
|
||||||
|
# cluster intended for demo/dev use. For production, point automation_orchestrator_operator
|
||||||
|
# at a customer-managed PostgreSQL instance instead of running this role.
|
||||||
|
#
|
||||||
|
# Modeled on https://github.com/ansible/aapctl (internal/install/cloudnativepg).
|
||||||
|
# CloudNativePG's OLM subscription uses Manual install-plan approval, so this role
|
||||||
|
# approves the InstallPlan itself rather than relying on Automatic.
|
||||||
|
#
|
||||||
|
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||||
|
|
||||||
|
- name: Resolve postgres credentials from Vault
|
||||||
|
ansible.builtin.include_tasks: vault_secrets.yml
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 1: Install CloudNativePG operator via OLM
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create CloudNativePG operator namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
|
||||||
|
- name: Create CloudNativePG OperatorGroup
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: operators.coreos.com/v1
|
||||||
|
kind: OperatorGroup
|
||||||
|
metadata:
|
||||||
|
name: "{{ cloudnativepg_operator_name }}"
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
spec: {}
|
||||||
|
|
||||||
|
- name: Subscribe to CloudNativePG operator
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: operators.coreos.com/v1alpha1
|
||||||
|
kind: Subscription
|
||||||
|
metadata:
|
||||||
|
name: "{{ cloudnativepg_operator_name }}"
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
spec:
|
||||||
|
channel: "{{ cloudnativepg_operator_channel }}"
|
||||||
|
installPlanApproval: Manual
|
||||||
|
name: "{{ cloudnativepg_operator_name }}"
|
||||||
|
source: "{{ cloudnativepg_operator_catalog_source }}"
|
||||||
|
sourceNamespace: openshift-marketplace
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 2: Approve the Manual InstallPlan and wait for the CSV to succeed
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Wait for OLM to create an InstallPlan for the Subscription
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: Subscription
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
name: "{{ cloudnativepg_operator_name }}"
|
||||||
|
register: __cloudnativepg_operator_sub
|
||||||
|
until: >-
|
||||||
|
__cloudnativepg_operator_sub.resources | length > 0 and
|
||||||
|
(__cloudnativepg_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0
|
||||||
|
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Read the InstallPlan
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: InstallPlan
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||||
|
register: __cloudnativepg_operator_installplan
|
||||||
|
|
||||||
|
- name: Approve the InstallPlan
|
||||||
|
kubernetes.core.k8s_json_patch:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: InstallPlan
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||||
|
patch:
|
||||||
|
- op: add
|
||||||
|
path: /spec/approved
|
||||||
|
value: true
|
||||||
|
|
||||||
|
- name: Wait for CloudNativePG CSV to succeed
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: operators.coreos.com/v1alpha1
|
||||||
|
kind: ClusterServiceVersion
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
name: "{{ __cloudnativepg_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }}"
|
||||||
|
register: __cloudnativepg_operator_csv
|
||||||
|
until: >-
|
||||||
|
__cloudnativepg_operator_csv.resources | length > 0 and
|
||||||
|
__cloudnativepg_operator_csv.resources[0].status.phase | default('') == 'Succeeded'
|
||||||
|
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Wait for CloudNativePG controller manager webhook to be reachable
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Endpoints
|
||||||
|
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
name: cnpg-controller-manager-service
|
||||||
|
register: __cloudnativepg_operator_endpoints
|
||||||
|
until: >-
|
||||||
|
__cloudnativepg_operator_endpoints.resources | length > 0 and
|
||||||
|
(__cloudnativepg_operator_endpoints.resources[0].subsets | default([])
|
||||||
|
| map(attribute='addresses', default=[]) | flatten | length) > 0
|
||||||
|
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 3: Credential secrets (type kubernetes.io/basic-auth)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create cluster namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||||
|
|
||||||
|
- name: Create postgres credential secrets
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||||
|
type: kubernetes.io/basic-auth
|
||||||
|
stringData:
|
||||||
|
username: "{{ cloudnativepg_operator_owner_user }}"
|
||||||
|
password: "{{ __cloudnativepg_operator_password }}"
|
||||||
|
host: "{{ cloudnativepg_operator_cluster_name }}-rw.{{ cloudnativepg_operator_cluster_namespace }}.svc"
|
||||||
|
port: "5432"
|
||||||
|
database: "{{ item.database }}"
|
||||||
|
loop: "{{ cloudnativepg_operator_secrets }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 4: PostgreSQL Cluster
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create PostgreSQL Cluster
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Cluster
|
||||||
|
metadata:
|
||||||
|
name: "{{ cloudnativepg_operator_cluster_name }}"
|
||||||
|
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||||
|
spec: >-
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
'instances': cloudnativepg_operator_instances,
|
||||||
|
'enablePDB': cloudnativepg_operator_enable_pdb | bool,
|
||||||
|
'imageName': cloudnativepg_operator_image_name,
|
||||||
|
'bootstrap': {
|
||||||
|
'initdb': {
|
||||||
|
'owner': cloudnativepg_operator_owner_user,
|
||||||
|
'secret': {'name': cloudnativepg_operator_secrets[0].name},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'storage': {'size': cloudnativepg_operator_storage_size},
|
||||||
|
}
|
||||||
|
| combine({'storage': {'storageClass': cloudnativepg_operator_storage_class}} if cloudnativepg_operator_storage_class else {}, recursive=True)
|
||||||
|
| combine({'postgresql': {'parameters': {'max_connections': cloudnativepg_operator_max_connections | string}}} if cloudnativepg_operator_max_connections else {}, recursive=True)
|
||||||
|
}}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 5: Logical databases
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Create Database CRs
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Database
|
||||||
|
metadata:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||||
|
spec:
|
||||||
|
name: "{{ item.dbname }}"
|
||||||
|
owner: "{{ cloudnativepg_operator_owner_user }}"
|
||||||
|
cluster:
|
||||||
|
name: "{{ cloudnativepg_operator_cluster_name }}"
|
||||||
|
loop: "{{ cloudnativepg_operator_databases }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
|
||||||
|
- name: Wait for Database CRs to be applied
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: postgresql.cnpg.io/v1
|
||||||
|
kind: Database
|
||||||
|
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||||
|
register: __cloudnativepg_operator_databases_status
|
||||||
|
until: >-
|
||||||
|
__cloudnativepg_operator_databases_status.resources | length >= (cloudnativepg_operator_databases | length) and
|
||||||
|
(__cloudnativepg_operator_databases_status.resources
|
||||||
|
| rejectattr('status.applied', 'undefined')
|
||||||
|
| selectattr('status.applied')
|
||||||
|
| list | length) == (__cloudnativepg_operator_databases_status.resources | length)
|
||||||
|
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Step 6: Summary
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
- name: Display CloudNativePG deployment summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "CloudNativePG PostgreSQL cluster ready!"
|
||||||
|
- " Operator namespace : {{ cloudnativepg_operator_operator_namespace }}"
|
||||||
|
- " Cluster : {{ cloudnativepg_operator_cluster_name }} ({{ cloudnativepg_operator_cluster_namespace }})"
|
||||||
|
- " Owner role : {{ cloudnativepg_operator_owner_user }}"
|
||||||
|
- " Databases : {{ cloudnativepg_operator_databases | map(attribute='dbname') | join(', ') }}"
|
||||||
|
- " Credential secrets : {{ cloudnativepg_operator_secrets | map(attribute='name') | join(', ') }}"
|
||||||
26
roles/cloudnativepg_operator/tasks/vault_secrets.yml
Normal file
26
roles/cloudnativepg_operator/tasks/vault_secrets.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
# Read the shared postgres owner password from Vault; generate and store it if missing.
|
||||||
|
# Sets __cloudnativepg_operator_password, consumed by the K8s secret tasks in main.yml.
|
||||||
|
|
||||||
|
- name: Read cloudnativepg postgres password from Vault
|
||||||
|
community.hashi_vault.vault_kv2_get:
|
||||||
|
path: "{{ cloudnativepg_operator_vault_path }}"
|
||||||
|
engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}"
|
||||||
|
register: __cloudnativepg_operator_vault_read
|
||||||
|
failed_when: false
|
||||||
|
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||||
|
|
||||||
|
- name: Resolve postgres password (generate if missing)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__cloudnativepg_operator_password: >-
|
||||||
|
{{ (__cloudnativepg_operator_vault_read.secret | default({})).password
|
||||||
|
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||||
|
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||||
|
|
||||||
|
- name: Write postgres password to Vault
|
||||||
|
community.hashi_vault.vault_kv2_write:
|
||||||
|
path: "{{ cloudnativepg_operator_vault_path }}"
|
||||||
|
engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}"
|
||||||
|
data:
|
||||||
|
password: "{{ __cloudnativepg_operator_password }}"
|
||||||
|
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||||
3
roles/cloudnativepg_operator/vars/main.yml
Normal file
3
roles/cloudnativepg_operator/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
# Computed internal variables - do not override
|
||||||
|
__cloudnativepg_operator_wait_retries: "{{ (cloudnativepg_operator_wait_timeout / 10) | int }}"
|
||||||
@@ -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
|
|
||||||
13
roles/sno_alert_tuning/defaults/main.yml
Normal file
13
roles/sno_alert_tuning/defaults/main.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
# Platform alerts that are false positives on a single-control-plane cluster.
|
||||||
|
# Dropped via an AlertRelabelConfig (monitoring.openshift.io/v1), so they never
|
||||||
|
# reach Alertmanager. Alerts are matched by exact alertname.
|
||||||
|
#
|
||||||
|
# KubeCPUOvercommit / KubeMemoryOvercommit: the HA branch of these rules assumes
|
||||||
|
# the cluster survives losing its largest node. With one control-plane node plus
|
||||||
|
# smaller workers, that node is master-0 — losing it loses the cluster anyway.
|
||||||
|
sno_alert_tuning_dropped_alerts:
|
||||||
|
- KubeCPUOvercommit
|
||||||
|
- KubeMemoryOvercommit
|
||||||
|
|
||||||
|
sno_alert_tuning_config_name: sno-single-control-plane
|
||||||
17
roles/sno_alert_tuning/meta/argument_specs.yml
Normal file
17
roles/sno_alert_tuning/meta/argument_specs.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Drop platform alerts that are false positives on a single-control-plane cluster
|
||||||
|
description:
|
||||||
|
- Creates an AlertRelabelConfig in openshift-monitoring that drops the listed alerts
|
||||||
|
before they reach Alertmanager. An empty list removes the AlertRelabelConfig.
|
||||||
|
options:
|
||||||
|
sno_alert_tuning_dropped_alerts:
|
||||||
|
description: Exact alertnames to drop.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: [KubeCPUOvercommit, KubeMemoryOvercommit]
|
||||||
|
sno_alert_tuning_config_name:
|
||||||
|
description: Name of the AlertRelabelConfig.
|
||||||
|
type: str
|
||||||
|
default: sno-single-control-plane
|
||||||
16
roles/sno_alert_tuning/meta/main.yml
Normal file
16
roles/sno_alert_tuning/meta/main.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: ptoal
|
||||||
|
description: Drop platform alerts that are false positives on single-node OpenShift
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.16"
|
||||||
|
platforms:
|
||||||
|
- name: GenericLinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- openshift
|
||||||
|
- monitoring
|
||||||
|
- sno
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
19
roles/sno_alert_tuning/tasks/main.yml
Normal file
19
roles/sno_alert_tuning/tasks/main.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
# Drop platform alerts that only fire because the cluster has a single
|
||||||
|
# control-plane node. Uses the supported AlertRelabelConfig API rather than
|
||||||
|
# silences (which expire) or editing cluster-monitoring-operator rules (reverted).
|
||||||
|
|
||||||
|
- name: Drop single-control-plane false-positive alerts
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: "{{ 'present' if sno_alert_tuning_dropped_alerts | length > 0 else 'absent' }}"
|
||||||
|
definition:
|
||||||
|
apiVersion: monitoring.openshift.io/v1
|
||||||
|
kind: AlertRelabelConfig
|
||||||
|
metadata:
|
||||||
|
name: "{{ sno_alert_tuning_config_name }}"
|
||||||
|
namespace: openshift-monitoring
|
||||||
|
spec:
|
||||||
|
configs:
|
||||||
|
- sourceLabels: [alertname]
|
||||||
|
regex: "{{ sno_alert_tuning_dropped_alerts | map('regex_escape') | join('|') }}"
|
||||||
|
action: Drop
|
||||||
9
roles/sno_etcd_defrag/defaults/main.yml
Normal file
9
roles/sno_etcd_defrag/defaults/main.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# Defragment only when the in-use fraction of the etcd database drops below this
|
||||||
|
# ratio and the database exceeds the minimum size (mirrors the thresholds of the
|
||||||
|
# etcdDatabaseHighFragmentationRatio alert: < 50% in use and > 100 MiB).
|
||||||
|
sno_etcd_defrag_max_in_use_ratio: 0.5
|
||||||
|
sno_etcd_defrag_min_db_size_bytes: 104857600
|
||||||
|
sno_etcd_defrag_command_timeout: 60
|
||||||
|
# Set true to defragment regardless of the thresholds.
|
||||||
|
sno_etcd_defrag_force: false
|
||||||
26
roles/sno_etcd_defrag/meta/argument_specs.yml
Normal file
26
roles/sno_etcd_defrag/meta/argument_specs.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Defragment etcd on a single-control-plane OpenShift cluster
|
||||||
|
description:
|
||||||
|
- cluster-etcd-operator disables automatic defragmentation on SingleReplica topology.
|
||||||
|
This role defragments the local member when fragmentation exceeds the thresholds of
|
||||||
|
the etcdDatabaseHighFragmentationRatio alert, then clears any NOSPACE alarm.
|
||||||
|
- Blocks etcd for the duration of the defrag. Requires pods/exec in openshift-etcd.
|
||||||
|
options:
|
||||||
|
sno_etcd_defrag_max_in_use_ratio:
|
||||||
|
description: Defragment when dbSizeInUse / dbSize is below this ratio.
|
||||||
|
type: float
|
||||||
|
default: 0.5
|
||||||
|
sno_etcd_defrag_min_db_size_bytes:
|
||||||
|
description: Skip defragmentation when the database is smaller than this.
|
||||||
|
type: int
|
||||||
|
default: 104857600
|
||||||
|
sno_etcd_defrag_command_timeout:
|
||||||
|
description: etcdctl --command-timeout for the defrag, in seconds.
|
||||||
|
type: int
|
||||||
|
default: 60
|
||||||
|
sno_etcd_defrag_force:
|
||||||
|
description: Defragment regardless of the thresholds.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
16
roles/sno_etcd_defrag/meta/main.yml
Normal file
16
roles/sno_etcd_defrag/meta/main.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: ptoal
|
||||||
|
description: Defragment etcd on single-node OpenShift (automatic defrag is disabled there)
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.16"
|
||||||
|
platforms:
|
||||||
|
- name: GenericLinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- openshift
|
||||||
|
- etcd
|
||||||
|
- sno
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
103
roles/sno_etcd_defrag/tasks/main.yml
Normal file
103
roles/sno_etcd_defrag/tasks/main.yml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
# Defragment etcd on a single-control-plane cluster.
|
||||||
|
#
|
||||||
|
# cluster-etcd-operator disables its defrag controller on SingleReplica topology
|
||||||
|
# (Etcd/cluster condition DefragControllerDisabled=True), so fragmentation grows
|
||||||
|
# until etcdDatabaseHighFragmentationRatio fires. This role follows the documented
|
||||||
|
# manual procedure: defrag the local member, then clear any NOSPACE alarm.
|
||||||
|
#
|
||||||
|
# Defragmenting the only member blocks etcd reads/writes for the duration (seconds
|
||||||
|
# for a few hundred MiB) — schedule it outside busy periods.
|
||||||
|
#
|
||||||
|
# Requires pods/exec in openshift-etcd (cluster-admin kubeconfig).
|
||||||
|
|
||||||
|
- name: Find the etcd pod
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Pod
|
||||||
|
namespace: openshift-etcd
|
||||||
|
label_selectors:
|
||||||
|
- app=etcd
|
||||||
|
field_selectors:
|
||||||
|
- status.phase=Running
|
||||||
|
register: __sno_etcd_defrag_pods
|
||||||
|
failed_when: __sno_etcd_defrag_pods.resources | length != 1
|
||||||
|
|
||||||
|
- name: Set etcd pod name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__sno_etcd_defrag_pod: "{{ __sno_etcd_defrag_pods.resources[0].metadata.name }}"
|
||||||
|
|
||||||
|
- name: Read etcd database size before defrag
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: openshift-etcd
|
||||||
|
pod: "{{ __sno_etcd_defrag_pod }}"
|
||||||
|
container: etcdctl
|
||||||
|
command: etcdctl endpoint status -w json
|
||||||
|
register: __sno_etcd_defrag_before
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Evaluate fragmentation
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__sno_etcd_defrag_status_before: "{{ (__sno_etcd_defrag_before.stdout | from_json)[0].Status }}"
|
||||||
|
|
||||||
|
- name: Decide whether to defragment
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
__sno_etcd_defrag_needed: >-
|
||||||
|
{{ sno_etcd_defrag_force | bool or (
|
||||||
|
__sno_etcd_defrag_status_before.dbSize | int > sno_etcd_defrag_min_db_size_bytes | int and
|
||||||
|
(__sno_etcd_defrag_status_before.dbSizeInUse | float / __sno_etcd_defrag_status_before.dbSize | float)
|
||||||
|
< sno_etcd_defrag_max_in_use_ratio | float) }}
|
||||||
|
|
||||||
|
- name: Defragment etcd
|
||||||
|
when: __sno_etcd_defrag_needed | bool
|
||||||
|
block:
|
||||||
|
- name: Defragment the local etcd member
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: openshift-etcd
|
||||||
|
pod: "{{ __sno_etcd_defrag_pod }}"
|
||||||
|
container: etcdctl
|
||||||
|
command: >-
|
||||||
|
sh -c 'unset ETCDCTL_ENDPOINTS;
|
||||||
|
etcdctl --command-timeout={{ sno_etcd_defrag_command_timeout }}s --endpoints=https://localhost:2379 defrag'
|
||||||
|
register: __sno_etcd_defrag_result
|
||||||
|
changed_when: true
|
||||||
|
failed_when: __sno_etcd_defrag_result.rc != 0
|
||||||
|
|
||||||
|
- name: List etcd alarms
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: openshift-etcd
|
||||||
|
pod: "{{ __sno_etcd_defrag_pod }}"
|
||||||
|
container: etcdctl
|
||||||
|
command: etcdctl alarm list
|
||||||
|
register: __sno_etcd_defrag_alarms
|
||||||
|
changed_when: false
|
||||||
|
retries: 6
|
||||||
|
delay: 10
|
||||||
|
until: __sno_etcd_defrag_alarms.rc | default(1) == 0
|
||||||
|
|
||||||
|
- name: Disarm NOSPACE alarm
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: openshift-etcd
|
||||||
|
pod: "{{ __sno_etcd_defrag_pod }}"
|
||||||
|
container: etcdctl
|
||||||
|
command: etcdctl alarm disarm
|
||||||
|
changed_when: true
|
||||||
|
when: "'NOSPACE' in __sno_etcd_defrag_alarms.stdout"
|
||||||
|
|
||||||
|
- name: Read etcd database size after defrag
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: openshift-etcd
|
||||||
|
pod: "{{ __sno_etcd_defrag_pod }}"
|
||||||
|
container: etcdctl
|
||||||
|
command: etcdctl endpoint status -w json
|
||||||
|
register: __sno_etcd_defrag_after
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display etcd defrag summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "etcd {{ 'defragmented' if __sno_etcd_defrag_needed | bool else 'not defragmented (below thresholds)' }}"
|
||||||
|
- " Before : {{ (__sno_etcd_defrag_status_before.dbSize / 1048576) | round(0) | int }} MiB allocated,
|
||||||
|
{{ (__sno_etcd_defrag_status_before.dbSizeInUse / 1048576) | round(0) | int }} MiB in use"
|
||||||
|
- " After : {{ ((__sno_etcd_defrag_after.stdout | from_json)[0].Status.dbSize / 1048576) | round(0) | int ~ ' MiB allocated'
|
||||||
|
if __sno_etcd_defrag_needed | bool else 'n/a' }}"
|
||||||
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