feat: Configure Diun with Docker Hub auth and watchRepo control
This commit resolves Docker Hub rate limiting issues on all servers by: 1. Adding Docker Hub authentication support to Diun configuration 2. Making watchRepo configurable (disabled to reduce API calls) 3. Creating automation to deploy changes across all 17 servers Changes: - Enhanced diun.yml.j2 template to support: - Configurable watchRepo setting (defaults to true for compatibility) - Docker Hub authentication via regopts when credentials provided - Created 260124-configure-diun-watchrepo.yml playbook to: - Disable watchRepo (only checks specific tags vs entire repo) - Enable Docker Hub authentication (5000 pulls/6h vs 100/6h) - Change schedule to weekly (Monday 6am UTC) - Created configure-diun-all-servers.sh automation script with: - Proper SOPS age key file path handling - Per-server SSH key management - Sequential deployment across all servers - Fixed Authentik OIDC provider meta_launch_url to use client_domain Successfully deployed to all 17 servers (bever, das, egel, haas, kikker, kraai, mees, mol, mus, otter, ree, specht, uil, valk, vos, wolf, zwaan). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
39c57d583a
commit
12d9fc06e5
4 changed files with 335 additions and 2 deletions
156
ansible/playbooks/260124-configure-diun-watchrepo.yml
Normal file
156
ansible/playbooks/260124-configure-diun-watchrepo.yml
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
---
|
||||||
|
# Configure Diun to disable watchRepo and add Docker Hub authentication
|
||||||
|
# This playbook updates all servers to:
|
||||||
|
# - Only watch specific image tags (not entire repositories) to reduce API calls
|
||||||
|
# - Add Docker Hub authentication for higher rate limits
|
||||||
|
#
|
||||||
|
# Background:
|
||||||
|
# - watchRepo: true checks ALL tags in a repository (hundreds of API calls)
|
||||||
|
# - watchRepo: false only checks the specific tag being used (1-2 API calls)
|
||||||
|
# - Docker Hub auth increases rate limit from 100 to 5000 pulls per 6 hours
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd ansible/
|
||||||
|
# SOPS_AGE_KEY_FILE="../keys/age-key.txt" HCLOUD_TOKEN="..." \
|
||||||
|
# ansible-playbook -i hcloud.yml playbooks/260124-configure-diun-watchrepo.yml
|
||||||
|
#
|
||||||
|
# Or for specific servers:
|
||||||
|
# SOPS_AGE_KEY_FILE="../keys/age-key.txt" HCLOUD_TOKEN="..." \
|
||||||
|
# ansible-playbook -i hcloud.yml playbooks/260124-configure-diun-watchrepo.yml \
|
||||||
|
# --limit das,uil,vos --private-key "../keys/ssh/das"
|
||||||
|
|
||||||
|
- name: Configure Diun watchRepo and Docker Hub authentication
|
||||||
|
hosts: all
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Diun base configuration
|
||||||
|
diun_version: "latest"
|
||||||
|
diun_log_level: "info"
|
||||||
|
diun_watch_workers: 10
|
||||||
|
diun_watch_all: true
|
||||||
|
diun_exclude_containers: []
|
||||||
|
diun_first_check_notif: false
|
||||||
|
|
||||||
|
# Schedule: Weekly on Monday at 6am UTC (to reduce API calls)
|
||||||
|
diun_schedule: "0 6 * * 1"
|
||||||
|
|
||||||
|
# Disable watchRepo - only check the specific tags we're using
|
||||||
|
diun_watch_repo: false
|
||||||
|
|
||||||
|
# Webhook configuration - sends to Matrix via custom webhook
|
||||||
|
diun_notif_enabled: true
|
||||||
|
diun_notif_type: webhook
|
||||||
|
diun_webhook_endpoint: "https://diun-webhook.postxsociety.cloud"
|
||||||
|
diun_webhook_method: POST
|
||||||
|
diun_webhook_headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
# Disable email notifications
|
||||||
|
diun_email_enabled: false
|
||||||
|
|
||||||
|
# SMTP defaults (not used when email disabled, but needed for template)
|
||||||
|
diun_smtp_host: "smtp.eu.mailgun.org"
|
||||||
|
diun_smtp_port: 587
|
||||||
|
diun_smtp_from: "{{ client_name }}@mg.vrije.cloud"
|
||||||
|
diun_smtp_to: "pieter@postxsociety.org"
|
||||||
|
|
||||||
|
# Optional notification defaults (unused but needed for template)
|
||||||
|
diun_slack_webhook_url: ""
|
||||||
|
diun_matrix_enabled: false
|
||||||
|
diun_matrix_homeserver_url: ""
|
||||||
|
diun_matrix_user: ""
|
||||||
|
diun_matrix_password: ""
|
||||||
|
diun_matrix_room_id: ""
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Gather facts
|
||||||
|
setup:
|
||||||
|
|
||||||
|
- name: Determine client name from hostname
|
||||||
|
set_fact:
|
||||||
|
client_name: "{{ inventory_hostname }}"
|
||||||
|
|
||||||
|
- name: Load client secrets
|
||||||
|
community.sops.load_vars:
|
||||||
|
file: "{{ playbook_dir }}/../../secrets/clients/{{ client_name }}.sops.yaml"
|
||||||
|
name: client_secrets
|
||||||
|
age_keyfile: "{{ lookup('env', 'SOPS_AGE_KEY_FILE') }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Load shared secrets
|
||||||
|
community.sops.load_vars:
|
||||||
|
file: "{{ playbook_dir }}/../../secrets/shared.sops.yaml"
|
||||||
|
name: shared_secrets
|
||||||
|
age_keyfile: "{{ lookup('env', 'SOPS_AGE_KEY_FILE') }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Merge shared secrets into client_secrets
|
||||||
|
set_fact:
|
||||||
|
client_secrets: "{{ client_secrets | combine(shared_secrets) }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Set SMTP credentials (required by template even if unused)
|
||||||
|
set_fact:
|
||||||
|
diun_smtp_username_final: "{{ client_secrets.mailgun_smtp_user | default('') }}"
|
||||||
|
diun_smtp_password_final: ""
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Set Docker Hub credentials for higher rate limits
|
||||||
|
set_fact:
|
||||||
|
diun_docker_hub_username: "{{ client_secrets.docker_hub_username }}"
|
||||||
|
diun_docker_hub_password: "{{ client_secrets.docker_hub_password }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Display configuration summary
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Configuring Diun on {{ inventory_hostname }}:
|
||||||
|
- Webhook endpoint: {{ diun_webhook_endpoint }}
|
||||||
|
- Email notifications: {{ 'enabled' if diun_email_enabled else 'disabled' }}
|
||||||
|
- Schedule: {{ diun_schedule }} (Weekly on Monday at 6am UTC)
|
||||||
|
- Watch entire repositories: {{ 'yes' if diun_watch_repo else 'no (only specific tags)' }}
|
||||||
|
- Docker Hub auth: {{ 'enabled' if diun_docker_hub_username else 'disabled' }}
|
||||||
|
|
||||||
|
- name: Deploy Diun configuration with watchRepo disabled and Docker Hub auth
|
||||||
|
template:
|
||||||
|
src: "{{ playbook_dir }}/../roles/diun/templates/diun.yml.j2"
|
||||||
|
dest: /opt/docker/diun/diun.yml
|
||||||
|
mode: '0644'
|
||||||
|
notify: Restart Diun
|
||||||
|
|
||||||
|
- name: Restart Diun to apply new configuration
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: /opt/docker/diun
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Wait for Diun to start
|
||||||
|
pause:
|
||||||
|
seconds: 5
|
||||||
|
|
||||||
|
- name: Check Diun status
|
||||||
|
shell: docker ps --filter name=diun --format "{{ '{{' }}.Status{{ '}}' }}"
|
||||||
|
register: diun_status
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display Diun status
|
||||||
|
debug:
|
||||||
|
msg: "Diun status on {{ inventory_hostname }}: {{ diun_status.stdout }}"
|
||||||
|
|
||||||
|
- name: Verify Diun configuration
|
||||||
|
shell: docker exec diun cat /diun.yml | grep -E "(watchRepo|regopts)" || echo "Config deployed"
|
||||||
|
register: diun_config_check
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display configuration verification
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Configuration applied on {{ inventory_hostname }}:
|
||||||
|
{{ diun_config_check.stdout }}
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart Diun
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: /opt/docker/diun
|
||||||
|
state: restarted
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
if not auth_flow or not key: print(json.dumps({'error': 'Config missing'}), file=sys.stderr); sys.exit(1)
|
if not auth_flow or not key: print(json.dumps({'error': 'Config missing'}), file=sys.stderr); sys.exit(1)
|
||||||
s, prov = req('/api/v3/providers/oauth2/', 'POST', {'name': 'Nextcloud', 'authorization_flow': auth_flow, 'invalidation_flow': inval_flow, 'client_type': 'confidential', 'redirect_uris': [{'matching_mode': 'strict', 'url': 'https://{{ nextcloud_domain }}/apps/user_oidc/code'}], 'signing_key': key, 'sub_mode': 'hashed_user_id', 'include_claims_in_id_token': True})
|
s, prov = req('/api/v3/providers/oauth2/', 'POST', {'name': 'Nextcloud', 'authorization_flow': auth_flow, 'invalidation_flow': inval_flow, 'client_type': 'confidential', 'redirect_uris': [{'matching_mode': 'strict', 'url': 'https://{{ nextcloud_domain }}/apps/user_oidc/code'}], 'signing_key': key, 'sub_mode': 'hashed_user_id', 'include_claims_in_id_token': True})
|
||||||
if s != 201: print(json.dumps({'error': 'Provider failed', 'details': prov}), file=sys.stderr); sys.exit(1)
|
if s != 201: print(json.dumps({'error': 'Provider failed', 'details': prov}), file=sys.stderr); sys.exit(1)
|
||||||
s, app = req('/api/v3/core/applications/', 'POST', {'name': 'Nextcloud', 'slug': 'nextcloud', 'provider': prov['pk'], 'meta_launch_url': 'https://{{ nextcloud_domain }}'})
|
s, app = req('/api/v3/core/applications/', 'POST', {'name': 'Nextcloud', 'slug': 'nextcloud', 'provider': prov['pk'], 'meta_launch_url': 'https://nextcloud.{{ client_domain }}'})
|
||||||
if s != 201: print(json.dumps({'error': 'App failed', 'details': app}), file=sys.stderr); sys.exit(1)
|
if s != 201: print(json.dumps({'error': 'App failed', 'details': app}), file=sys.stderr); sys.exit(1)
|
||||||
print(json.dumps({'success': True, 'provider_id': prov['pk'], 'application_id': app['pk'], 'client_id': prov['client_id'], 'client_secret': prov['client_secret'], 'discovery_uri': f"https://{{ authentik_domain }}/application/o/nextcloud/.well-known/openid-configuration", 'issuer': f"https://{{ authentik_domain }}/application/o/nextcloud/"}))
|
print(json.dumps({'success': True, 'provider_id': prov['pk'], 'application_id': app['pk'], 'client_id': prov['client_id'], 'client_secret': prov['client_secret'], 'discovery_uri': f"https://{{ authentik_domain }}/application/o/nextcloud/.well-known/openid-configuration", 'issuer': f"https://{{ authentik_domain }}/application/o/nextcloud/"}))
|
||||||
dest: /tmp/create_oidc.py
|
dest: /tmp/create_oidc.py
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,18 @@ watch:
|
||||||
firstCheckNotif: {{ diun_first_check_notif | lower }}
|
firstCheckNotif: {{ diun_first_check_notif | lower }}
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
watchRepo: true
|
watchRepo: {{ diun_watch_repo | default(true) | lower }}
|
||||||
notifyOn:
|
notifyOn:
|
||||||
- new
|
- new
|
||||||
- update
|
- update
|
||||||
|
|
||||||
|
{% if diun_docker_hub_username is defined and diun_docker_hub_password is defined %}
|
||||||
|
regopts:
|
||||||
|
- selector: image
|
||||||
|
username: {{ diun_docker_hub_username }}
|
||||||
|
password: {{ diun_docker_hub_password }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
docker:
|
docker:
|
||||||
watchByDefault: {{ diun_watch_all | lower }}
|
watchByDefault: {{ diun_watch_all | lower }}
|
||||||
|
|
|
||||||
170
scripts/configure-diun-all-servers.sh
Executable file
170
scripts/configure-diun-all-servers.sh
Executable file
|
|
@ -0,0 +1,170 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Configure Diun on all servers (disable watchRepo, add Docker Hub auth)
|
||||||
|
# Created: 2026-01-24
|
||||||
|
#
|
||||||
|
# This script runs the diun configuration playbook on each server
|
||||||
|
# with its corresponding SSH key.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd infrastructure/
|
||||||
|
# SOPS_AGE_KEY_FILE="keys/age-key.txt" HCLOUD_TOKEN="..." ./scripts/configure-diun-all-servers.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
ANSIBLE_DIR="$PROJECT_ROOT/ansible"
|
||||||
|
KEYS_DIR="$PROJECT_ROOT/keys/ssh"
|
||||||
|
PLAYBOOK="playbooks/260124-configure-diun-watchrepo.yml"
|
||||||
|
|
||||||
|
# Check required environment variables
|
||||||
|
if [ -z "${HCLOUD_TOKEN:-}" ]; then
|
||||||
|
echo -e "${RED}Error: HCLOUD_TOKEN environment variable is required${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${SOPS_AGE_KEY_FILE:-}" ]; then
|
||||||
|
echo -e "${RED}Error: SOPS_AGE_KEY_FILE environment variable is required${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert SOPS_AGE_KEY_FILE to absolute path if it's relative
|
||||||
|
if [[ ! "$SOPS_AGE_KEY_FILE" = /* ]]; then
|
||||||
|
export SOPS_AGE_KEY_FILE="$PROJECT_ROOT/$SOPS_AGE_KEY_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change to ansible directory
|
||||||
|
cd "$ANSIBLE_DIR"
|
||||||
|
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo -e "${BLUE}Diun Configuration - All Servers${NC}"
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Playbook: $PLAYBOOK"
|
||||||
|
echo "Ansible directory: $ANSIBLE_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "Configuration changes:"
|
||||||
|
echo " - Disable watchRepo (only check specific tags, not entire repos)"
|
||||||
|
echo " - Add Docker Hub authentication (5000 pulls/6h limit)"
|
||||||
|
echo " - Schedule: Weekly on Monday at 6am UTC"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get list of all servers with SSH keys
|
||||||
|
SERVERS=()
|
||||||
|
for keyfile in "$KEYS_DIR"/*.pub; do
|
||||||
|
if [ -f "$keyfile" ]; then
|
||||||
|
server=$(basename "$keyfile" .pub)
|
||||||
|
# Skip special servers
|
||||||
|
if [[ "$server" != "README" ]] && [[ "$server" != "edge" ]]; then
|
||||||
|
SERVERS+=("$server")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${BLUE}Found ${#SERVERS[@]} servers:${NC}"
|
||||||
|
printf '%s\n' "${SERVERS[@]}" | sort
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Counters
|
||||||
|
SUCCESS_COUNT=0
|
||||||
|
FAILED_COUNT=0
|
||||||
|
SKIPPED_COUNT=0
|
||||||
|
declare -a SUCCESS_SERVERS
|
||||||
|
declare -a FAILED_SERVERS
|
||||||
|
declare -a SKIPPED_SERVERS
|
||||||
|
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo -e "${BLUE}Starting configuration run...${NC}"
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run playbook for each server
|
||||||
|
for server in "${SERVERS[@]}"; do
|
||||||
|
echo -e "${YELLOW}-----------------------------------------------------------${NC}"
|
||||||
|
echo -e "${YELLOW}Processing: $server${NC}"
|
||||||
|
echo -e "${YELLOW}-----------------------------------------------------------${NC}"
|
||||||
|
|
||||||
|
SSH_KEY="$KEYS_DIR/$server"
|
||||||
|
|
||||||
|
if [ ! -f "$SSH_KEY" ]; then
|
||||||
|
echo -e "${RED}✗ SSH key not found: $SSH_KEY${NC}"
|
||||||
|
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||||
|
SKIPPED_SERVERS+=("$server")
|
||||||
|
echo ""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the playbook (with SSH options to prevent agent key issues)
|
||||||
|
if env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
||||||
|
SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" \
|
||||||
|
ANSIBLE_SSH_ARGS="-o IdentitiesOnly=yes" \
|
||||||
|
~/.local/bin/ansible-playbook \
|
||||||
|
-i hcloud.yml \
|
||||||
|
"$PLAYBOOK" \
|
||||||
|
--limit "$server" \
|
||||||
|
--private-key "$SSH_KEY" 2>&1; then
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Success: $server${NC}"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
SUCCESS_SERVERS+=("$server")
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Failed: $server${NC}"
|
||||||
|
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||||
|
FAILED_SERVERS+=("$server")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo -e "${BLUE}CONFIGURATION RUN SUMMARY${NC}"
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Total servers: ${#SERVERS[@]}"
|
||||||
|
echo -e "${GREEN}Successful: $SUCCESS_COUNT${NC}"
|
||||||
|
echo -e "${RED}Failed: $FAILED_COUNT${NC}"
|
||||||
|
echo -e "${YELLOW}Skipped: $SKIPPED_COUNT${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $SUCCESS_COUNT -gt 0 ]; then
|
||||||
|
echo -e "${GREEN}Successful servers:${NC}"
|
||||||
|
printf ' %s\n' "${SUCCESS_SERVERS[@]}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $FAILED_COUNT -gt 0 ]; then
|
||||||
|
echo -e "${RED}Failed servers:${NC}"
|
||||||
|
printf ' %s\n' "${FAILED_SERVERS[@]}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $SKIPPED_COUNT -gt 0 ]; then
|
||||||
|
echo -e "${YELLOW}Skipped servers:${NC}"
|
||||||
|
printf ' %s\n' "${SKIPPED_SERVERS[@]}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Wait for next Monday at 6am UTC for scheduled run"
|
||||||
|
echo " 2. Or manually trigger: docker exec diun diun once"
|
||||||
|
echo " 3. Check logs: docker logs diun"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Exit with error if any failures
|
||||||
|
if [ $FAILED_COUNT -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Loading…
Add table
Reference in a new issue