diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0edc4694..a2fcd807 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - exclude: ^guix/resources/guix-acl-keys/ + exclude: ^guix/(resources/guix-acl-keys/|machines|services) - repo: local hooks: diff --git a/ansible/playbook.yml b/ansible/playbook.yml index f39f4524..30979072 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -59,11 +59,6 @@ - nginx-geoip - nginx-cloudflare-mtls -- name: Deploy Git mirrors - hosts: nginx - roles: - - git-mirrors - - name: Deploy our database hosts hosts: databases roles: diff --git a/ansible/roles/git-mirrors/meta/main.yml b/ansible/roles/git-mirrors/meta/main.yml deleted file mode 100644 index 72b1bd7b..00000000 --- a/ansible/roles/git-mirrors/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - nginx diff --git a/ansible/roles/git-mirrors/tasks/main.yml b/ansible/roles/git-mirrors/tasks/main.yml deleted file mode 100644 index 2e86aa02..00000000 --- a/ansible/roles/git-mirrors/tasks/main.yml +++ /dev/null @@ -1,163 +0,0 @@ ---- -- name: Install cgit - package: - state: present - name: cgit - tags: - - role::git-mirrors - -- name: Install formatting dependencies for cgit - package: - state: present - name: - - python3-markdown - - python3-docutils - - python3-pygments - tags: - - role::git-mirrors - -- name: Install moreutils for cron utilities - package: - state: present - name: - - moreutils - tags: - - role::git-mirrors - -- name: Create mirroring user - user: - state: present - system: true - name: "{{ git_mirrors_user }}" - home: "{{ git_mirrors_base_dir }}" - tags: - - role::git-mirrors - -- name: Create mirror storage directory - file: - state: directory - path: "{{ git_mirrors_base_dir }}/mirrored" - owner: "{{ git_mirrors_user }}" - group: "{{ git_mirrors_user }}" - mode: "0755" - tags: - - role::git-mirrors - -- name: Create organisation folders - file: - state: directory - path: "{{ git_mirrors_base_dir }}/mirrored/{{ item.owner }}" - owner: "{{ git_mirrors_user }}" - group: "{{ git_mirrors_user }}" - mode: "0755" - with_items: - - "{{ git_mirrors_mirrored_repositories }}" - tags: - - role::git-mirrors - -# Unfortunately, the Ansible git module does not support the --mirror -# option to git clone, so for now we run the command ourselves if the -# directories could not be found. -- name: Clone repositories # noqa: command-instead-of-module - become: true - become_user: "{{ git_mirrors_user }}" - command: - argv: - - "git" - - "clone" - - "--mirror" - - "https://{{ item.domain | default('github.com') }}/{{ item.owner }}/{{ item.repo }}.git" - - "{{ git_mirrors_base_dir }}/mirrored/{{ item.owner }}/{{ item.repo }}" - creates: "{{ git_mirrors_base_dir }}/mirrored/{{ item.owner }}/{{ item.repo }}" - with_items: - - "{{ git_mirrors_mirrored_repositories }}" - tags: - - role::git-mirrors - -- name: Set repository descriptions - copy: - content: "{{ item.description | default('A mirrored copy of the ' + item.owner + '/' + item.repo + ' repository.') }}" - dest: "{{ git_mirrors_base_dir }}/mirrored/{{ item.owner }}/{{ item.repo }}/description" - owner: "{{ git_mirrors_user }}" - group: "{{ git_mirrors_user }}" - mode: "0444" - with_items: - - "{{ git_mirrors_mirrored_repositories }}" - tags: - - role::git-mirrors - -- name: Template cgitrc configuration file - template: - src: cgitrc.j2 - dest: /etc/cgitrc - mode: "0444" - owner: root - group: root - tags: - - role::git-mirrors - -- name: Install fcgiwrap for NGINX - package: - state: present - name: fcgiwrap - tags: - - role::git-mirrors - -- name: Enable fcgiwrap - service: - state: started - enabled: true - name: fcgiwrap - tags: - - role::git-mirrors - -- name: Template NGINX configuration - template: - src: nginx-site.conf.j2 - dest: "/etc/nginx/sites-available/{{ git_mirrors_nginx_config_name }}" - mode: "0444" - owner: root - group: root - tags: - - role::git-mirrors - notify: - - Reload the nginx service - -- name: Enable the NGINX site - file: - src: "/etc/nginx/sites-available/{{ git_mirrors_nginx_config_name }}" - dest: "/etc/nginx/sites-enabled/{{ git_mirrors_nginx_config_name }}" - state: link - tags: - - role::git-mirrors - notify: - - Reload the nginx service - -- name: Template mirror update script - template: - src: update-mirrors.sh.j2 - dest: "{{ git_mirrors_base_dir }}/update-mirrors.sh" - mode: "0544" - owner: "{{ git_mirrors_user }}" - group: "{{ git_mirrors_user }}" - tags: - - role::git-mirrors - -- name: Add cronjob for mirror updating - cron: - name: "Update the git mirrors published by cgit (git-mirrors role)" - # Every 5 minutes - minute: "*/5" - job: "chronic {{ git_mirrors_base_dir }}/update-mirrors.sh" - user: git-mirrors - cron_file: "{{ git_mirrors_cron_file }}" - tags: - - role::git-mirrors - -- name: Set cronjob failure email - community.general.cronvar: - name: MAILTO - value: "{{ git_mirrors_error_email }}" - cron_file: "{{ git_mirrors_cron_file }}" - tags: - - role::git-mirrors diff --git a/ansible/roles/git-mirrors/templates/cgitrc.j2 b/ansible/roles/git-mirrors/templates/cgitrc.j2 deleted file mode 100644 index 46b31500..00000000 --- a/ansible/roles/git-mirrors/templates/cgitrc.j2 +++ /dev/null @@ -1,39 +0,0 @@ -# {{ ansible_managed }} - -# See cgitrc(5) for details - -# Operational config -cache-root=/var/cache/cgit - -# Web config -css=/cgit.css -logo={{ git_mirrors_cgit_logo }} -favicon={{ git_mirrors_cgit_logo }} -virtual-root=/ -root-title={{ git_mirrors_cgit_title }} -root-desc={{ git_mirrors_cgit_description }} - -# Filters config -about-filter=/usr/lib/cgit/filters/about-formatting.sh -source-filter=/usr/lib/cgit/filters/syntax-highlighting.py -email-filter=lua:/usr/lib/cgit/filters/email-gravatar.lua - -# Design options -enable-commit-graph=1 -enable-log-linecount=1 -enable-blame=1 -enable-follow-links=1 -enable-index-owner=0 -enable-subject-links=1 -max-stats=year - -# Content options -readme=:README.md -readme=:README.rst -readme=:README.man -readme=:README.txt - -# Repositories -section-from-path=1 -scan-path={{ git_mirrors_base_dir }}/mirrored -repository-sort=age diff --git a/ansible/roles/git-mirrors/templates/nginx-site.conf.j2 b/ansible/roles/git-mirrors/templates/nginx-site.conf.j2 deleted file mode 100644 index 5b819f25..00000000 --- a/ansible/roles/git-mirrors/templates/nginx-site.conf.j2 +++ /dev/null @@ -1,25 +0,0 @@ -server { - server_name {{ git_mirrors_nginx_domain }}; - - listen 443 ssl http2; - listen [::]:443 ssl http2; - - ssl_certificate {{ git_mirrors_nginx_cert_file }}; - ssl_certificate_key {{ git_mirrors_nginx_cert_key }}; - - access_log /var/log/nginx/cgit-access.log; - error_log /var/log/nginx/cgit-error.log; - - root /usr/share/cgit; - try_files $uri @cgit; - - location @cgit { - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; - fastcgi_pass unix:/run/fcgiwrap.socket; - - fastcgi_param PATH_INFO $uri; - fastcgi_param QUERY_STRING $args; - fastcgi_param HTTP_HOST $server_name; - } -} diff --git a/ansible/roles/git-mirrors/templates/update-mirrors.sh.j2 b/ansible/roles/git-mirrors/templates/update-mirrors.sh.j2 deleted file mode 100644 index 32bfe1d7..00000000 --- a/ansible/roles/git-mirrors/templates/update-mirrors.sh.j2 +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env sh - -set -u - -# Base location of all mirrors -MIRRORS_BASE_DIR="{{ git_mirrors_base_dir }}/mirrored" - -# Locate repositories knowing that there will be a HEAD file inside them -FOUND_REPOS=$(find "$MIRRORS_BASE_DIR" -name "HEAD" -print0 | xargs -0 dirname) - -for repo in $FOUND_REPOS; do - cd "$repo"; - echo "Updating $repo mirror..." - if ! nice git fetch -q --prune; then - echo "Error: Failed to update repository $repo" - exit 1 - fi - echo "Updated repository." - - mkdir -p "info/web" || { echo "Error: Failed to create info/web directory in $repo"; exit 1; } - - git for-each-ref \ - --sort=-committerdate --count=1 \ - --format='%(committerdate:iso8601)' \ - --exclude='refs/pull/*/merge' \ - > info/web/last-modified || { echo "Error: Failed to write last-modified file in $repo"; exit 1; } - - cd - -done diff --git a/ansible/roles/git-mirrors/vars/main.yml b/ansible/roles/git-mirrors/vars/main.yml deleted file mode 100644 index 3b3865c1..00000000 --- a/ansible/roles/git-mirrors/vars/main.yml +++ /dev/null @@ -1,114 +0,0 @@ ---- -git_mirrors_base_dir: "/srv/git-mirrors" -git_mirrors_user: "git-mirrors" -git_mirrors_error_email: "devops+cron@pydis.wtf" - -git_mirrors_cgit_logo: "https://raw.githubusercontent.com/python-discord/ops-site/main/src/images/icon.png" -git_mirrors_cgit_title: PyDis DevOps Git Server -git_mirrors_cgit_description: Mirrored copies of Python Discord and related projects - -git_mirrors_cron_file: "ansible_git_mirrors_update" - -# Sources are assumed to be GitHub.com repositories -git_mirrors_mirrored_repositories: - # DevOps Repos - - owner: python-discord - repo: infra - description: >- - The PyDis DevOps boiler house, the engine room, where the magic happens. - - owner: python-discord - repo: king-arthur - description: >- - Our trusty job-centre recruited assistant for all things DevOps - - owner: python-discord - repo: ops-site - description: >- - Webscale-enhanced frontpage for all Python Discord international operations - - # Other Python Discord repos - - owner: python-discord - repo: bot - - owner: python-discord - repo: site - - owner: python-discord - repo: bot-core - - owner: python-discord - repo: sir-lancebot - - owner: python-discord - repo: snekbox - - owner: python-discord - repo: forms-backend - - owner: python-discord - repo: forms-frontend - - owner: python-discord - repo: metricity - - owner: python-discord - repo: blog - - # Skunk Works repos - - owner: owl-corp - repo: thallium - description: >- - Outsourced work with the KGB, MIA, CIA and NSTV - - owner: owl-corp - repo: pydis-keycloak-theme - description: >- - Syndicated branding for psy-op campaigns - - owner: owl-corp - repo: python-poetry-base - description: >- - Literary works written at our primary base of operations - - owner: owl-corp - repo: inotify-base - description: >- - The Owl Corp Early Warning System - - owner: owl-corp - repo: psql_extended - description: >- - Owl Corp surveillance storage platform - - owner: owl-corp - repo: lithium - description: >- - Digital mitigation strategy for emotional instability in the workplace - - # Uncle Christ's Assorted Works - - owner: jc - repo: poetry-restrict-plugin - domain: git.jchri.st - description: >- - The Last Stand against malware in Python packages - - owner: jc - repo: frommilter - domain: git.jchri.st - description: >- - A love letter from former DevOps Director Mr. Milter - - owner: jc - repo: bolt - domain: git.jchri.st - description: >- - State surveillance software for the modern age - - owner: jc - repo: twitchup - domain: git.jchri.st - description: >- - Total platform subjugation for the Twitch streaming service - - owner: jc - repo: dkim-milter-packaging - domain: git.jchri.st - description: >- - Packaging for the Mr. Milter's DKIM milter, a tool for email surveillance - - owner: jc - repo: ansible-role-nncp - domain: git.jchri.st - description: >- - Ansible role for the Node-to-Node Copy Protocol, a tool for exfiltrating data from secure environments - - owner: jc - repo: ansible-role-nftables - domain: git.jchri.st - description: >- - Ansible role for the nftables firewall, a tool for maintaining secure perimeters around our operations - -git_mirrors_nginx_domain: "git.pydis.wtf" -git_mirrors_nginx_cert_file: "/etc/letsencrypt/live/pydis.wtf/fullchain.pem" -git_mirrors_nginx_cert_key: "/etc/letsencrypt/live/pydis.wtf/privkey.pem" -git_mirrors_nginx_config_name: "git.pydis.wtf.conf" diff --git a/dns/zones/pydis.wtf.zone/root.yaml b/dns/zones/pydis.wtf.zone/root.yaml index 2a367625..8e1325c3 100644 --- a/dns/zones/pydis.wtf.zone/root.yaml +++ b/dns/zones/pydis.wtf.zone/root.yaml @@ -31,14 +31,6 @@ alertmanager: type: CNAME value: nginx-gf.box.pydis.wtf. -beta.git: - octodns: - cloudflare: - proxied: false - ttl: 300 - type: CNAME - value: turing.box.pydis.wtf. - bitwarden: octodns: cloudflare: @@ -85,7 +77,7 @@ git: proxied: true ttl: 300 type: CNAME - value: lovelace.box.pydis.wtf. + value: turing.box.pydis.wtf. grafana: octodns: diff --git a/guix/README.md b/guix/README.md index 510e5be4..d83b12a9 100644 --- a/guix/README.md +++ b/guix/README.md @@ -52,3 +52,17 @@ guix time-machine -C channels-lock.scm -- deploy deployment.scm # If you wish to sandbox the whole thing in a container: guix shell --preserve=^SSH_AUTH_SOCK --expose=/etc/guix --expose=$HOME/.ssh --share=$SSH_AUTH_SOCK --container --network --nesting guix nss-certs age sops -- guix time-machine -C channels-lock.scm -- deploy deployment.scm ``` + +## Troubleshooting + +- **Activation service scripts** can be hard to troubleshoot, because errors in + them are not logged. What you can do is run the activation script manually + after deployment using `sudo /run/current-system/activate`, which will run + the activation script in foreground and reveal any issues. + +- **Syntax errors** in included files from the `deploy` command are presently + not reported directly - instead, a `%turing-os: undefined variable` is + reported. [This has been reported + upstream](https://codeberg.org/guix/guix/issues/8005). Use `-- system + container machines/turing.scm` on the command line instead of `deploy + deployment.scm` to directly evaluate the machine and receive proper errors. diff --git a/guix/machines/turing.scm b/guix/machines/turing.scm index db82b0cd..4f4af513 100644 --- a/guix/machines/turing.scm +++ b/guix/machines/turing.scm @@ -1,8 +1,10 @@ -;; Module imports (define-module (machines turing) + #:use-module (services common) + #:use-module (services git-mirror) #:export (%turing-os)) (use-modules (gnu) (guix) + (guix modules) (sops secrets) (sops services sops)) (use-service-modules admin @@ -12,14 +14,17 @@ desktop networking security + shepherd ssh syncthing web) -(use-package-modules bootloaders +(use-package-modules bash + bootloaders databases golang-crypto linux tmux + version-control vim) (define %guix-dir (dirname (dirname (canonicalize-path (current-filename))))) @@ -50,15 +55,6 @@ #~(let ((pid (call-with-input-file "/var/run/nginx/pid" read))) (kill pid SIGHUP)))) -(define (letsencrypt-path hostname filename) - (string-append "/etc/letsencrypt/live/" hostname "/" filename)) - -(define (letsencrypt-key hostname) - (letsencrypt-path hostname "privkey.pem")) - -(define (letsencrypt-cert hostname) - (letsencrypt-path hostname "fullchain.pem")) - (define %services (append (list (service openssh-service-type (openssh-configuration @@ -92,7 +88,9 @@ (postgresql-configuration (postgresql postgresql-16))) (service tor-service-type) - (service nftables-service-type) + (service nftables-service-type + (nftables-configuration + (ruleset (resource "nftables.conf")))) (service fail2ban-service-type (fail2ban-configuration (extra-jails @@ -102,7 +100,10 @@ (enabled? #t)))))) (service ntp-service-type) %hidden-service-turing - (service nginx-service-type + %cgit-service + %git-mirror-activation-service + %git-mirror-update-service + (service nginx-service-type (nginx-configuration (server-blocks (list @@ -145,6 +146,9 @@ (webroot "/var/www") (certificates (list + (certificate-configuration + (domains '("beta.git.pydis.wtf")) + (deploy-hook %certbot-deploy-hook)) (certificate-configuration (domains '("turing.box.pydis.wtf")) (deploy-hook %certbot-deploy-hook)))))) diff --git a/guix/resources/nftables.conf b/guix/resources/nftables.conf new file mode 100644 index 00000000..eaa96a09 --- /dev/null +++ b/guix/resources/nftables.conf @@ -0,0 +1,37 @@ +# A simple and safe firewall +table inet filter { + chain input { + type filter hook input priority 0; policy drop; + + # early drop of invalid connections + ct state invalid drop + + # allow established/related connections + ct state { established, related } accept + + # allow from loopback + iif lo accept + # drop connections to lo not coming from lo + iif != lo ip daddr 127.0.0.1/8 drop + iif != lo ip6 daddr ::1/128 drop + + # allow icmp + ip protocol icmp accept + ip6 nexthdr icmpv6 accept + + # allow ssh + tcp dport ssh accept + # allow unsafe technologies + tcp dport http accept + tcp dport https accept + + # reject everything else + reject with icmpx type port-unreachable + } + chain forward { + type filter hook forward priority 0; policy drop; + } + chain output { + type filter hook output priority 0; policy accept; + } +} diff --git a/guix/services/common.scm b/guix/services/common.scm new file mode 100644 index 00000000..2dc3920d --- /dev/null +++ b/guix/services/common.scm @@ -0,0 +1,11 @@ +(define-module (services common) + #:export (letsencrypt-key letsencrypt-cert)) + +(define (letsencrypt-path hostname filename) + (string-append "/etc/letsencrypt/live/" hostname "/" filename)) + +(define (letsencrypt-key hostname) + (letsencrypt-path hostname "privkey.pem")) + +(define (letsencrypt-cert hostname) + (letsencrypt-path hostname "fullchain.pem")) diff --git a/guix/services/git-mirror.scm b/guix/services/git-mirror.scm new file mode 100644 index 00000000..4ab76919 --- /dev/null +++ b/guix/services/git-mirror.scm @@ -0,0 +1,202 @@ +(define-module (services git-mirror) + #:use-module (services common) + #:export (%cgit-service %git-mirror-activation-service + %git-mirror-update-service)) +(use-modules (gnu) + (guix) + (guix modules)) +(use-package-modules admin bash version-control) +(use-service-modules certbot cgit mcron shepherd web) + +(define %mirrored-repos + (list + ;; DevOps repos + (list "github.com" "python-discord" "infra" + "The PyDis DevOps boiler house, the engine room, where the magic happens.") + (list "github.com" "python-discord" "king-arthur" + "Our trusty job-centre recruited assistant for all things DevOps") + (list "github.com" "python-discord" "ops-site" + "Webscale-enhanced frontpage for all Python Discord international operations") + + ;; Other Python Discord repos + (list "github.com" "python-discord" "bot" "") + (list "github.com" "python-discord" "site" "") + (list "github.com" "python-discord" "bot-core" "") + (list "github.com" "python-discord" "sir-lancebot" "") + (list "github.com" "python-discord" "snekbox" "") + (list "github.com" "python-discord" "forms-backend" "") + (list "github.com" "python-discord" "forms-frontend" "") + (list "github.com" "python-discord" "metricity" "") + (list "github.com" "python-discord" "blog" "") + + ;; Skunk Works repos + (list "github.com" "owl-corp" "thallium" + "Outsourced work with the KGB, MIA, CIA and NSTV") + (list "github.com" "owl-corp" "pydis-keycloak-theme" + "Syndicated branding for psy-op campaigns") + (list "github.com" "owl-corp" "python-poetry-base" + "Literary works written at our primary base of operations") + (list "github.com" "owl-corp" "inotify-base" + "The Owl Corp Early Warning System") + (list "github.com" "owl-corp" "psql_extended" + "Owl Corp surveillance storage platform") + (list "github.com" "owl-corp" "lithium" + "Digital mitigation strategy for emotional instability in the workplace") + + ;; Uncle Christ's Assorted Works + (list "git.jchri.st" "jc" "poetry-restrict-plugin" + "The Last Stand against malware in Python packages") + (list "git.jchri.st" "jc" "frommilter" + "A love letter from former DevOps Director Mr. Milter") + (list "git.jchri.st" "jc" "bolt" + "State surveillance software for the modern age") + (list "git.jchri.st" "jc" "twitchup" + "Total platform subjugation for the Twitch streaming service") + (list "git.jchri.st" "jc" "dkim-milter-packaging" + "Packaging for the Mr. Milter's DKIM milter, a tool for email surveillance") + (list "git.jchri.st" "jc" "ansible-role-nncp" + "Ansible role for the Node-to-Node Copy Protocol, a tool for exfiltrating data from secure environments") + (list "git.jchri.st" "jc" "ansible-role-nftables" + "Ansible role for the nftables firewall, a tool for maintaining secure perimeters around our operations"))) + +(define %cgit-service + (service cgit-service-type + (cgit-configuration (root-title "PyDis DevOps Git Server") + (root-desc + "Mirrored copies of Python Discord and related projects") + ;; XXX: This should support multiple readme files, fix upstream. + ;; Alternatively, use plain file + (readme ":README.md") + (section-from-path 1) + (enable-commit-graph? #t) + (enable-log-linecount? #t) + ;; (enable-blame? #t) + (enable-follow-links? #t) + (enable-index-owner? #f) + (enable-subject-links? #t) + (max-stats "year") + (repository-sort "age") + (logo + "https://raw.githubusercontent.com/python-discord/ops-site/main/src/images/icon.png") + ;; Default is /srv/git + (repository-directory "/srv/git/mirrored") + (about-filter (file-append cgit + "/lib/cgit/filters/about-formatting.sh")) + (source-filter (file-append cgit + "/lib/cgit/filters/syntax-highlighting.py")) + (email-filter (file-append cgit + "/lib/cgit/filters/email-gravatar.py")) + (nginx (list (nginx-server-configuration (root + cgit) + (listen ' + ("443 ssl http2")) + (server-name ' + ("beta.git.pydis.wtf")) + (ssl-certificate + (letsencrypt-cert + "beta.git.pydis.wtf")) + (ssl-certificate-key + (letsencrypt-key + "beta.git.pydis.wtf")) + ;; This should probably be better documented upstream. + (locations + (list + (nginx-location-configuration + (uri + "@cgit") + (body ' + ("fastcgi_param SCRIPT_FILENAME $document_root/lib/cgit/cgit.cgi;" + "fastcgi_param PATH_INFO $uri;" + "fastcgi_param QUERY_STRING $args;" + "fastcgi_param HTTP_HOST $server_name;" + "fastcgi_pass 127.0.0.1:9000;"))))) + (try-files + (list + "$uri" + "@cgit")))))))) + +(define %git-mirror-activation + ;; Clone our repositories for cgit. Activations run on system reconfigure + ;; (that is, after deploy) or at boot. + #~(begin + (use-modules (guix build utils) + (ice-9 match)) + + (define (maybe-description->description org name maybe-description) + "Transform MAYBE-DESCRIPTION to an actual description." + (if (string=? maybe-description "") + (string-append "A mirrored copy of the " org "/" name + " repository.") maybe-description)) + + (define (clone-repo repo) + "Clone the repository REPO, a triplet HOST, ORG, NAME if it does not already exist locally." + (match repo + ((hostname org name maybe-description) + (let* ((base-directory "/srv/git/mirrored") + (org-directory (string-append base-directory "/" org)) + (repo-directory (string-append org-directory "/" name)) + (description-file (string-append repo-directory "/" + "description")) + (description (maybe-description->description org name + maybe-description))) + + (unless (file-exists? repo-directory) + (begin + (mkdir-p org-directory) + (invoke (string-append #$git "/bin/git") "clone" "--mirror" + (string-append "https://" + hostname + "/" + org + "/" + name + ".git") repo-directory))) + (call-with-output-file description-file + (lambda (file) + (format file description))))))) + + ;; #$ is "ungexp" (when used in a g-expression, which we have with #~ above) + ;; meaning "inject this code from the host". The `%` is just from the + ;; function name. Finally, we quote `'` the value, because otherwise the `(list` + ;; code from above is treated as forms instead of data. + (map clone-repo + '#$%mirrored-repos))) + +(define %git-mirror-activation-service + (simple-service 'git-mirror-activation activation-service-type + %git-mirror-activation)) + +(define %git-mirror-update + (with-imported-modules (source-module-closure '((guix build utils))) + #~(begin + (use-modules (guix build utils) + (ice-9 match)) + + (define (update-repo repo) + "Update the repository REPO, a triplet HOST, ORG, NAME, to the latest remote changes." + + (match repo + ((hostname org name _) + (with-directory-excursion (string-append + "/srv/git/mirrored/" + org "/" name) + (invoke (string-append #$git "/bin/git") + "fetch" "-q" "--prune") + (mkdir-p "info/web") + (invoke (string-append #$bash "/bin/bash") + "-c" + (string-append #$git + "/bin/git for-each-ref --sort=-committerdate --count=1 --format='%(committerdate:iso8601)' --exclude='refs/pull/*/merge' > info/web/last-modified")))))) + + ;; See comment in %git-mirror-activation. + (map update-repo + '#$%mirrored-repos)))) + +(define %git-mirror-update-timer + (shepherd-timer '(git-mirror-update) "*/5 * * * *" + #~(#$(program-file "git-mirror-update" %git-mirror-update)) + #:requirement '(networking user-processes))) + +(define %git-mirror-update-service + (simple-service 'git-mirror-update-timer shepherd-root-service-type + (list %git-mirror-update-timer)))