P4: Miniflux migration + PostgreSQL consolidation (#33)
## 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
This commit is contained in:
parent
463f476374
commit
735b643429
25 changed files with 336 additions and 518 deletions
|
|
@ -2,59 +2,10 @@
|
||||||
- name: Configure indri
|
- name: Configure indri
|
||||||
hosts: 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: <var> is not defined')
|
# Each role also fetches its own credentials (with 'when: <var> is not defined')
|
||||||
# so they still work when running with --tags
|
# so they still work when running with --tags
|
||||||
# Tags ensure pre_tasks only run when relevant roles are included
|
|
||||||
pre_tasks:
|
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
|
- name: Fetch borgmatic database password
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mw2bv5we7woicjza7hc6s44yvy --fields db-password --reveal
|
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mw2bv5we7woicjza7hc6s44yvy --fields db-password --reveal
|
||||||
|
|
@ -63,16 +14,13 @@
|
||||||
changed_when: false
|
changed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
check_mode: false
|
check_mode: false
|
||||||
tags: [postgresql]
|
tags: [borgmatic]
|
||||||
|
|
||||||
- name: Build PostgreSQL user password lookup
|
- name: Set borgmatic database password fact
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
postgresql_user_passwords:
|
borgmatic_db_password: "{{ _borgmatic_db_pw.stdout }}"
|
||||||
miniflux: "{{ _miniflux_db_pw.stdout }}"
|
|
||||||
borgmatic: "{{ _borgmatic_db_pw.stdout }}"
|
|
||||||
alloy: "{{ _pg_alloy_pw.stdout }}"
|
|
||||||
no_log: true
|
no_log: true
|
||||||
tags: [postgresql]
|
tags: [borgmatic]
|
||||||
|
|
||||||
roles:
|
roles:
|
||||||
- role: loki
|
- role: loki
|
||||||
|
|
@ -110,9 +58,6 @@
|
||||||
tags: minikube_metrics
|
tags: minikube_metrics
|
||||||
- role: plex_metrics
|
- role: plex_metrics
|
||||||
tags: plex_metrics
|
tags: plex_metrics
|
||||||
- role: postgresql
|
# NOTE: postgresql and miniflux roles removed - now hosted in k8s
|
||||||
tags: postgresql
|
|
||||||
- role: miniflux
|
|
||||||
tags: miniflux
|
|
||||||
- role: tailscale_serve
|
- role: tailscale_serve
|
||||||
tags: tailscale-serve
|
tags: tailscale-serve
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,7 @@ alloy_brew_logs:
|
||||||
- path: /opt/homebrew/var/transmission/transmission-daemon.log
|
- path: /opt/homebrew/var/transmission/transmission-daemon.log
|
||||||
service: transmission
|
service: transmission
|
||||||
stream: stdout
|
stream: stdout
|
||||||
- path: /opt/homebrew/var/log/postgresql@18.log
|
# NOTE: postgresql and miniflux removed - now hosted in k8s
|
||||||
service: postgresql
|
|
||||||
stream: stdout
|
|
||||||
- path: /opt/homebrew/var/log/miniflux.log
|
|
||||||
service: miniflux
|
|
||||||
stream: stdout
|
|
||||||
|
|
||||||
alloy_mcquack_logs:
|
alloy_mcquack_logs:
|
||||||
- path: /Users/erichblume/Library/Logs/mcquack.devpi.out.log
|
- 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"
|
alloy_zot_metrics_url: "http://localhost:5050/metrics"
|
||||||
|
|
||||||
# PostgreSQL metrics collection
|
# 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_host: localhost
|
||||||
alloy_postgres_port: 5432
|
alloy_postgres_port: 5432
|
||||||
alloy_postgres_user: alloy
|
alloy_postgres_user: alloy
|
||||||
alloy_postgres_database: postgres
|
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_vault: vg6xf6vvfmoh5hqjjhlhbeoaie
|
||||||
alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui
|
alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui
|
||||||
alloy_op_postgres_field: alloy-user-pw
|
alloy_op_postgres_field: alloy-user-pw
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,12 @@ borgmatic_keep_monthly: 12
|
||||||
borgmatic_keep_yearly: 1000
|
borgmatic_keep_yearly: 1000
|
||||||
|
|
||||||
# PostgreSQL databases to backup (streamed via pg_dump)
|
# 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
|
# 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_pg_dump_command: /opt/homebrew/opt/postgresql@18/bin/pg_dump
|
||||||
borgmatic_postgresql_databases:
|
borgmatic_postgresql_databases:
|
||||||
# Brew PostgreSQL on indri (current production)
|
# k8s PostgreSQL (CloudNativePG)
|
||||||
- name: miniflux
|
- name: miniflux
|
||||||
hostname: localhost
|
hostname: pg.tail8d86e.ts.net
|
||||||
port: 5432
|
|
||||||
username: borgmatic
|
|
||||||
# k8s PostgreSQL (CloudNativePG) - backup both during migration
|
|
||||||
- name: miniflux
|
|
||||||
hostname: k8s-pg.tail8d86e.ts.net
|
|
||||||
port: 5432
|
port: 5432
|
||||||
username: borgmatic
|
username: borgmatic
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,17 @@
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0700'
|
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
|
- name: Deploy borgmatic configuration
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: config.yaml.j2
|
src: config.yaml.j2
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
- name: Restart miniflux
|
|
||||||
ansible.builtin.command: brew services restart miniflux
|
|
||||||
async: 120
|
|
||||||
poll: 0
|
|
||||||
changed_when: true
|
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
cmd: podman machine list --format json
|
cmd: podman machine list --format json
|
||||||
register: podman_machine_list
|
register: podman_machine_list
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false # Safe to run in check mode - read-only
|
||||||
|
|
||||||
- name: Initialize podman machine (if not exists)
|
- name: Initialize podman machine (if not exists)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
cmd: podman machine list --format "{{ '{{' }}.Running{{ '}}' }}"
|
cmd: podman machine list --format "{{ '{{' }}.Running{{ '}}' }}"
|
||||||
register: podman_running
|
register: podman_running
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
check_mode: false # Safe to run in check mode - read-only
|
||||||
|
|
||||||
- name: Start podman machine (if stopped)
|
- name: Start podman machine (if stopped)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
|
|
@ -52,4 +54,5 @@
|
||||||
msg: "WARNING: podman machine may not have started. Run 'podman machine start' manually on indri if needed."
|
msg: "WARNING: podman machine may not have started. Run 'podman machine start' manually on indri if needed."
|
||||||
when:
|
when:
|
||||||
- "'true' not in podman_running.stdout"
|
- "'true' not in podman_running.stdout"
|
||||||
|
- podman_start is defined
|
||||||
- podman_start.rc != 0 or "'started successfully' not in podman_start.stdout"
|
- podman_start.rc != 0 or "'started successfully' not in podman_start.stdout"
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
- name: Restart postgresql
|
|
||||||
ansible.builtin.command: brew services restart {{ postgresql_formula }}
|
|
||||||
async: 120
|
|
||||||
poll: 0
|
|
||||||
changed_when: true
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Each service maps a Tailscale service name to local endpoints
|
# Each service maps a Tailscale service name to local endpoints
|
||||||
|
|
||||||
tailscale_serve_services:
|
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
|
- name: svc:forge
|
||||||
https:
|
https:
|
||||||
|
|
@ -23,16 +23,6 @@ tailscale_serve_services:
|
||||||
port: 443
|
port: 443
|
||||||
upstream: http://127.0.0.1:3141
|
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
|
- name: svc:registry
|
||||||
https:
|
https:
|
||||||
port: 443
|
port: 443
|
||||||
|
|
|
||||||
28
argocd/apps/miniflux.yaml
Normal file
28
argocd/apps/miniflux.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
# Tailscale LoadBalancer for PostgreSQL access
|
# Tailscale LoadBalancer for PostgreSQL access
|
||||||
# Temporary service for testing during migration (k8s-pg.tail8d86e.ts.net)
|
# Canonical hostname: pg.tail8d86e.ts.net
|
||||||
# Will be replaced by pg.tail8d86e.ts.net in Phase 4
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: blumeops-pg-tailscale
|
name: blumeops-pg-tailscale
|
||||||
namespace: databases
|
namespace: databases
|
||||||
annotations:
|
annotations:
|
||||||
tailscale.com/hostname: "k8s-pg"
|
tailscale.com/hostname: "pg"
|
||||||
tailscale.com/proxy-class: "crio-compat"
|
tailscale.com/proxy-class: "crio-compat"
|
||||||
spec:
|
spec:
|
||||||
type: LoadBalancer
|
type: LoadBalancer
|
||||||
|
|
|
||||||
62
argocd/manifests/miniflux/README.md
Normal file
62
argocd/manifests/miniflux/README.md
Normal file
|
|
@ -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`
|
||||||
59
argocd/manifests/miniflux/deployment.yaml
Normal file
59
argocd/manifests/miniflux/deployment.yaml
Normal file
|
|
@ -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
|
||||||
17
argocd/manifests/miniflux/ingress-tailscale.yaml
Normal file
17
argocd/manifests/miniflux/ingress-tailscale.yaml
Normal file
|
|
@ -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
|
||||||
9
argocd/manifests/miniflux/kustomization.yaml
Normal file
9
argocd/manifests/miniflux/kustomization.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: miniflux
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-tailscale.yaml
|
||||||
13
argocd/manifests/miniflux/secret-db.yaml.tpl
Normal file
13
argocd/manifests/miniflux/secret-db.yaml.tpl
Normal file
|
|
@ -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
|
||||||
13
argocd/manifests/miniflux/service.yaml
Normal file
13
argocd/manifests/miniflux/service.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -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 "kiwix-serve" "ssh indri 'launchctl list | grep kiwix | grep -v \"^-\"'"
|
||||||
check_service "forgejo" "ssh indri 'brew services list | grep forgejo | grep started'"
|
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 "devpi" "ssh indri 'launchctl list | grep devpi | grep -v \"^-\"'"
|
||||||
check_service "postgresql" "ssh indri 'brew services list | grep postgresql | grep started'"
|
# NOTE: postgresql and miniflux moved to k8s - checked below
|
||||||
check_service "miniflux" "ssh indri 'brew services list | grep miniflux | grep started'"
|
|
||||||
check_service "zot" "ssh indri 'launchctl list | grep mcquack.eblume.zot | grep -v \"^-\"'"
|
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 "zot-metrics" "ssh indri 'launchctl list | grep zot-metrics | grep -v \"^-\"'"
|
||||||
check_service "minikube-metrics" "ssh indri 'launchctl list | grep minikube-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_service "Transmission RPC" "ssh indri 'curl -sf http://127.0.0.1:9091/transmission/rpc'"
|
||||||
# Check that transmission metrics are being collected
|
# Check that transmission metrics are being collected
|
||||||
check_service "Transmission metrics" "ssh indri 'test -f /opt/homebrew/var/node_exporter/textfile/transmission.prom'"
|
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)
|
# Zot registry (via Tailscale service)
|
||||||
check_http "Zot Registry" "https://registry.tail8d86e.ts.net/v2/_catalog"
|
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'"
|
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):"
|
echo "Kubernetes workloads (via Tailscale):"
|
||||||
check_http "ArgoCD" "https://argocd.tail8d86e.ts.net/healthz"
|
check_http "ArgoCD" "https://argocd.tail8d86e.ts.net/healthz"
|
||||||
# k8s PostgreSQL - check TCP connection (no auth needed for pg_isready)
|
# 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
|
# 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"
|
check_service "ArgoCD apps synced" "kubectl --context=minikube-indri get applications -n argocd -o jsonpath='{.items[*].status.sync.status}' | grep -v OutOfSync"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
This phase completed the miniflux migration and retired brew PostgreSQL:
|
||||||
|
1. Deployed miniflux container in k8s via ArgoCD
|
||||||
```yaml
|
2. Exposed via Tailscale Ingress at `feed.tail8d86e.ts.net`
|
||||||
image: ghcr.io/miniflux/miniflux:latest
|
3. Removed all miniflux infrastructure from indri (ansible role, brew service, Tailscale serve)
|
||||||
env:
|
4. Retired brew PostgreSQL (no longer needed)
|
||||||
DATABASE_URL: from secret
|
5. Renamed k8s-pg to pg (canonical Tailscale hostname)
|
||||||
RUN_MIGRATIONS: "1"
|
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
|
*Lessons learned and issues encountered*
|
||||||
- Feeds refresh
|
|
||||||
- API works
|
|
||||||
|
|
||||||
---
|
### 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
|
```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`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue