about summary refs log tree commit diff
;;; guix-forge --- Guix software forge meta-service
;;; Copyright © 2022, 2025 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; This file is part of guix-forge.
;;;
;;; guix-forge is free software: you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published
;;; by the Free Software Foundation, either version 3 of the License,
;;; or (at your option) any later version.
;;;
;;; guix-forge is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with guix-forge.  If not, see
;;; <https://www.gnu.org/licenses/>.

(define-module (forge webhook)
  #:use-module (srfi srfi-1)
  #:use-module (gnu build linux-container)
  #:use-module ((gnu packages admin) #:select (shadow))
  #:use-module ((gnu packages guile) #:select (guile-json-4))
  #:use-module ((gnu packages web) #:select (webhook))
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module (gnu system accounts)
  #:use-module (gnu system file-systems)
  #:use-module (gnu system shadow)
  #:use-module (guix gexp)
  #:use-module (guix records)
  #:use-module (guix least-authority)
  #:use-module (forge socket)
  #:export (webhook-service-type
            webhook-configuration
            webhook-configuration?
            webhook-configuration-package
            webhook-configuration-socket
            webhook-configuration-log-directory
            webhook-configuration-hooks
            webhook-hook
            webhook-hook?
            webhook-hook-id
            webhook-hook-run))

(define-record-type* <webhook-configuration>
  webhook-configuration make-webhook-configuration
  webhook-configuration?
  (package webhook-configuration-package
           (default webhook))
  (socket webhook-configuration-socket
          (default (forge-ip-socket
                    (ip "127.0.0.1")
                    (port 9000))))
  (log-directory webhook-configuration-log-directory
                 (default "/var/log/webhook"))
  (hooks webhook-configuration-hooks
         (default '())))

(define-record-type* <webhook-hook>
  webhook-hook make-webhook-hook
  webhook-hook?
  (id webhook-hook-id)
  (run webhook-hook-run))

(define %webhook-accounts
  (list (user-account
         (name "webhook")
         (group "webhook")
         (system? #t)
         (comment "webhook user")
         (home-directory "/var/empty")
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name "webhook")
         (system? #t))))

(define (webhook-activation config)
  (match-record config <webhook-configuration>
    (log-directory)
    #~(begin
        ;; Create log directory and set permissions.
        (mkdir-p #$log-directory)
        (let ((user (getpw "webhook")))
          (for-each (lambda (file)
                      (chown file (passwd:uid user) (passwd:gid user)))
                    (find-files #$log-directory #:directories? #t))))))

(define (hooks-json-gexp config)
  (with-extensions (list guile-json-4)
    #~(begin
        (use-modules (srfi srfi-26)
                     (json))
        
        (call-with-output-file #$output
          (cut scm->json
               ;; We convert from list to vector on the build-side
               ;; because a vector cannot be lowered correctly into a
               ;; G-expression.
               (list->vector
                ;; We build a true dotted association list in this
                ;; roundabout way because a true dotted association
                ;; list cannot be lowered correctly into a
                ;; G-expression.
                (map (cut map (cut apply cons <>) <>)
                     '#$(map (lambda (hook)
                               `(("id" ,(webhook-hook-id hook))
                                 ("execute-command" ,(program-file (string-append (webhook-hook-id hook)
                                                                                  "-webhook")
                                                                   (webhook-hook-run hook)))))
                             (webhook-configuration-hooks config))))
               <>)))))

(define (webhook-shepherd-service config)
  (shepherd-service
   (documentation "Run webhook.")
   (provision '(webhook))
   (requirement '(networking))
   (start
    (let ((hooks-json (computed-file "hooks.json" (hooks-json-gexp config))))
      #~(make-forkexec-constructor
         (list #$(least-authority-wrapper
                  (file-append (webhook-configuration-package config)
                               "/bin/webhook")
                  #:name "webhook"
                  #:mappings (list (file-system-mapping
                                    (source hooks-json)
                                    (target source))
                                   (file-system-mapping
                                    (source (webhook-configuration-log-directory config))
                                    (target source)
                                    (writable? #t)))
                  ;; TODO: If socket is a Unix socket, run in a
                  ;; network namespace. We can't do this yet due to
                  ;; https://yhetil.org/guix/m1ilknoi5r.fsf@fastmail.net/
                  #:namespaces (delq 'net %namespaces))
               "-hooks" #$hooks-json
               "-ip" #$(forge-ip-socket-ip (webhook-configuration-socket config))
               "-port" #$(number->string (forge-ip-socket-port (webhook-configuration-socket config)))
               "-logfile" #$(string-append (webhook-configuration-log-directory config)
                                           "/webhook.log"))
         #:user "webhook"
         #:group "webhook"
         #:log-file "/var/log/webhook.log")))
   (stop #~(make-kill-destructor))))

(define webhook-service-type
  (service-type
   (name 'webhook)
   (description "Run webhook.")
   (extensions (list (service-extension account-service-type
                                        (const %webhook-accounts))
                     (service-extension activation-service-type
                                        webhook-activation)
                     (service-extension shepherd-root-service-type
                                        (compose list webhook-shepherd-service))))
   (compose concatenate)
   (extend (lambda (config hook-extensions)
             (webhook-configuration
              (inherit config)
              (hooks (append (webhook-configuration-hooks config)
                             hook-extensions)))))
   (default-value (webhook-configuration))))