From 12d9fc06e5559d7fecd997315c585e7b7d51a4f2 Mon Sep 17 00:00:00 2001 From: Pieter Date: Sat, 24 Jan 2026 13:16:25 +0100 Subject: [PATCH] feat: Configure Diun with Docker Hub auth and watchRepo control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../260124-configure-diun-watchrepo.yml | 156 ++++++++++++++++ ansible/roles/authentik/tasks/providers.yml | 2 +- ansible/roles/diun/templates/diun.yml.j2 | 9 +- scripts/configure-diun-all-servers.sh | 170 ++++++++++++++++++ 4 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 ansible/playbooks/260124-configure-diun-watchrepo.yml create mode 100755 scripts/configure-diun-all-servers.sh diff --git a/ansible/playbooks/260124-configure-diun-watchrepo.yml b/ansible/playbooks/260124-configure-diun-watchrepo.yml new file mode 100644 index 0000000..1a92f30 --- /dev/null +++ b/ansible/playbooks/260124-configure-diun-watchrepo.yml @@ -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 diff --git a/ansible/roles/authentik/tasks/providers.yml b/ansible/roles/authentik/tasks/providers.yml index c570a12..fac2f59 100644 --- a/ansible/roles/authentik/tasks/providers.yml +++ b/ansible/roles/authentik/tasks/providers.yml @@ -23,7 +23,7 @@ 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}) 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) 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 diff --git a/ansible/roles/diun/templates/diun.yml.j2 b/ansible/roles/diun/templates/diun.yml.j2 index 1ea3ee1..803a945 100644 --- a/ansible/roles/diun/templates/diun.yml.j2 +++ b/ansible/roles/diun/templates/diun.yml.j2 @@ -11,11 +11,18 @@ watch: firstCheckNotif: {{ diun_first_check_notif | lower }} defaults: - watchRepo: true + watchRepo: {{ diun_watch_repo | default(true) | lower }} notifyOn: - new - 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: docker: watchByDefault: {{ diun_watch_all | lower }} diff --git a/scripts/configure-diun-all-servers.sh b/scripts/configure-diun-all-servers.sh new file mode 100755 index 0000000..719c632 --- /dev/null +++ b/scripts/configure-diun-all-servers.sh @@ -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