From 735b64342935dd6a5b2cb18b549ec70372db7648 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 20 Jan 2026 09:04:47 -0800 Subject: [PATCH] P4: Miniflux migration + PostgreSQL consolidation (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Deploy miniflux in k8s via ArgoCD - Expose via Tailscale Ingress at feed.tail8d86e.ts.net - Retire brew PostgreSQL (no longer needed) - Rename k8s-pg to pg (canonical hostname) - Remove ansible miniflux and postgresql roles - Update borgmatic to backup pg.tail8d86e.ts.net - Update all zk documentation ## Deployment and Testing - [x] Miniflux pod running in k8s - [x] User login works at https://feed.tail8d86e.ts.net - [x] Feeds and entries visible - [x] brew miniflux and postgresql stopped - [x] Tailscale services migrated (feed, pg) - [x] zk documentation updated - [x] Run ansible to apply role removals - [ ] Verify borgmatic backup with new pg hostname 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/33 --- ansible/playbooks/indri.yml | 67 +----- ansible/roles/alloy/defaults/main.yml | 12 +- ansible/roles/borgmatic/defaults/main.yml | 11 +- ansible/roles/borgmatic/tasks/main.yml | 11 + ansible/roles/miniflux/defaults/main.yml | 36 ---- ansible/roles/miniflux/handlers/main.yml | 6 - ansible/roles/miniflux/meta/main.yml | 4 - ansible/roles/miniflux/tasks/main.yml | 64 ------ .../roles/miniflux/templates/miniflux.conf.j2 | 32 --- ansible/roles/podman/tasks/main.yml | 3 + ansible/roles/postgresql/defaults/main.yml | 44 ---- ansible/roles/postgresql/handlers/main.yml | 6 - ansible/roles/postgresql/tasks/main.yml | 190 ------------------ .../roles/postgresql/templates/pg_hba.conf.j2 | 16 -- .../roles/tailscale_serve/defaults/main.yml | 12 +- argocd/apps/miniflux.yaml | 28 +++ .../databases/service-tailscale.yaml | 5 +- argocd/manifests/miniflux/README.md | 62 ++++++ argocd/manifests/miniflux/deployment.yaml | 59 ++++++ .../manifests/miniflux/ingress-tailscale.yaml | 17 ++ argocd/manifests/miniflux/kustomization.yaml | 9 + argocd/manifests/miniflux/secret-db.yaml.tpl | 13 ++ argocd/manifests/miniflux/service.yaml | 13 ++ mise-tasks/indri-services-check | 9 +- plans/k8s-migration/P4_miniflux.md | 125 +++++++++--- 25 files changed, 336 insertions(+), 518 deletions(-) delete mode 100644 ansible/roles/miniflux/defaults/main.yml delete mode 100644 ansible/roles/miniflux/handlers/main.yml delete mode 100644 ansible/roles/miniflux/meta/main.yml delete mode 100644 ansible/roles/miniflux/tasks/main.yml delete mode 100644 ansible/roles/miniflux/templates/miniflux.conf.j2 delete mode 100644 ansible/roles/postgresql/defaults/main.yml delete mode 100644 ansible/roles/postgresql/handlers/main.yml delete mode 100644 ansible/roles/postgresql/tasks/main.yml delete mode 100644 ansible/roles/postgresql/templates/pg_hba.conf.j2 create mode 100644 argocd/apps/miniflux.yaml create mode 100644 argocd/manifests/miniflux/README.md create mode 100644 argocd/manifests/miniflux/deployment.yaml create mode 100644 argocd/manifests/miniflux/ingress-tailscale.yaml create mode 100644 argocd/manifests/miniflux/kustomization.yaml create mode 100644 argocd/manifests/miniflux/secret-db.yaml.tpl create mode 100644 argocd/manifests/miniflux/service.yaml diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index 64fc407..dac4f5f 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -2,59 +2,10 @@ - name: Configure indri hosts: indri - # Fetch all 1Password credentials upfront to minimize prompts + # Fetch 1Password credentials upfront to minimize prompts # Each role also fetches its own credentials (with 'when: is not defined') # so they still work when running with --tags - # Tags ensure pre_tasks only run when relevant roles are included pre_tasks: - - name: Fetch PostgreSQL superuser password - ansible.builtin.command: - cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get guxu3j7ajhjyey6xxl2ovsl2ui --fields password --reveal - delegate_to: localhost - register: _pg_superuser_pw - changed_when: false - no_log: true - check_mode: false - tags: [postgresql] - - - name: Set PostgreSQL superuser password fact - ansible.builtin.set_fact: - postgresql_superuser_password: "{{ _pg_superuser_pw.stdout }}" - no_log: true - tags: [postgresql] - - - name: Fetch PostgreSQL alloy user password - ansible.builtin.command: - cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get guxu3j7ajhjyey6xxl2ovsl2ui --fields alloy-user-pw --reveal - delegate_to: localhost - register: _pg_alloy_pw - changed_when: false - no_log: true - check_mode: false - tags: [alloy, postgresql] - - - name: Set PostgreSQL alloy password fact - ansible.builtin.set_fact: - alloy_postgres_password: "{{ _pg_alloy_pw.stdout }}" - no_log: true - tags: [alloy, postgresql] - - - name: Fetch miniflux database password - ansible.builtin.command: - cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get ns6wylqiuqgczpo7gq2akaxbti --fields password --reveal - delegate_to: localhost - register: _miniflux_db_pw - changed_when: false - no_log: true - check_mode: false - tags: [miniflux, postgresql] - - - name: Set miniflux passwords fact - ansible.builtin.set_fact: - miniflux_db_password: "{{ _miniflux_db_pw.stdout }}" - no_log: true - tags: [miniflux, postgresql] - - name: Fetch borgmatic database password ansible.builtin.command: cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mw2bv5we7woicjza7hc6s44yvy --fields db-password --reveal @@ -63,16 +14,13 @@ changed_when: false no_log: true check_mode: false - tags: [postgresql] + tags: [borgmatic] - - name: Build PostgreSQL user password lookup + - name: Set borgmatic database password fact ansible.builtin.set_fact: - postgresql_user_passwords: - miniflux: "{{ _miniflux_db_pw.stdout }}" - borgmatic: "{{ _borgmatic_db_pw.stdout }}" - alloy: "{{ _pg_alloy_pw.stdout }}" + borgmatic_db_password: "{{ _borgmatic_db_pw.stdout }}" no_log: true - tags: [postgresql] + tags: [borgmatic] roles: - role: loki @@ -110,9 +58,6 @@ tags: minikube_metrics - role: plex_metrics tags: plex_metrics - - role: postgresql - tags: postgresql - - role: miniflux - tags: miniflux + # NOTE: postgresql and miniflux roles removed - now hosted in k8s - role: tailscale_serve tags: tailscale-serve diff --git a/ansible/roles/alloy/defaults/main.yml b/ansible/roles/alloy/defaults/main.yml index 117d703..ccc2d8d 100644 --- a/ansible/roles/alloy/defaults/main.yml +++ b/ansible/roles/alloy/defaults/main.yml @@ -40,12 +40,7 @@ alloy_brew_logs: - path: /opt/homebrew/var/transmission/transmission-daemon.log service: transmission stream: stdout - - path: /opt/homebrew/var/log/postgresql@18.log - service: postgresql - stream: stdout - - path: /opt/homebrew/var/log/miniflux.log - service: miniflux - stream: stdout + # NOTE: postgresql and miniflux removed - now hosted in k8s alloy_mcquack_logs: - path: /Users/erichblume/Library/Logs/mcquack.devpi.out.log @@ -86,13 +81,14 @@ alloy_collect_zot: true alloy_zot_metrics_url: "http://localhost:5050/metrics" # PostgreSQL metrics collection -alloy_collect_postgres: true +# NOTE: Disabled - brew postgresql removed, k8s CNPG metrics TBD +alloy_collect_postgres: false alloy_postgres_host: localhost alloy_postgres_port: 5432 alloy_postgres_user: alloy alloy_postgres_database: postgres -# 1Password settings for PostgreSQL metrics +# 1Password settings for PostgreSQL metrics (unused when alloy_collect_postgres is false) alloy_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui alloy_op_postgres_field: alloy-user-pw diff --git a/ansible/roles/borgmatic/defaults/main.yml b/ansible/roles/borgmatic/defaults/main.yml index 8fae283..e7807fd 100644 --- a/ansible/roles/borgmatic/defaults/main.yml +++ b/ansible/roles/borgmatic/defaults/main.yml @@ -41,17 +41,12 @@ borgmatic_keep_monthly: 12 borgmatic_keep_yearly: 1000 # PostgreSQL databases to backup (streamed via pg_dump) -# Password is read from ~/.pgpass (managed by postgresql role) +# Password is read from ~/.pgpass (managed by this role) # pg_dump_command must be full path since LaunchAgent doesn't have homebrew in PATH borgmatic_pg_dump_command: /opt/homebrew/opt/postgresql@18/bin/pg_dump borgmatic_postgresql_databases: - # Brew PostgreSQL on indri (current production) + # k8s PostgreSQL (CloudNativePG) - name: miniflux - hostname: localhost - port: 5432 - username: borgmatic - # k8s PostgreSQL (CloudNativePG) - backup both during migration - - name: miniflux - hostname: k8s-pg.tail8d86e.ts.net + hostname: pg.tail8d86e.ts.net port: 5432 username: borgmatic diff --git a/ansible/roles/borgmatic/tasks/main.yml b/ansible/roles/borgmatic/tasks/main.yml index aff9a6b..e5cc1f0 100644 --- a/ansible/roles/borgmatic/tasks/main.yml +++ b/ansible/roles/borgmatic/tasks/main.yml @@ -8,6 +8,17 @@ state: directory mode: '0700' +# .pgpass is used by pg_dump for database backups +# Password is fetched in playbook pre_tasks as borgmatic_db_password +- name: Write .pgpass file for borgmatic PostgreSQL backups + ansible.builtin.copy: + content: | + # Managed by ansible (borgmatic role) - k8s PostgreSQL backup credentials + pg.tail8d86e.ts.net:5432:*:borgmatic:{{ borgmatic_db_password }} + dest: ~/.pgpass + mode: '0600' + no_log: true + - name: Deploy borgmatic configuration ansible.builtin.template: src: config.yaml.j2 diff --git a/ansible/roles/miniflux/defaults/main.yml b/ansible/roles/miniflux/defaults/main.yml deleted file mode 100644 index 5419803..0000000 --- a/ansible/roles/miniflux/defaults/main.yml +++ /dev/null @@ -1,36 +0,0 @@ ---- -# Miniflux configuration - -# Network settings -miniflux_listen_addr: "127.0.0.1:8080" -miniflux_base_url: "https://feed.tail8d86e.ts.net/" - -# Database connection (password fetched from 1Password) -miniflux_db_host: localhost -miniflux_db_port: 5432 -miniflux_db_name: miniflux -miniflux_db_user: miniflux - -# Config paths -miniflux_config_file: /opt/homebrew/etc/miniflux.conf - -# 1Password settings for admin password -miniflux_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie -miniflux_op_item: ns6wylqiuqgczpo7gq2akaxbti - -# First run settings -# Set miniflux_create_admin to 1 for initial setup, then 0 after -miniflux_create_admin: 0 -miniflux_admin_username: admin - -# Always run migrations to keep schema updated -miniflux_run_migrations: 1 - -# Polling settings -miniflux_polling_frequency: 60 -miniflux_batch_size: 100 -miniflux_polling_scheduler: "entry_frequency" - -# Cleanup settings (archive old entries) -miniflux_cleanup_archive_unread_days: 180 -miniflux_cleanup_archive_read_days: 60 diff --git a/ansible/roles/miniflux/handlers/main.yml b/ansible/roles/miniflux/handlers/main.yml deleted file mode 100644 index cf38bf1..0000000 --- a/ansible/roles/miniflux/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Restart miniflux - ansible.builtin.command: brew services restart miniflux - async: 120 - poll: 0 - changed_when: true diff --git a/ansible/roles/miniflux/meta/main.yml b/ansible/roles/miniflux/meta/main.yml deleted file mode 100644 index b05a43b..0000000 --- a/ansible/roles/miniflux/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# Role ordering is controlled by indri.yml playbook - do not add dependencies here -# (Ansible's tag accumulation prevents proper deduplication when using meta dependencies) -dependencies: [] diff --git a/ansible/roles/miniflux/tasks/main.yml b/ansible/roles/miniflux/tasks/main.yml deleted file mode 100644 index 0d40284..0000000 --- a/ansible/roles/miniflux/tasks/main.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -# Miniflux installation and configuration -# -# Prerequisites: -# - PostgreSQL role has run (creates database, user, and ~/.miniflux-db-password) -# - 1Password CLI authenticated on control machine -# -# First run: -# mise run provision-indri -- --tags miniflux -e miniflux_create_admin=1 - -- name: Install miniflux via homebrew - community.general.homebrew: - name: miniflux - state: present - -# === Fetch passwords from 1Password === -# These are skipped when running full playbook (pre_tasks sets them) -# but run when using --tags miniflux - -- name: Fetch miniflux database password from 1Password - ansible.builtin.command: - cmd: op --vault {{ miniflux_op_vault }} item get {{ miniflux_op_item }} --fields password --reveal - delegate_to: localhost - register: miniflux_db_password_result - changed_when: false - no_log: true - when: miniflux_db_password is not defined - -- name: Set database password fact - ansible.builtin.set_fact: - miniflux_db_password: "{{ miniflux_db_password_result.stdout }}" - no_log: true - when: miniflux_db_password is not defined - -- name: Fetch miniflux admin password from 1Password (for first run) - ansible.builtin.command: - cmd: op --vault {{ miniflux_op_vault }} item get {{ miniflux_op_item }} --fields admin-password --reveal - delegate_to: localhost - register: miniflux_admin_password_result - changed_when: false - no_log: true - when: miniflux_create_admin | int == 1 - -- name: Set admin password fact - ansible.builtin.set_fact: - miniflux_admin_password: "{{ miniflux_admin_password_result.stdout }}" - no_log: true - when: miniflux_create_admin | int == 1 - -# === Deploy configuration === - -- name: Deploy miniflux configuration - ansible.builtin.template: - src: miniflux.conf.j2 - dest: "{{ miniflux_config_file }}" - mode: '0600' - notify: Restart miniflux - no_log: true - -- name: Ensure miniflux service is started - ansible.builtin.command: brew services start miniflux - register: miniflux_brew_start - changed_when: "'Successfully started' in miniflux_brew_start.stdout" - failed_when: false diff --git a/ansible/roles/miniflux/templates/miniflux.conf.j2 b/ansible/roles/miniflux/templates/miniflux.conf.j2 deleted file mode 100644 index 2e23e07..0000000 --- a/ansible/roles/miniflux/templates/miniflux.conf.j2 +++ /dev/null @@ -1,32 +0,0 @@ -# {{ ansible_managed }} -# Miniflux configuration - KEY=VALUE format -# Passwords fetched from 1Password at deploy time. - -# Server settings -LISTEN_ADDR={{ miniflux_listen_addr }} -BASE_URL={{ miniflux_base_url }} - -# Database connection -DATABASE_URL=postgres://{{ miniflux_db_user }}:{{ miniflux_db_password }}@{{ miniflux_db_host }}:{{ miniflux_db_port }}/{{ miniflux_db_name }}?sslmode=disable - -# Migrations (always run to keep schema updated) -RUN_MIGRATIONS={{ miniflux_run_migrations }} - -{% if miniflux_create_admin | int == 1 and miniflux_admin_password is defined %} -# First run admin creation (remove these after initial setup) -CREATE_ADMIN=1 -ADMIN_USERNAME={{ miniflux_admin_username }} -ADMIN_PASSWORD={{ miniflux_admin_password }} -{% endif %} - -# Polling settings -POLLING_FREQUENCY={{ miniflux_polling_frequency }} -BATCH_SIZE={{ miniflux_batch_size }} -POLLING_SCHEDULER={{ miniflux_polling_scheduler }} - -# Cleanup settings -CLEANUP_ARCHIVE_UNREAD_DAYS={{ miniflux_cleanup_archive_unread_days }} -CLEANUP_ARCHIVE_READ_DAYS={{ miniflux_cleanup_archive_read_days }} - -# Logging -LOG_LEVEL=info diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index 86a3cda..e1bb78f 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -24,6 +24,7 @@ cmd: podman machine list --format json register: podman_machine_list changed_when: false + check_mode: false # Safe to run in check mode - read-only - name: Initialize podman machine (if not exists) ansible.builtin.command: @@ -38,6 +39,7 @@ cmd: podman machine list --format "{{ '{{' }}.Running{{ '}}' }}" register: podman_running changed_when: false + check_mode: false # Safe to run in check mode - read-only - name: Start podman machine (if stopped) ansible.builtin.command: @@ -52,4 +54,5 @@ msg: "WARNING: podman machine may not have started. Run 'podman machine start' manually on indri if needed." when: - "'true' not in podman_running.stdout" + - podman_start is defined - podman_start.rc != 0 or "'started successfully' not in podman_start.stdout" diff --git a/ansible/roles/postgresql/defaults/main.yml b/ansible/roles/postgresql/defaults/main.yml deleted file mode 100644 index 29aa850..0000000 --- a/ansible/roles/postgresql/defaults/main.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -# PostgreSQL configuration - -# Superuser name (explicit, not inherited from OS user) -postgresql_superuser: eblume - -# Formula and version -postgresql_formula: postgresql@18 - -# Paths (keg-only formula on macOS) -postgresql_bin_dir: /opt/homebrew/opt/postgresql@18/bin -postgresql_data_dir: /opt/homebrew/var/postgresql@18 -postgresql_config_dir: /opt/homebrew/var/postgresql@18 - -# Network settings -postgresql_port: 5432 -postgresql_listen_addresses: "localhost" - -# 1Password vault and item IDs for credentials -postgresql_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie -postgresql_op_superuser_item: guxu3j7ajhjyey6xxl2ovsl2ui -postgresql_op_miniflux_item: ns6wylqiuqgczpo7gq2akaxbti -postgresql_op_borgmatic_item: mw2bv5we7woicjza7hc6s44yvy - -# Databases to create -postgresql_databases: - - name: miniflux - owner: miniflux - -# Users to create (passwords fetched from 1Password) -postgresql_users: - - name: miniflux - op_item: "{{ postgresql_op_miniflux_item }}" - op_field: password - - name: borgmatic - op_item: "{{ postgresql_op_borgmatic_item }}" - op_field: db-password - roles: - - pg_read_all_data - - name: alloy - op_item: "{{ postgresql_op_superuser_item }}" - op_field: alloy-user-pw - roles: - - pg_monitor diff --git a/ansible/roles/postgresql/handlers/main.yml b/ansible/roles/postgresql/handlers/main.yml deleted file mode 100644 index 0603e20..0000000 --- a/ansible/roles/postgresql/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Restart postgresql - ansible.builtin.command: brew services restart {{ postgresql_formula }} - async: 120 - poll: 0 - changed_when: true diff --git a/ansible/roles/postgresql/tasks/main.yml b/ansible/roles/postgresql/tasks/main.yml deleted file mode 100644 index c79ffd5..0000000 --- a/ansible/roles/postgresql/tasks/main.yml +++ /dev/null @@ -1,190 +0,0 @@ ---- -# PostgreSQL installation and configuration -# -# Passwords are fetched from 1Password at runtime using the `op` CLI. -# Requires: `op` authenticated on the control machine (run `op signin` first). - -- name: Install postgresql via homebrew - community.general.homebrew: - name: "{{ postgresql_formula }}" - state: present - -# === Fetch passwords from 1Password (on control machine) === -# These are skipped when running full playbook (pre_tasks sets them) -# but run when using --tags postgresql - -- name: Fetch superuser password from 1Password - ansible.builtin.command: - cmd: op --vault {{ postgresql_op_vault }} item get {{ postgresql_op_superuser_item }} --fields password --reveal - delegate_to: localhost - register: postgresql_superuser_password_result - changed_when: false - no_log: true - check_mode: false - when: postgresql_superuser_password is not defined - -- name: Set superuser password fact - ansible.builtin.set_fact: - postgresql_superuser_password: "{{ postgresql_superuser_password_result.stdout }}" - no_log: true - when: postgresql_superuser_password is not defined - -- name: Fetch user passwords from 1Password - ansible.builtin.command: - cmd: op --vault {{ postgresql_op_vault }} item get {{ item.op_item }} --fields {{ item.op_field }} --reveal - delegate_to: localhost - loop: "{{ postgresql_users }}" - register: postgresql_user_passwords_result - changed_when: false - no_log: true - check_mode: false - when: postgresql_user_passwords is not defined - -- name: Build user password lookup - ansible.builtin.set_fact: - postgresql_user_passwords: "{{ postgresql_user_passwords | default({}) | combine({item.item.name: item.stdout}) }}" - loop: "{{ postgresql_user_passwords_result.results }}" - no_log: true - when: postgresql_user_passwords is not defined - -# === Initialize PostgreSQL cluster === - -- name: Check if postgresql data directory is initialized - ansible.builtin.stat: - path: "{{ postgresql_data_dir }}/PG_VERSION" - register: postgresql_data_check - -- name: Create temporary password file for initdb - ansible.builtin.copy: - content: "{{ postgresql_superuser_password }}" - dest: /tmp/.pg_init_pwfile - mode: '0600' - when: not postgresql_data_check.stat.exists - no_log: true - -- name: Initialize postgresql database cluster with superuser password - ansible.builtin.command: > - {{ postgresql_bin_dir }}/initdb - -U {{ postgresql_superuser }} - --locale=en_US.UTF-8 -E UTF-8 - --pwfile=/tmp/.pg_init_pwfile - {{ postgresql_data_dir }} - when: not postgresql_data_check.stat.exists - changed_when: true - -- name: Remove temporary password file - ansible.builtin.file: - path: /tmp/.pg_init_pwfile - state: absent - when: not postgresql_data_check.stat.exists - -# === Configure and start PostgreSQL === - -- name: Deploy pg_hba.conf - ansible.builtin.template: - src: pg_hba.conf.j2 - dest: "{{ postgresql_config_dir }}/pg_hba.conf" - mode: '0600' - notify: Restart postgresql - -- name: Ensure postgresql service is started - ansible.builtin.command: brew services start {{ postgresql_formula }} - register: postgresql_brew_start - changed_when: "'Successfully started' in postgresql_brew_start.stdout" - failed_when: false - -- name: Wait for postgresql to accept connections - ansible.builtin.command: > - {{ postgresql_bin_dir }}/pg_isready -h localhost -p {{ postgresql_port }} - register: postgresql_ready - until: postgresql_ready.rc == 0 - retries: 10 - delay: 2 - changed_when: false - -# === Create users with passwords === - -- name: Check if postgresql users exist - ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -tAc - "SELECT 1 FROM pg_roles WHERE rolname = '{{ item.name }}';" - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_users }}" - register: postgresql_user_check - changed_when: false - failed_when: false - no_log: true - -- name: Create postgresql users with passwords - ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -c - "CREATE USER {{ item.item.name }} WITH PASSWORD '{{ postgresql_user_passwords[item.item.name] }}';" - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_user_check.results }}" - when: item.stdout != "1" - changed_when: true - no_log: true - -- name: Update postgresql user passwords (idempotent) - ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -c - "ALTER USER {{ item.name }} WITH PASSWORD '{{ postgresql_user_passwords[item.name] }}';" - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_users }}" - changed_when: false - no_log: true - -# === Grant roles to users === - -- name: Grant roles to users - ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} - -d postgres -c "GRANT {{ item.1 }} TO {{ item.0.name }};" - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_users | subelements('roles', skip_missing=True) }}" - changed_when: false - no_log: true - -# === Create databases === - -- name: Check if postgresql databases exist - ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -tAc - "SELECT 1 FROM pg_database WHERE datname = '{{ item.name }}';" - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_databases }}" - register: postgresql_db_check - changed_when: false - failed_when: false - no_log: true - -- name: Create postgresql databases - ansible.builtin.command: > - {{ postgresql_bin_dir }}/createdb -h localhost -U {{ postgresql_superuser }} - --owner={{ item.item.owner }} - {{ item.item.name }} - environment: - PGPASSWORD: "{{ postgresql_superuser_password }}" - loop: "{{ postgresql_db_check.results }}" - when: item.stdout != "1" - changed_when: true - no_log: true - -# === Write credential files for local access === - -# .pgpass is used by borgmatic for pg_dump backups -# Only includes read-only roles (borgmatic has pg_read_all_data) -- name: Write .pgpass file for borgmatic backups - ansible.builtin.copy: - content: | - # Managed by ansible - only read-only roles - localhost:{{ postgresql_port }}:*:borgmatic:{{ postgresql_user_passwords['borgmatic'] }} - k8s-pg.tail8d86e.ts.net:5432:*:borgmatic:{{ postgresql_user_passwords['borgmatic'] }} - dest: ~/.pgpass - mode: '0600' - no_log: true diff --git a/ansible/roles/postgresql/templates/pg_hba.conf.j2 b/ansible/roles/postgresql/templates/pg_hba.conf.j2 deleted file mode 100644 index 992897a..0000000 --- a/ansible/roles/postgresql/templates/pg_hba.conf.j2 +++ /dev/null @@ -1,16 +0,0 @@ -# {{ ansible_managed }} -# PostgreSQL Client Authentication Configuration File -# -# All connections require password authentication (scram-sha-256). -# Passwords are managed via 1Password and fetched by ansible at runtime. - -# TYPE DATABASE USER ADDRESS METHOD - -# Local connections (Unix socket) -local all all scram-sha-256 - -# IPv4 local connections (services connect via TCP) -host all all 127.0.0.1/32 scram-sha-256 - -# IPv6 local connections -host all all ::1/128 scram-sha-256 diff --git a/ansible/roles/tailscale_serve/defaults/main.yml b/ansible/roles/tailscale_serve/defaults/main.yml index ab80910..b8cd4ac 100644 --- a/ansible/roles/tailscale_serve/defaults/main.yml +++ b/ansible/roles/tailscale_serve/defaults/main.yml @@ -3,7 +3,7 @@ # Each service maps a Tailscale service name to local endpoints tailscale_serve_services: - # NOTE: svc:grafana removed - now hosted in k8s (see argocd/apps/grafana.yaml) + # NOTE: svc:grafana, svc:pg, svc:feed removed - now hosted in k8s - name: svc:forge https: @@ -23,16 +23,6 @@ tailscale_serve_services: port: 443 upstream: http://127.0.0.1:3141 - - name: svc:pg - tcp: - port: 5432 - upstream: tcp://localhost:5432 - - - name: svc:feed - https: - port: 443 - upstream: http://localhost:8080 - - name: svc:registry https: port: 443 diff --git a/argocd/apps/miniflux.yaml b/argocd/apps/miniflux.yaml new file mode 100644 index 0000000..d277058 --- /dev/null +++ b/argocd/apps/miniflux.yaml @@ -0,0 +1,28 @@ +# Miniflux RSS Reader +# Requires: CloudNativePG PostgreSQL cluster and manual secret setup +# +# Before syncing, create the database secret: +# kubectl create namespace miniflux +# op inject -i argocd/manifests/miniflux/secret-db.yaml.tpl | kubectl apply -f - +# +# Note: The Tailscale Ingress may initially get hostname "feed-1" if "feed" is +# already claimed. After clearing the old service, delete the device from +# Tailscale admin to allow the Ingress to claim "feed". +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: miniflux + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/miniflux + destination: + server: https://kubernetes.default.svc + namespace: miniflux + syncPolicy: + syncOptions: + - CreateNamespace=true + # Manual sync only - no automated sync on git push diff --git a/argocd/manifests/databases/service-tailscale.yaml b/argocd/manifests/databases/service-tailscale.yaml index 06d3e5d..7d816ff 100644 --- a/argocd/manifests/databases/service-tailscale.yaml +++ b/argocd/manifests/databases/service-tailscale.yaml @@ -1,13 +1,12 @@ # Tailscale LoadBalancer for PostgreSQL access -# Temporary service for testing during migration (k8s-pg.tail8d86e.ts.net) -# Will be replaced by pg.tail8d86e.ts.net in Phase 4 +# Canonical hostname: pg.tail8d86e.ts.net apiVersion: v1 kind: Service metadata: name: blumeops-pg-tailscale namespace: databases annotations: - tailscale.com/hostname: "k8s-pg" + tailscale.com/hostname: "pg" tailscale.com/proxy-class: "crio-compat" spec: type: LoadBalancer diff --git a/argocd/manifests/miniflux/README.md b/argocd/manifests/miniflux/README.md new file mode 100644 index 0000000..18780ca --- /dev/null +++ b/argocd/manifests/miniflux/README.md @@ -0,0 +1,62 @@ +# Miniflux Kubernetes Deployment + +RSS/Atom feed reader deployed via ArgoCD. + +## Prerequisites + +- CloudNativePG PostgreSQL cluster running in `databases` namespace +- Miniflux database and user created in PostgreSQL (from Phase 3 migration) +- Tailscale operator installed + +## Setup + +1. Create the namespace and database secret: + +```bash +kubectl create namespace miniflux + +# The miniflux user password is auto-generated by CNPG in blumeops-pg-app secret +kubectl create secret generic miniflux-db -n miniflux \ + --from-literal=url="$(kubectl -n databases get secret blumeops-pg-app -o jsonpath='{.data.uri}' | base64 -d)" +``` + +2. Apply the ArgoCD application: + +```bash +kubectl apply -f argocd/apps/miniflux.yaml +argocd app sync miniflux +``` + +## Access + +- URL: https://feed.tail8d86e.ts.net +- Exposed via Tailscale Ingress + +## Configuration + +Environment variables in `deployment.yaml`: +- `POLLING_FREQUENCY`: How often to check feeds (minutes) +- `BATCH_SIZE`: Number of feeds to refresh per interval +- `CLEANUP_ARCHIVE_UNREAD_DAYS`: Days to keep unread entries +- `CLEANUP_ARCHIVE_READ_DAYS`: Days to keep read entries + +## Management + +```bash +# View logs +kubectl -n miniflux logs -f deployment/miniflux + +# Restart deployment +kubectl -n miniflux rollout restart deployment/miniflux + +# Check health +curl https://feed.tail8d86e.ts.net/healthcheck +``` + +## Database Connection + +Connects to PostgreSQL via internal k8s DNS: +`blumeops-pg-rw.databases.svc.cluster.local:5432` + +The database is also accessible externally via Tailscale at: +`pg.tail8d86e.ts.net:5432` diff --git a/argocd/manifests/miniflux/deployment.yaml b/argocd/manifests/miniflux/deployment.yaml new file mode 100644 index 0000000..3884e1d --- /dev/null +++ b/argocd/manifests/miniflux/deployment.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: miniflux + namespace: miniflux +spec: + replicas: 1 + selector: + matchLabels: + app: miniflux + template: + metadata: + labels: + app: miniflux + spec: + containers: + - name: miniflux + image: ghcr.io/miniflux/miniflux:latest + ports: + - containerPort: 8080 + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: miniflux-db + key: url + - name: RUN_MIGRATIONS + value: "1" + - name: BASE_URL + value: "https://feed.tail8d86e.ts.net/" + - name: POLLING_FREQUENCY + value: "60" + - name: BATCH_SIZE + value: "100" + - name: POLLING_SCHEDULER + value: "entry_frequency" + - name: CLEANUP_ARCHIVE_UNREAD_DAYS + value: "180" + - name: CLEANUP_ARCHIVE_READ_DAYS + value: "60" + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: /healthcheck + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /healthcheck + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/argocd/manifests/miniflux/ingress-tailscale.yaml b/argocd/manifests/miniflux/ingress-tailscale.yaml new file mode 100644 index 0000000..e384f66 --- /dev/null +++ b/argocd/manifests/miniflux/ingress-tailscale.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: miniflux-tailscale + namespace: miniflux + annotations: + tailscale.com/proxy-class: "crio-compat" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: miniflux + port: + number: 8080 + tls: + - hosts: + - feed diff --git a/argocd/manifests/miniflux/kustomization.yaml b/argocd/manifests/miniflux/kustomization.yaml new file mode 100644 index 0000000..80927eb --- /dev/null +++ b/argocd/manifests/miniflux/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: miniflux + +resources: + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/miniflux/secret-db.yaml.tpl b/argocd/manifests/miniflux/secret-db.yaml.tpl new file mode 100644 index 0000000..462e407 --- /dev/null +++ b/argocd/manifests/miniflux/secret-db.yaml.tpl @@ -0,0 +1,13 @@ +# Miniflux database connection secret +# +# The miniflux user password is auto-generated by CloudNativePG and stored in +# blumeops-pg-app secret in the databases namespace. To create this secret: +# +# 1. Get the URI from CNPG secret: +# kubectl -n databases get secret blumeops-pg-app -o jsonpath='{.data.uri}' | base64 -d +# +# 2. Create the secret (one-liner): +# kubectl create secret generic miniflux-db -n miniflux \ +# --from-literal=url="$(kubectl -n databases get secret blumeops-pg-app -o jsonpath='{.data.uri}' | base64 -d)" +# +# Note: Uses internal k8s DNS hostname (blumeops-pg-rw.databases) not Tailscale diff --git a/argocd/manifests/miniflux/service.yaml b/argocd/manifests/miniflux/service.yaml new file mode 100644 index 0000000..a2cc8b0 --- /dev/null +++ b/argocd/manifests/miniflux/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: miniflux + namespace: miniflux +spec: + selector: + app: miniflux + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP diff --git a/mise-tasks/indri-services-check b/mise-tasks/indri-services-check index d669304..dcda013 100755 --- a/mise-tasks/indri-services-check +++ b/mise-tasks/indri-services-check @@ -51,8 +51,7 @@ check_service "transmission-metrics" "ssh indri 'launchctl list | grep transmiss check_service "kiwix-serve" "ssh indri 'launchctl list | grep kiwix | grep -v \"^-\"'" check_service "forgejo" "ssh indri 'brew services list | grep forgejo | grep started'" check_service "devpi" "ssh indri 'launchctl list | grep devpi | grep -v \"^-\"'" -check_service "postgresql" "ssh indri 'brew services list | grep postgresql | grep started'" -check_service "miniflux" "ssh indri 'brew services list | grep miniflux | grep started'" +# NOTE: postgresql and miniflux moved to k8s - checked below check_service "zot" "ssh indri 'launchctl list | grep mcquack.eblume.zot | grep -v \"^-\"'" check_service "zot-metrics" "ssh indri 'launchctl list | grep zot-metrics | grep -v \"^-\"'" check_service "minikube-metrics" "ssh indri 'launchctl list | grep minikube-metrics | grep -v \"^-\"'" @@ -70,8 +69,6 @@ check_http "Miniflux" "https://feed.tail8d86e.ts.net/healthcheck" check_service "Transmission RPC" "ssh indri 'curl -sf http://127.0.0.1:9091/transmission/rpc'" # Check that transmission metrics are being collected check_service "Transmission metrics" "ssh indri 'test -f /opt/homebrew/var/node_exporter/textfile/transmission.prom'" -# PostgreSQL uses TCP not HTTP, check via pg_isready -check_service "PostgreSQL" "ssh indri '/opt/homebrew/opt/postgresql@18/bin/pg_isready -h localhost'" # Zot registry (via Tailscale service) check_http "Zot Registry" "https://registry.tail8d86e.ts.net/v2/_catalog" check_service "Zot metrics file" "ssh indri 'test -f /opt/homebrew/var/node_exporter/textfile/zot.prom'" @@ -87,7 +84,9 @@ echo "" echo "Kubernetes workloads (via Tailscale):" check_http "ArgoCD" "https://argocd.tail8d86e.ts.net/healthz" # k8s PostgreSQL - check TCP connection (no auth needed for pg_isready) -check_service "k8s-pg" "pg_isready -h k8s-pg.tail8d86e.ts.net -p 5432" +check_service "PostgreSQL (k8s)" "pg_isready -h pg.tail8d86e.ts.net -p 5432" +# k8s miniflux pod +check_service "Miniflux pod" "kubectl --context=minikube-indri -n miniflux get pods -l app=miniflux -o jsonpath='{.items[0].status.phase}' | grep -q Running" # ArgoCD apps sync status check_service "ArgoCD apps synced" "kubectl --context=minikube-indri get applications -n argocd -o jsonpath='{.items[*].status.sync.status}' | grep -v OutOfSync" diff --git a/plans/k8s-migration/P4_miniflux.md b/plans/k8s-migration/P4_miniflux.md index c4a31a0..958ba05 100644 --- a/plans/k8s-migration/P4_miniflux.md +++ b/plans/k8s-migration/P4_miniflux.md @@ -1,48 +1,125 @@ -# Phase 4: Miniflux Migration +# Phase 4: Miniflux Migration to Kubernetes -**Goal**: Migrate Miniflux to k8s +**Goal**: Migrate Miniflux entirely off indri and onto k8s, retire brew PostgreSQL, rename k8s-pg to pg -**Status**: Pending +**Status**: Complete (2026-01-20) -**Prerequisites**: [Phase 3](P3_postgresql.md) complete +**Prerequisites**: [Phase 3](P3_postgresql.complete.md) complete --- -## Steps +## Overview -### 1. Deploy Miniflux - -```yaml -image: ghcr.io/miniflux/miniflux:latest -env: - DATABASE_URL: from secret - RUN_MIGRATIONS: "1" -``` +This phase completed the miniflux migration and retired brew PostgreSQL: +1. Deployed miniflux container in k8s via ArgoCD +2. Exposed via Tailscale Ingress at `feed.tail8d86e.ts.net` +3. Removed all miniflux infrastructure from indri (ansible role, brew service, Tailscale serve) +4. Retired brew PostgreSQL (no longer needed) +5. Renamed k8s-pg to pg (canonical Tailscale hostname) +6. Updated borgmatic to backup only `pg.tail8d86e.ts.net` +7. Updated all zk documentation --- -### 2. Configure Tailscale LoadBalancer +## New Files -Tag: `svc:feed` +| Path | Purpose | +|------|---------| +| `argocd/apps/miniflux.yaml` | ArgoCD Application definition | +| `argocd/manifests/miniflux/deployment.yaml` | Miniflux Deployment | +| `argocd/manifests/miniflux/service.yaml` | ClusterIP Service | +| `argocd/manifests/miniflux/ingress-tailscale.yaml` | Tailscale Ingress for `feed.tail8d86e.ts.net` | +| `argocd/manifests/miniflux/secret-db.yaml.tpl` | Database URL secret documentation | +| `argocd/manifests/miniflux/kustomization.yaml` | Kustomize configuration | +| `argocd/manifests/miniflux/README.md` | Setup instructions | + +## Modified Files + +| Path | Change | +|------|--------| +| `ansible/playbooks/indri.yml` | Removed miniflux and postgresql roles, simplified pre_tasks | +| `ansible/roles/tailscale_serve/defaults/main.yml` | Removed `svc:feed` and `svc:pg` entries | +| `ansible/roles/alloy/defaults/main.yml` | Removed miniflux and postgresql logs, disabled postgres metrics | +| `ansible/roles/borgmatic/defaults/main.yml` | Updated to backup only `pg.tail8d86e.ts.net` | +| `ansible/roles/borgmatic/tasks/main.yml` | Added .pgpass file management | +| `argocd/manifests/databases/service-tailscale.yaml` | Renamed hostname from k8s-pg to pg | + +## Deleted Files + +| Path | Reason | +|------|--------| +| `ansible/roles/miniflux/` | Entire role no longer needed | +| `ansible/roles/postgresql/` | Brew PostgreSQL no longer needed | --- -### 3. Update Alloy log collection +## Verification -Add k8s namespace +- [x] Miniflux pod healthy in k8s +- [x] https://feed.tail8d86e.ts.net accessible +- [x] User `eblume` can log in +- [x] Feeds visible and entries readable +- [x] `pg.tail8d86e.ts.net` resolves to k8s PostgreSQL +- [x] Old `k8s-pg` and `feed` devices removed from Tailscale +- [x] brew miniflux and postgresql services stopped +- [x] Tailscale serve entries cleared from indri +- [x] zk documentation updated --- -### 4. Verify +## Implementation Notes -- Login works -- Feeds refresh -- API works +*Lessons learned and issues encountered* ---- +### CNPG-Generated Password vs 1Password -### 5. Stop brew miniflux +**Problem**: Initial secret template used 1Password for miniflux database password, but CNPG auto-generates the bootstrap owner password. +**Solution**: Reference the CNPG-generated password from `blumeops-pg-app` secret: ```bash -brew services stop miniflux +kubectl create secret generic miniflux-db -n miniflux \ + --from-literal=url="$(kubectl -n databases get secret blumeops-pg-app -o jsonpath='{.data.uri}' | base64 -d)" ``` + +### Table Ownership Issue After P3 Restore + +**Problem**: Miniflux pod crashed with "permission denied for table schema_version". + +**Root cause**: P3 restore was run as the `eblume` superuser, so all tables were created owned by `eblume`, not `miniflux`. + +**Solution**: Transfer ownership of all tables to miniflux: +```sql +DO $$ +DECLARE r RECORD; +BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP + EXECUTE 'ALTER TABLE public.' || quote_ident(r.tablename) || ' OWNER TO miniflux'; + END LOOP; +END$$; +``` + +### Tailscale Ingress Hostname Suffix + +**Behavior**: When requesting a Tailscale hostname that's already taken, the operator adds a suffix (e.g., `feed-1`). + +**Workflow**: +1. Deploy initially - gets `feed-1.tail8d86e.ts.net` +2. Clear old `svc:feed` from indri +3. Delete old `feed` device from Tailscale admin +4. Delete and recreate the Ingress - now claims `feed` + +### Renaming Tailscale Service Hostname + +**Problem**: Changing the `tailscale.com/hostname` annotation doesn't automatically update the Tailscale device. + +**Solution**: Delete the service and let ArgoCD recreate it: +```bash +kubectl -n databases delete service blumeops-pg-tailscale +argocd app sync blumeops-pg +``` + +### .pgpass Management Migration + +**Issue**: The postgresql role managed `~/.pgpass` for borgmatic. With postgresql role deleted, borgmatic couldn't authenticate. + +**Solution**: Moved .pgpass management to the borgmatic role. Password is still fetched in playbook pre_tasks as `borgmatic_db_password`.