Add PostgreSQL and Miniflux services to tailnet (#16)
## Summary
- Add PostgreSQL 18 as a new service at `pg.tail8d86e.ts.net:5432`
- Add Miniflux RSS/Atom feed reader at `feed.tail8d86e.ts.net`
- Both services managed via homebrew/brew services
- Pulumi ACL tags added (tag:pg, tag:feed)
- Alloy log collection configured for both services
- Zettelkasten documentation updated
## Manual Setup Required
Before running ansible, the following steps are needed on indri:
### 1. Apply Pulumi tags
```bash
mise run tailnet-up
```
Then apply tags to indri in Tailscale admin console.
### 2. Create 1Password entries
- miniflux PostgreSQL user password
- miniflux admin password (for first run)
### 3. Set PostgreSQL user password (after ansible installs postgres)
```bash
ssh indri '/opt/homebrew/opt/postgresql@18/bin/psql -c "ALTER USER miniflux PASSWORD '\''your-password'\'';"'
```
### 4. Create password files on indri
```bash
ssh indri 'echo "your-db-password" > ~/.miniflux-db-password && chmod 600 ~/.miniflux-db-password'
ssh indri 'echo "your-admin-password" > ~/.miniflux-admin-password && chmod 600 ~/.miniflux-admin-password'
```
### 5. Create ~/.pgpass for borgmatic
```bash
ssh indri 'echo "localhost:5432:miniflux:miniflux:YOUR_PASSWORD" > ~/.pgpass && chmod 600 ~/.pgpass'
```
### 6. Run ansible with first-run admin creation
```bash
mise run provision-indri -- -e miniflux_create_admin=1
```
### 7. Update borgmatic config
Add to `~/.config/borgmatic/config.yaml` on indri:
```yaml
postgresql_databases:
- name: miniflux
hostname: localhost
port: 5432
username: miniflux
```
### 8. Cleanup after first run
```bash
ssh indri 'rm ~/.miniflux-admin-password'
```
## Test plan
- [ ] Run `mise run tailnet-up` and verify Pulumi changes
- [ ] Apply tags to indri in Tailscale admin
- [ ] Run `mise run provision-indri -- --check --diff` for dry run
- [ ] Run `mise run provision-indri -- -e miniflux_create_admin=1`
- [ ] Approve services in Tailscale admin
- [ ] Verify PostgreSQL: `ssh indri '/opt/homebrew/opt/postgresql@18/bin/pg_isready'`
- [ ] Verify Miniflux: `curl https://feed.tail8d86e.ts.net/healthcheck`
- [ ] Run `mise run indri-services-check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/16
This commit is contained in:
parent
3f4e40f3ae
commit
adf6f4fbe9
24 changed files with 1309 additions and 13 deletions
|
|
@ -20,6 +20,8 @@ This displays all cards tagged `blumeops`, with the main project card first and
|
||||||
|
|
||||||
You are encouraged to explore the zk, follow links, and propose updates to it as the project evolves. **Always keep the zettelkasten documentation up to date with any changes you make.**
|
You are encouraged to explore the zk, follow links, and propose updates to it as the project evolves. **Always keep the zettelkasten documentation up to date with any changes you make.**
|
||||||
|
|
||||||
|
Note: The zettelkasten is synced via Obsidian Sync, not git. You don't need to commit or push zk changes.
|
||||||
|
|
||||||
## Rules for all sessions
|
## Rules for all sessions
|
||||||
|
|
||||||
1. Always start by reading the zk docs with the command above.
|
1. Always start by reading the zk docs with the command above.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,75 @@
|
||||||
---
|
---
|
||||||
- name: Configure indri
|
- name: Configure indri
|
||||||
hosts: indri
|
hosts: indri
|
||||||
|
|
||||||
|
# Fetch all 1Password credentials upfront to minimize prompts
|
||||||
|
# Each role also fetches its own credentials (with 'when: <var> 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
|
||||||
|
tags: [postgresql]
|
||||||
|
|
||||||
|
- name: Set PostgreSQL superuser password fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pg_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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
delegate_to: localhost
|
||||||
|
register: _borgmatic_db_pw
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
tags: [postgresql]
|
||||||
|
|
||||||
|
- name: Build PostgreSQL user password lookup
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pg_user_passwords:
|
||||||
|
miniflux: "{{ _miniflux_db_pw.stdout }}"
|
||||||
|
borgmatic: "{{ _borgmatic_db_pw.stdout }}"
|
||||||
|
alloy: "{{ _pg_alloy_pw.stdout }}"
|
||||||
|
no_log: true
|
||||||
|
tags: [postgresql]
|
||||||
|
|
||||||
roles:
|
roles:
|
||||||
- role: loki
|
- role: loki
|
||||||
tags: loki
|
tags: loki
|
||||||
|
|
@ -26,5 +95,9 @@
|
||||||
tags: devpi_metrics
|
tags: devpi_metrics
|
||||||
- role: plex_metrics
|
- role: plex_metrics
|
||||||
tags: plex_metrics
|
tags: plex_metrics
|
||||||
|
- role: postgresql
|
||||||
|
tags: postgresql
|
||||||
|
- role: miniflux
|
||||||
|
tags: miniflux
|
||||||
- role: tailscale_serve
|
- role: tailscale_serve
|
||||||
tags: tailscale-serve
|
tags: tailscale-serve
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ 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
|
||||||
|
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
|
||||||
|
|
@ -68,3 +74,15 @@ alloy_plex_logs:
|
||||||
|
|
||||||
# Enable log collection (requires Loki to be running)
|
# Enable log collection (requires Loki to be running)
|
||||||
alloy_collect_logs: true
|
alloy_collect_logs: true
|
||||||
|
|
||||||
|
# PostgreSQL metrics collection
|
||||||
|
alloy_collect_postgres: true
|
||||||
|
alloy_postgres_host: localhost
|
||||||
|
alloy_postgres_port: 5432
|
||||||
|
alloy_postgres_user: alloy
|
||||||
|
alloy_postgres_database: postgres
|
||||||
|
|
||||||
|
# 1Password settings for PostgreSQL metrics
|
||||||
|
alloy_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie
|
||||||
|
alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui
|
||||||
|
alloy_op_postgres_field: alloy-user-pw
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,46 @@
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
|
|
||||||
|
# === Fetch PostgreSQL password from 1Password ===
|
||||||
|
# Skipped when running full playbook (pre_tasks sets it)
|
||||||
|
# but runs when using --tags alloy
|
||||||
|
|
||||||
|
- name: Fetch PostgreSQL metrics password from 1Password
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: op --vault {{ alloy_op_vault }} item get {{ alloy_op_postgres_item }} --fields {{ alloy_op_postgres_field }} --reveal
|
||||||
|
delegate_to: localhost
|
||||||
|
register: alloy_postgres_password_result
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- alloy_collect_postgres | default(false)
|
||||||
|
- alloy_postgres_password is not defined
|
||||||
|
|
||||||
|
- name: Set PostgreSQL password fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
alloy_postgres_password: "{{ alloy_postgres_password_result.stdout }}"
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- alloy_collect_postgres | default(false)
|
||||||
|
- alloy_postgres_password is not defined
|
||||||
|
|
||||||
|
# === Deploy configuration ===
|
||||||
|
|
||||||
|
- name: Deploy PostgreSQL custom queries config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: postgres_queries.yaml.j2
|
||||||
|
dest: "{{ alloy_config_dir }}/postgres_queries.yaml"
|
||||||
|
mode: '0600'
|
||||||
|
notify: restart alloy
|
||||||
|
when: alloy_collect_postgres | default(false)
|
||||||
|
|
||||||
- name: Deploy alloy configuration
|
- name: Deploy alloy configuration
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: config.alloy.j2
|
src: config.alloy.j2
|
||||||
dest: "{{ alloy_config_dir }}/config.alloy"
|
dest: "{{ alloy_config_dir }}/config.alloy"
|
||||||
mode: '0644'
|
mode: '0600'
|
||||||
notify: restart alloy
|
notify: restart alloy
|
||||||
|
no_log: true
|
||||||
|
|
||||||
- name: Ensure alloy service is started
|
- name: Ensure alloy service is started
|
||||||
ansible.builtin.command: brew services start grafana-alloy
|
ansible.builtin.command: brew services start grafana-alloy
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,25 @@ prometheus.remote_write "prometheus" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% if alloy_collect_postgres | default(false) %}
|
||||||
|
// ============== POSTGRESQL METRICS ==============
|
||||||
|
|
||||||
|
// PostgreSQL exporter (read-only metrics via pg_monitor role)
|
||||||
|
prometheus.exporter.postgres "postgresql" {
|
||||||
|
data_source_names = ["postgresql://{{ alloy_postgres_user }}:{{ alloy_postgres_password | urlencode }}@{{ alloy_postgres_host }}:{{ alloy_postgres_port }}/{{ alloy_postgres_database }}?sslmode=disable"]
|
||||||
|
|
||||||
|
// Custom queries for vacuum and XID monitoring
|
||||||
|
custom_queries_config_path = "/opt/homebrew/etc/grafana-alloy/postgres_queries.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrape PostgreSQL metrics
|
||||||
|
prometheus.scrape "postgresql" {
|
||||||
|
targets = prometheus.exporter.postgres.postgresql.targets
|
||||||
|
forward_to = [prometheus.relabel.instance.receiver]
|
||||||
|
scrape_interval = "{{ alloy_scrape_interval }}"
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if alloy_collect_logs %}
|
{% if alloy_collect_logs %}
|
||||||
// ============== LOG COLLECTION ==============
|
// ============== LOG COLLECTION ==============
|
||||||
|
|
||||||
|
|
|
||||||
20
ansible/roles/alloy/templates/postgres_queries.yaml.j2
Normal file
20
ansible/roles/alloy/templates/postgres_queries.yaml.j2
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Custom PostgreSQL queries for XID exhaustion monitoring
|
||||||
|
|
||||||
|
pg_database_xid_age:
|
||||||
|
query: |
|
||||||
|
SELECT datname,
|
||||||
|
age(datfrozenxid) as xid_age,
|
||||||
|
current_setting('autovacuum_freeze_max_age')::bigint as freeze_max_age
|
||||||
|
FROM pg_database
|
||||||
|
WHERE datallowconn
|
||||||
|
metrics:
|
||||||
|
- datname:
|
||||||
|
usage: "LABEL"
|
||||||
|
description: "Database name"
|
||||||
|
- xid_age:
|
||||||
|
usage: "GAUGE"
|
||||||
|
description: "Age of oldest unfrozen transaction ID"
|
||||||
|
- freeze_max_age:
|
||||||
|
usage: "GAUGE"
|
||||||
|
description: "autovacuum_freeze_max_age setting"
|
||||||
|
|
@ -1,7 +1,48 @@
|
||||||
---
|
---
|
||||||
borgmatic_config: /Users/erichblume/.config/borgmatic/config.yaml
|
borgmatic_config: /Users/erichblume/.config/borgmatic/config.yaml
|
||||||
|
borgmatic_config_dir: /Users/erichblume/.config/borgmatic
|
||||||
borgmatic_log_dir: /Users/erichblume/Library/Logs
|
borgmatic_log_dir: /Users/erichblume/Library/Logs
|
||||||
|
|
||||||
# Schedule: runs daily at 2:00 AM
|
# Schedule: runs daily at 2:00 AM
|
||||||
borgmatic_schedule_hour: 2
|
borgmatic_schedule_hour: 2
|
||||||
borgmatic_schedule_minute: 0
|
borgmatic_schedule_minute: 0
|
||||||
|
|
||||||
|
# Source directories to back up
|
||||||
|
borgmatic_source_directories:
|
||||||
|
- /Users/erichblume/code/personal/zk
|
||||||
|
- /opt/homebrew/var/forgejo
|
||||||
|
- /Users/erichblume/code/3rd/kiwix-tools
|
||||||
|
- /Users/erichblume/.config/borgmatic
|
||||||
|
- /Users/erichblume/Documents
|
||||||
|
- /Users/erichblume/Pictures
|
||||||
|
- /Users/erichblume/devpi
|
||||||
|
- /opt/homebrew/var/loki
|
||||||
|
|
||||||
|
# Backup repository
|
||||||
|
borgmatic_repositories:
|
||||||
|
- path: /Volumes/backups/borg/
|
||||||
|
label: sifaka-borg-backups
|
||||||
|
encryption: repokey
|
||||||
|
append_only: true
|
||||||
|
|
||||||
|
# Exclude patterns
|
||||||
|
borgmatic_exclude_patterns:
|
||||||
|
# Exclude mirrored PyPI cache (only backup private packages)
|
||||||
|
- /Users/erichblume/devpi/+files/root/pypi
|
||||||
|
- /opt/homebrew/var/loki
|
||||||
|
|
||||||
|
# Encryption passcommand (reads borg passphrase)
|
||||||
|
borgmatic_encryption_passcommand: cat /Users/erichblume/.borg/config.yaml
|
||||||
|
|
||||||
|
# Retention policy
|
||||||
|
borgmatic_keep_daily: 7
|
||||||
|
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)
|
||||||
|
borgmatic_postgresql_databases:
|
||||||
|
- name: miniflux
|
||||||
|
hostname: localhost
|
||||||
|
port: 5432
|
||||||
|
username: borgmatic
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
---
|
---
|
||||||
# Note: borgmatic is installed via mise (pipx), not managed here.
|
# Note: borgmatic is installed via mise (pipx), not managed here.
|
||||||
# This role manages only the scheduled LaunchAgent.
|
# This role manages the config file and scheduled LaunchAgent.
|
||||||
|
|
||||||
|
- name: Ensure borgmatic config directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ borgmatic_config_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Deploy borgmatic configuration
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: config.yaml.j2
|
||||||
|
dest: "{{ borgmatic_config }}"
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
- name: Deploy borgmatic LaunchAgent plist
|
- name: Deploy borgmatic LaunchAgent plist
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
|
|
|
||||||
45
ansible/roles/borgmatic/templates/config.yaml.j2
Normal file
45
ansible/roles/borgmatic/templates/config.yaml.j2
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
|
||||||
|
source_directories:
|
||||||
|
{% for dir in borgmatic_source_directories %}
|
||||||
|
- {{ dir }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
source_directories_must_exist: true
|
||||||
|
|
||||||
|
repositories:
|
||||||
|
{% for repo in borgmatic_repositories %}
|
||||||
|
- path: {{ repo.path }}
|
||||||
|
label: {{ repo.label }}
|
||||||
|
{% if repo.encryption is defined %}
|
||||||
|
encryption: {{ repo.encryption }}
|
||||||
|
{% endif %}
|
||||||
|
{% if repo.append_only is defined %}
|
||||||
|
append_only: {{ repo.append_only | lower }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if borgmatic_exclude_patterns %}
|
||||||
|
exclude_patterns:
|
||||||
|
{% for pattern in borgmatic_exclude_patterns %}
|
||||||
|
- {{ pattern }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
encryption_passcommand: {{ borgmatic_encryption_passcommand }}
|
||||||
|
|
||||||
|
# Retention policy
|
||||||
|
keep_daily: {{ borgmatic_keep_daily }}
|
||||||
|
keep_monthly: {{ borgmatic_keep_monthly }}
|
||||||
|
keep_yearly: {{ borgmatic_keep_yearly }}
|
||||||
|
|
||||||
|
{% if borgmatic_postgresql_databases %}
|
||||||
|
# PostgreSQL database backups (streamed via pg_dump)
|
||||||
|
postgresql_databases:
|
||||||
|
{% for db in borgmatic_postgresql_databases %}
|
||||||
|
- name: {{ db.name }}
|
||||||
|
hostname: {{ db.hostname | default('localhost') }}
|
||||||
|
port: {{ db.port | default(5432) }}
|
||||||
|
username: {{ db.username }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
594
ansible/roles/grafana/files/dashboards/postgresql.json
Normal file
594
ansible/roles/grafana/files/dashboards/postgresql.json
Normal file
|
|
@ -0,0 +1,594 @@
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null },
|
||||||
|
{ "color": "yellow", "value": 80 },
|
||||||
|
{ "color": "red", "value": 100 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pg_up",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "PostgreSQL Up",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pg_stat_activity_count{state=\"active\"}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Active Connections",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(pg_stat_activity_count)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Total Connections",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "decbytes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(pg_database_size_bytes)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Total Database Size",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pg_stat_activity_count",
|
||||||
|
"legendFormat": "{{state}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Connections by State",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "decbytes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pg_database_size_bytes{datname!~\"template.*\"}",
|
||||||
|
"legendFormat": "{{datname}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Database Size",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "ops"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 },
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_tup_fetched{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} fetched",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_tup_inserted{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} inserted",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_tup_updated{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} updated",
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_tup_deleted{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} deleted",
|
||||||
|
"refId": "D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Tuple Operations Rate",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 },
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_xact_commit{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} commits",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(pg_stat_database_xact_rollback{datname!~\"template.*\"}[5m])",
|
||||||
|
"legendFormat": "{{datname}} rollbacks",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Transactions Rate",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 6, "w": 12, "x": 12, "y": 20 },
|
||||||
|
"id": 10,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pg_database_xid_age_xid_age",
|
||||||
|
"legendFormat": "{{datname}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "XID Age Over Time",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["postgresql", "database"],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "PostgreSQL",
|
||||||
|
"uid": "postgresql",
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
36
ansible/roles/miniflux/defaults/main.yml
Normal file
36
ansible/roles/miniflux/defaults/main.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
# 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
|
||||||
5
ansible/roles/miniflux/handlers/main.yml
Normal file
5
ansible/roles/miniflux/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: restart miniflux
|
||||||
|
ansible.builtin.command: brew services restart miniflux
|
||||||
|
async: 120
|
||||||
|
poll: 0
|
||||||
3
ansible/roles/miniflux/meta/main.yml
Normal file
3
ansible/roles/miniflux/meta/main.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
dependencies:
|
||||||
|
- role: postgresql
|
||||||
64
ansible/roles/miniflux/tasks/main.yml
Normal file
64
ansible/roles/miniflux/tasks/main.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
# 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: brew_start
|
||||||
|
changed_when: "'Successfully started' in brew_start.stdout"
|
||||||
|
failed_when: false
|
||||||
32
ansible/roles/miniflux/templates/miniflux.conf.j2
Normal file
32
ansible/roles/miniflux/templates/miniflux.conf.j2
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# {{ 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
|
||||||
41
ansible/roles/postgresql/defaults/main.yml
Normal file
41
ansible/roles/postgresql/defaults/main.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
# PostgreSQL configuration
|
||||||
|
|
||||||
|
# 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
|
||||||
5
ansible/roles/postgresql/handlers/main.yml
Normal file
5
ansible/roles/postgresql/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: restart postgresql
|
||||||
|
ansible.builtin.command: brew services restart {{ postgresql_formula }}
|
||||||
|
async: 120
|
||||||
|
poll: 0
|
||||||
184
ansible/roles/postgresql/tasks/main.yml
Normal file
184
ansible/roles/postgresql/tasks/main.yml
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
---
|
||||||
|
# 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_formula }} 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: pg_superuser_password_result
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
when: pg_superuser_password is not defined
|
||||||
|
|
||||||
|
- name: Set superuser password fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pg_superuser_password: "{{ pg_superuser_password_result.stdout }}"
|
||||||
|
no_log: true
|
||||||
|
when: pg_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: pg_user_passwords_result
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
when: pg_user_passwords is not defined
|
||||||
|
|
||||||
|
- name: Build user password lookup
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pg_user_passwords: "{{ pg_user_passwords | default({}) | combine({item.item.name: item.stdout}) }}"
|
||||||
|
loop: "{{ pg_user_passwords_result.results }}"
|
||||||
|
no_log: true
|
||||||
|
when: pg_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: pg_data
|
||||||
|
|
||||||
|
- name: Create temporary password file for initdb
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ pg_superuser_password }}"
|
||||||
|
dest: /tmp/.pg_init_pwfile
|
||||||
|
mode: '0600'
|
||||||
|
when: not pg_data.stat.exists
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Initialize postgresql database cluster with superuser password
|
||||||
|
ansible.builtin.command: >
|
||||||
|
{{ postgresql_bin_dir }}/initdb
|
||||||
|
--locale=en_US.UTF-8 -E UTF-8
|
||||||
|
--pwfile=/tmp/.pg_init_pwfile
|
||||||
|
{{ postgresql_data_dir }}
|
||||||
|
when: not pg_data.stat.exists
|
||||||
|
|
||||||
|
- name: Remove temporary password file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /tmp/.pg_init_pwfile
|
||||||
|
state: absent
|
||||||
|
when: not pg_data.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: brew_start
|
||||||
|
changed_when: "'Successfully started' in 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: pg_ready
|
||||||
|
until: pg_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 -d postgres -tAc
|
||||||
|
"SELECT 1 FROM pg_roles WHERE rolname = '{{ item.name }}';"
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_superuser_password }}"
|
||||||
|
loop: "{{ postgresql_users }}"
|
||||||
|
register: 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 -d postgres -c
|
||||||
|
"CREATE USER {{ item.item.name }} WITH PASSWORD '{{ pg_user_passwords[item.item.name] }}';"
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_superuser_password }}"
|
||||||
|
loop: "{{ 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 -d postgres -c
|
||||||
|
"ALTER USER {{ item.name }} WITH PASSWORD '{{ pg_user_passwords[item.name] }}';"
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_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 -d postgres -c "GRANT {{ item.1 }} TO {{ item.0.name }};"
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_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 -d postgres -tAc
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname = '{{ item.name }}';"
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_superuser_password }}"
|
||||||
|
loop: "{{ postgresql_databases }}"
|
||||||
|
register: db_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Create postgresql databases
|
||||||
|
ansible.builtin.command: >
|
||||||
|
{{ postgresql_bin_dir }}/createdb -h localhost
|
||||||
|
--owner={{ item.item.owner }}
|
||||||
|
{{ item.item.name }}
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: "{{ pg_superuser_password }}"
|
||||||
|
loop: "{{ 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:{{ pg_user_passwords['borgmatic'] }}
|
||||||
|
dest: ~/.pgpass
|
||||||
|
mode: '0600'
|
||||||
|
no_log: true
|
||||||
16
ansible/roles/postgresql/templates/pg_hba.conf.j2
Normal file
16
ansible/roles/postgresql/templates/pg_hba.conf.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# {{ 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
|
||||||
|
|
@ -25,3 +25,13 @@ tailscale_services:
|
||||||
https:
|
https:
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ dependencies:
|
||||||
- role: forgejo
|
- role: forgejo
|
||||||
- role: kiwix
|
- role: kiwix
|
||||||
- role: devpi
|
- role: devpi
|
||||||
|
- role: miniflux
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,33 @@
|
||||||
register: serve_status
|
register: serve_status
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Parse serve status
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
serve_config: "{{ (serve_status.stdout | from_json).Services | default({}) }}"
|
||||||
|
|
||||||
|
# Configure HTTPS if service doesn't have Web config yet
|
||||||
- name: Configure HTTPS services
|
- name: Configure HTTPS services
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
tailscale serve --service="{{ item.name }}"
|
tailscale serve --service="{{ item.name }}"
|
||||||
--https={{ item.https.port }} {{ item.https.upstream }}
|
--https={{ item.https.port }} {{ item.https.upstream }}
|
||||||
loop: "{{ tailscale_services }}"
|
loop: "{{ tailscale_services }}"
|
||||||
when: item.https is defined
|
when:
|
||||||
|
- item.https is defined
|
||||||
|
- serve_config[item.name] is not defined or serve_config[item.name].Web is not defined
|
||||||
register: https_result
|
register: https_result
|
||||||
changed_when: "'already serving' not in https_result.stderr | default('')"
|
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
|
# Configure TCP if service doesn't have the specific port configured yet
|
||||||
- name: Configure TCP services
|
- name: Configure TCP services
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
tailscale serve --service="{{ item.name }}"
|
tailscale serve --service="{{ item.name }}"
|
||||||
--tcp={{ item.tcp.port }} {{ item.tcp.upstream }}
|
--tcp={{ item.tcp.port }} {{ item.tcp.upstream }}
|
||||||
loop: "{{ tailscale_services }}"
|
loop: "{{ tailscale_services }}"
|
||||||
when: item.tcp is defined
|
when:
|
||||||
|
- item.tcp is defined
|
||||||
|
- serve_config[item.name] is not defined or
|
||||||
|
serve_config[item.name].TCP is not defined or
|
||||||
|
serve_config[item.name].TCP[item.tcp.port | string] is not defined or
|
||||||
|
serve_config[item.name].TCP[item.tcp.port | string].TCPForward is not defined
|
||||||
register: tcp_result
|
register: tcp_result
|
||||||
changed_when: "'already serving' not in tcp_result.stderr | default('')"
|
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,28 @@ acl = tailscale.Acl(
|
||||||
acl=policy_content,
|
acl=policy_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ============== Device Tags ==============
|
||||||
|
# Manage tags for devices in the tailnet
|
||||||
|
|
||||||
|
# indri - Mac Mini M1 running homelab services
|
||||||
|
indri = tailscale.get_device(name="indri.tail8d86e.ts.net")
|
||||||
|
indri_tags = tailscale.DeviceTags(
|
||||||
|
"indri-tags",
|
||||||
|
device_id=indri.node_id,
|
||||||
|
tags=[
|
||||||
|
"tag:homelab",
|
||||||
|
"tag:grafana",
|
||||||
|
"tag:forge",
|
||||||
|
"tag:kiwix",
|
||||||
|
"tag:devpi",
|
||||||
|
"tag:loki",
|
||||||
|
"tag:pg",
|
||||||
|
"tag:feed",
|
||||||
|
"tag:blumeops",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# Export useful info
|
# Export useful info
|
||||||
pulumi.export("acl_id", acl.id)
|
pulumi.export("acl_id", acl.id)
|
||||||
|
pulumi.export("indri_device_id", indri.node_id)
|
||||||
|
pulumi.export("indri_tags", indri_tags.tags)
|
||||||
|
|
|
||||||
|
|
@ -72,25 +72,32 @@
|
||||||
|
|
||||||
"tagOwners": {
|
"tagOwners": {
|
||||||
// Grafana service host tag
|
// Grafana service host tag
|
||||||
"tag:grafana": ["autogroup:admin"],
|
"tag:grafana": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// This tag applies to instances which are meant to be accessible in my homelab. These instances can be SSH'ed in to by any member of the admin autogroup.
|
// This tag applies to instances which are meant to be accessible in my homelab. These instances can be SSH'ed in to by any member of the admin autogroup.
|
||||||
"tag:homelab": ["autogroup:admin"],
|
"tag:homelab": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// Kiwix, a local wiki server. I use it to create mirrors of wikipedia.
|
// Kiwix, a local wiki server. I use it to create mirrors of wikipedia.
|
||||||
"tag:kiwix": ["autogroup:admin"],
|
"tag:kiwix": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// Service tag for forgejo, scm host and code forge
|
// Service tag for forgejo, scm host and code forge
|
||||||
"tag:forge": ["autogroup:admin"],
|
"tag:forge": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// devpi pypi index
|
// devpi pypi index
|
||||||
"tag:devpi": ["autogroup:admin"],
|
"tag:devpi": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// Loki log collection
|
// Loki log collection
|
||||||
"tag:loki": ["autogroup:admin"],
|
"tag:loki": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
|
// PostgreSQL database server
|
||||||
|
"tag:pg": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
|
// Miniflux RSS/Atom feed reader
|
||||||
|
"tag:feed": ["autogroup:admin", "tag:blumeops"],
|
||||||
|
|
||||||
// This tag is applied to resources modified by blumeops-pulumi IaC
|
// This tag is applied to resources modified by blumeops-pulumi IaC
|
||||||
"tag:blumeops": ["autogroup:admin"],
|
// Includes itself so the OAuth client can apply it to devices
|
||||||
|
"tag:blumeops": ["autogroup:admin", "tag:blumeops"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Test access rules every time they're saved.
|
// Test access rules every time they're saved.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue