summaryrefslogtreecommitdiff
path: root/gnu
diff options
context:
space:
mode:
Diffstat (limited to 'gnu')
-rw-r--r--gnu/services/web.scm194
-rw-r--r--gnu/tests/web.scm194
2 files changed, 386 insertions, 2 deletions
diff --git a/gnu/services/web.scm b/gnu/services/web.scm
index 3989607646e..ae33a25394d 100644
--- a/gnu/services/web.scm
+++ b/gnu/services/web.scm
@@ -19,6 +19,7 @@
;;; Copyright © 2023 Miguel Ángel Moreno <mail@migalmoreno.com>
;;; Copyright © 2024 Leo Nikkilä <hello@lnikki.la>
;;; Copyright © 2025 Maxim Cournoyer <maxim.cournoyer@gmail.com>
+;;; Copyright © 2025 Rodion Goritskov <rodion@goritskov.com>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -40,8 +41,10 @@
#:use-module (gnu services shepherd)
#:use-module (gnu services admin)
#:use-module (gnu services configuration)
+ #:use-module (gnu services databases)
#:use-module (gnu services getmail)
#:use-module (gnu services mail)
+ #:use-module (gnu system file-systems)
#:use-module (gnu system pam)
#:use-module (gnu system shadow)
#:use-module (gnu packages admin)
@@ -59,7 +62,9 @@
#:use-module (gnu packages mail)
#:use-module (gnu packages rust-apps)
#:autoload (guix i18n) (G_)
+ #:autoload (gnu build linux-container) (%namespaces)
#:use-module (guix diagnostics)
+ #:use-module (guix least-authority)
#:use-module (guix packages)
#:use-module (guix records)
#:use-module (guix modules)
@@ -74,6 +79,7 @@
#:use-module (srfi srfi-34)
#:use-module (ice-9 match)
#:use-module (ice-9 format)
+ #:use-module (ice-9 regex)
#:export (httpd-configuration
httpd-configuration?
httpd-configuration-package
@@ -328,7 +334,23 @@
agate-configuration-group
agate-configuration-log-file
- agate-service-type))
+ agate-service-type
+
+ miniflux-configuration
+ miniflux-configuration?
+ miniflux-configuration-listen-address
+ miniflux-configuration-base-url
+ miniflux-configuration-create-administrator-account?
+ miniflux-configuration-administrator-account-name
+ miniflux-configuration-administrator-account-password
+ miniflux-configuration-run-migrations?
+ miniflux-configuration-database-url
+ miniflux-configuration-user
+ miniflux-configuration-group
+ miniflux-configuration-log-file
+ miniflux-configuration-extra-settings
+
+ miniflux-service-type))
;;; Commentary:
;;;
@@ -2279,3 +2301,173 @@ root=/srv/gemini
(default-value (agate-configuration))
(description "Run Agate, a simple Gemini protocol server written in
Rust.")))
+
+(define (serialize-string field-name val)
+ (format #f "~a=~a\n" field-name val))
+
+(define (string-or-file-path? val)
+ (string? val))
+(define (serialize-string-or-file-path field-name val)
+ (serialize-string (if (absolute-file-name? val)
+ (format #f "~a_FILE" field-name) field-name) val))
+(define-maybe string-or-file-path)
+
+(define (serialize-list field-name val)
+ (string-append (string-join val "\n") "\n"))
+(define-maybe list)
+
+(define (serialize-boolean field-name val)
+ (if val (serialize-string field-name "1") (serialize-string field-name "0")))
+
+(define-configuration/no-serialization miniflux-configuration
+ (listen-address
+ (string "127.0.0.1:8080")
+ "Address to listen on.
+Use absolute path like @code{\"/var/run/miniflux/miniflux.sock\"} for a Unix socket.")
+ (base-url
+ (string "http://127.0.0.1/")
+ "Base URL to generate HTML links and base path for cookies.")
+ (create-administrator-account?
+ (boolean #f)
+ "Create an initial administrator account.")
+ (administrator-account-name
+ maybe-string-or-file-path
+ "Initial administrator account name as a string or an absolute path to a file with a account name inside.")
+ (administrator-account-password
+ maybe-string-or-file-path
+ "Initial administrator account password as a string or an absolute path to a file with a password inside.")
+ (run-migrations?
+ (boolean #t)
+ "Run database migrations during application startup.")
+ (database-url
+ (string "host=/var/run/postgresql")
+ "PostgreSQL connection string.")
+ (user
+ (string "miniflux")
+ "User name for Postgresql and system account.")
+ (group
+ (string "miniflux")
+ "Group for the system account.")
+ (log-file
+ (string "/var/log/miniflux.log")
+ "Path to the log file.")
+ (extra-settings
+ maybe-list
+ "Extra configuration parameters as a list of strings."))
+
+(define (miniflux-serialize-configuration config)
+ (match-record config <miniflux-configuration>
+ (listen-address base-url create-administrator-account?
+ administrator-account-name administrator-account-password
+ run-migrations? database-url extra-settings)
+ (string-append (serialize-string "LISTEN_ADDR" listen-address)
+ (serialize-string "BASE_URL" base-url)
+ (serialize-boolean "CREATE_ADMIN" create-administrator-account?)
+ (serialize-maybe-string-or-file-path "ADMIN_USERNAME" administrator-account-name)
+ (serialize-maybe-string-or-file-path "ADMIN_PASSWORD" administrator-account-password)
+ (serialize-boolean "RUN_MIGRATIONS" run-migrations?)
+ (serialize-string "DATABASE_URL" database-url)
+ (serialize-maybe-list #f extra-settings))))
+
+(define (miniflux-configuration-file config)
+ (mixed-text-file "miniflux.conf" (miniflux-serialize-configuration config)))
+
+(define (pair->file-system-mapping pair previous)
+ (if (pair? pair)
+ (let ((path (car pair))
+ (writable (cdr pair)))
+ (if (or (and (string? path)
+ (absolute-file-name? path))
+ (computed-file? path))
+ (append previous (list (file-system-mapping
+ (source path)
+ (target source)
+ (writable? writable))))
+ previous))
+ previous))
+
+(define (miniflux-shepherd-service config)
+ (match-record config <miniflux-configuration>
+ (user group log-file database-url listen-address
+ administrator-account-name administrator-account-password)
+ (let ((config-file (miniflux-configuration-file config)))
+ (list (shepherd-service
+ (documentation "Run Miniflux server")
+ (provision '(miniflux))
+ (requirement '(postgres networking))
+ (start #~(make-forkexec-constructor
+ (list #$(least-authority-wrapper
+ (file-append miniflux "/bin/miniflux")
+ #:name "miniflux"
+ #:user user
+ #:group group
+ #:preserved-environment-variables
+ (append %default-preserved-environment-variables
+ '("SSL_CERT_FILE"))
+ #:mappings
+ (fold pair->file-system-mapping
+ '()
+ `((,log-file . #t)
+ (,config-file . #f)
+ ("/etc/ssl/certs/ca-certificates.crt" . #f)
+ (,administrator-account-name . #f)
+ (,administrator-account-password . #f)
+ (,(dirname listen-address) . #t)
+ ,(let* ((db-socket-match (string-match ".*host=(/[^ ]*).*" database-url))
+ (db-socket (if db-socket-match (match:substring db-socket-match 1) #f)))
+ (if db-socket
+ `(,db-socket . #t)))))
+ #:namespaces
+ (fold delq %namespaces '(net user)))
+ "-config-file"
+ #$config-file)
+ #:log-file #$log-file))
+ (stop #~(make-kill-destructor)))))))
+
+(define (miniflux-accounts config)
+ (match-record config <miniflux-configuration>
+ (user group)
+ `(,(user-group
+ (name group)
+ (system? #t))
+ ,(user-account
+ (name user)
+ (group group)
+ (system? #t)
+ (comment "miniflux server user")
+ (home-directory "/var/empty")
+ (shell (file-append shadow "/sbin/nologin"))))))
+
+(define (miniflux-postgresql-role config)
+ (list (postgresql-role
+ (name (miniflux-configuration-user config))
+ (create-database? #t))))
+
+(define (miniflux-log-files config)
+ (list (miniflux-configuration-log-file config)))
+
+(define (miniflux-activation-service-type config)
+ (match-record config <miniflux-configuration>
+ (user listen-address)
+ #~(begin
+ (use-modules (gnu build activation))
+ (let ((user (getpwnam #$user)))
+ (if (absolute-file-name? #$listen-address)
+ (mkdir-p/perms (dirname #$listen-address) user #o755))))))
+
+(define miniflux-service-type
+ (service-type
+ (name 'miniflux)
+ (default-value (miniflux-configuration))
+ (extensions
+ (list (service-extension account-service-type
+ miniflux-accounts)
+ (service-extension postgresql-role-service-type
+ miniflux-postgresql-role)
+ (service-extension shepherd-root-service-type
+ miniflux-shepherd-service)
+ (service-extension log-rotation-service-type
+ miniflux-log-files)
+ (service-extension activation-service-type
+ miniflux-activation-service-type)))
+ (description "Run Miniflux, minimalist feed reader")))
diff --git a/gnu/tests/web.scm b/gnu/tests/web.scm
index 431996ede48..419b5f0b5bf 100644
--- a/gnu/tests/web.scm
+++ b/gnu/tests/web.scm
@@ -5,6 +5,7 @@
;;; Copyright © 2018 Pierre-Antoine Rouby <pierre-antoine.rouby@inria.fr>
;;; Copyright © 2018 Marius Bakke <mbakke@fastmail.com>
;;; Copyright © 2024 Maxim Cournoyer <maxim@guixotic.coop>
+;;; Copyright © 2025 Rodion Goritskov <rodion@goritskov.com>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -37,6 +38,7 @@
#:use-module (gnu packages base)
#:use-module (gnu packages databases)
#:use-module (gnu packages guile-xyz)
+ #:use-module (gnu packages gnupg)
#:use-module (gnu packages patchutils)
#:use-module (gnu packages python)
#:use-module (gnu packages tls)
@@ -56,7 +58,10 @@
%test-hpcguix-web
%test-anonip
%test-patchwork
- %test-agate))
+ %test-agate
+ %test-miniflux-admin-string
+ %test-miniflux-admin-file
+ %test-miniflux-socket))
(define %index.html-contents
;; Contents of the /index.html file.
@@ -848,3 +853,190 @@ HTTP-PORT."
(name "agate")
(description "Connect to a running Agate service.")
(value (run-agate-test name %agate-os %index.gmi-contents))))
+
+
+;;;
+;;; Miniflux
+;;;
+
+(define %miniflux-create-admin-credentials
+ #~(begin
+ (mkdir "/var/miniflux")
+ (call-with-output-file "/var/miniflux/admin-username"
+ (lambda (port)
+ (display "test" port)))
+ (call-with-output-file "/var/miniflux/admin-password"
+ (lambda (port)
+ (display "testpassword" port)))))
+
+(define miniflux-base-system
+ (lambda (miniflux-config)
+ (simple-operating-system
+ (simple-service 'create-admin-credentials
+ activation-service-type
+ %miniflux-create-admin-credentials)
+ (service dhcpcd-service-type)
+ (service postgresql-service-type
+ (postgresql-configuration
+ (postgresql postgresql-13)))
+ (service miniflux-service-type
+ miniflux-config))))
+
+(define %miniflux-with-admin-as-string
+ (miniflux-base-system
+ (miniflux-configuration
+ (listen-address "0.0.0.0:8080")
+ (create-administrator-account? #t)
+ (administrator-account-name "test")
+ (administrator-account-password "testpassword"))))
+
+(define %miniflux-with-admin-as-file
+ (miniflux-base-system
+ (miniflux-configuration
+ (listen-address "0.0.0.0:8080")
+ (create-administrator-account? #t)
+ (administrator-account-name "/var/miniflux/admin-username")
+ (administrator-account-password "/var/miniflux/admin-password"))))
+
+(define %miniflux-with-socket
+ (miniflux-base-system
+ (miniflux-configuration
+ (listen-address "/var/run/miniflux/miniflux.sock"))))
+
+(define* (run-miniflux-test name test-os)
+ (define os
+ (marionette-operating-system
+ test-os
+ #:imported-modules '((gnu services herd)
+ (guix combinators))))
+
+ (define forwarded-port 8080)
+
+ (define vm
+ (virtual-machine
+ (operating-system os)
+ (memory-size 512)
+ (port-forwardings `((8080 . ,forwarded-port)))))
+
+ (define test
+ (with-extensions (list guile-gcrypt)
+ (with-imported-modules '((gnu build marionette))
+ #~(begin
+ (use-modules (srfi srfi-64)
+ (srfi srfi-11)
+ (gnu build marionette)
+ (web client)
+ (web uri)
+ (web response)
+ (ice-9 match)
+ (ice-9 iconv)
+ (gcrypt base64))
+
+ (define marionette
+ (make-marionette (list #$vm)))
+
+ (test-runner-current (system-test-runner #$output))
+ (test-begin #$name)
+
+ (test-assert "Check Miniflux service is running"
+ (begin
+ (#$retry-on-error
+ (lambda ()
+ (marionette-eval
+ '(begin
+ (use-modules (gnu services herd))
+ (match (start-service '#$(string->symbol "miniflux"))
+ (#f #f)
+ (('service response-parts ...)
+ (match (assq-ref response-parts 'running)
+ (#f #f)
+ ((running) #t)))))
+ marionette))
+ #:delay 1
+ #:times 10)))
+
+ (test-assert "Miniflux TCP port ready, IPv4"
+ (wait-for-tcp-port #$forwarded-port marionette))
+
+ (test-assert "Miniflux login page is opened"
+ (begin
+ (wait-for-tcp-port #$forwarded-port marionette)
+ (#$retry-on-error
+ (lambda ()
+ (let-values (((_ text)
+ (http-get
+ #$(format #f "http://localhost:~A/" forwarded-port)
+ #:decode-body? #t)))
+ (string-contains text "<title>Sign In - Miniflux</title>")))
+ #:times 10
+ #:delay 2)))
+
+ (define authorization-header
+ (let ((encoded (base64-encode (string->bytevector "test:testpassword" "utf-8"))))
+ `(authorization . (basic . ,encoded))))
+
+ (test-equal "Miniflux initial admin API call is successful"
+ 200
+ (begin
+ (wait-for-tcp-port #$forwarded-port marionette)
+ (#$retry-on-error
+ (lambda ()
+ (let-values (((response _)
+ (http-get #$(format #f "http://localhost:~A/v1/me" forwarded-port)
+ #:headers (list authorization-header)
+ #:decode-body? #t)))
+
+ (response-code response)))
+ #:times 10
+ #:delay 2)))
+
+ (test-end)))))
+ (gexp->derivation (string-append name "-test") test))
+
+(define* (run-miniflux-socket-test name test-os)
+ (define os
+ (marionette-operating-system
+ test-os
+ #:imported-modules '((gnu services herd)
+ (guix combinators))))
+
+ (define vm
+ (virtual-machine
+ (operating-system os)
+ (memory-size 512)))
+
+ (define test
+ (with-imported-modules '((gnu build marionette))
+ #~(begin
+ (use-modules (srfi srfi-64)
+ (gnu build marionette))
+
+ (define marionette
+ (make-marionette (list #$vm)))
+
+ (test-runner-current (system-test-runner #$output))
+ (test-begin #$name)
+
+ (test-assert "Check socket file is created"
+ (wait-for-unix-socket "/var/run/miniflux/miniflux.sock" marionette))
+
+ (test-end))))
+ (gexp->derivation (string-append name "-test") test))
+
+(define %test-miniflux-admin-string
+ (system-test
+ (name "miniflux-admin-string")
+ (description "Run Miniflux with initial admin credentials as string.")
+ (value (run-miniflux-test name %miniflux-with-admin-as-string))))
+
+(define %test-miniflux-admin-file
+ (system-test
+ (name "miniflux-admin-file")
+ (description "Run Miniflux with initial admin credentials as file.")
+ (value (run-miniflux-test name %miniflux-with-admin-as-file))))
+
+(define %test-miniflux-socket
+ (system-test
+ (name "miniflux-socket")
+ (description "Run Miniflux on unix socket.")
+ (value (run-miniflux-socket-test name %miniflux-with-socket))))