guix_mirror_bot pushed a commit to branch master in repository guix. commit 054aae7bb201f6ba83390017a4cc644061a6abda Author: Rodion Goritskov <rod...@goritskov.com> AuthorDate: Sat Jul 19 10:52:11 2025 +0200
services: Add miniflux-service-type. * doc/guix.texi: Document Miniflux service and configuration. * gnu/services/web.scm: New service. * gnu/services/web.scm: Define shepherd service and account roles. * gnu/tests/web.scm: (%miniflux-create-admin-credentials, miniflux-base-system, %test-miniflux-admin-string, %test-miniflux-admin-file, %test-miniflux-socket): Add system tests for Miniflux service. Change-Id: I4a336e677ec8b46aed632f0ded9cc11c2d38975f Signed-off-by: Ludovic Courtès <l...@gnu.org> --- doc/guix.texi | 75 ++++++++++++++++++++ gnu/services/web.scm | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++- gnu/tests/web.scm | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 461 insertions(+), 2 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index af91f02d1f..9aadad4c2e 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -144,6 +144,7 @@ Copyright @copyright{} 2024 Evgeny Pisemsky@* Copyright @copyright{} 2025 jgart@* Copyright @copyright{} 2025 Artur Wroblewski@* Copyright @copyright{} 2025 Edouard Klein@* +Copyright @copyright{} 2025 Rodion Goritskov@* Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or @@ -35700,6 +35701,80 @@ The file which should store the logging output of Agate. @end table @end deftp +@subsubheading Miniflux + +@cindex miniflux +The @uref{https://miniflux.app/, Miniflux} is a minimalist RSS feed reader +with a web interface. + +Depending on the configuration, an initial administrator user can be pre-created +on startup. To enable this, @code{create-admin?} should be set to @code{#t}, and +both @code{admin-username-file} and @code{admin-password-file} should point to +files containing the username and password, respectively. However, it is +recommended to manually change the password to a secure one via the web +UI after the initial service startup. + +@defvar miniflux-service-type +This is the type of the Miniflux service. Its value +must be a @code{miniflux-configuration} record as in this example: + +@lisp +(service miniflux-service-type + (miniflux-configuration + (listen-address "0.0.0.0:8080") + (base-url "http://my-news-source.test") + (create-administrator-account? #t) + (administrator-account-name "/var/miniflux/initial-admin-username") + (administrator-account-password "/var/miniflux/initial-admin-password"))) +@end lisp + +The details of the @code{miniflux-configuration} record type are given below. + +@end defvar + +@deftp {Data Type} miniflux-configuration +Available @code{miniflux-configuration} fields are: + +@table @asis +@item @code{listen-address} (default: @code{"127.0.0.1:8080"}) (type: string) +Address to listen on. +Use absolute path like @code{"/var/run/miniflux/miniflux.sock"} for a Unix socket. + +@item @code{base-url} (default: @code{"http://127.0.0.1/"}) (type: string) +Base URL to generate HTML links and base path for cookies. + +@item @code{create-administrator-account?} (default: @code{#f}) (type: boolean) +Create an initial administrator account. + +@item @code{administrator-account-name} (type: maybe-string-or-file-path) +Initial administrator account name as a string or an absolute path +to a file with an account name inside. + +@item @code{administrator-account-password} (type: maybe-string-or-file-path) +Initial administrator account password as a string or an absolute path +to a file with a password inside. + +@item @code{run-migrations?} (default: @code{#t}) (type: boolean) +Run database migrations during application startup. + +@item @code{database-url} (default: @code{"host=/var/run/postgresql"}) (type: string) +PostgreSQL connection string. + +@item @code{user} (default: @code{"miniflux"}) (type: string) +User name for Postgresql and system account. + +@item @code{group} (default: @code{"miniflux"}) (type: string) +Group for the system account. + +@item @code{log-file} (default: @code{"/var/log/miniflux.log"}) (type: string) +Path to the log file. + +@item @code{extra-settings} (type: maybe-list) +Extra configuration parameters as a list of strings. + +@end table +@end deftp + @node High Availability Services @subsection High Availability Services diff --git a/gnu/services/web.scm b/gnu/services/web.scm index 3989607646..ae33a25394 100644 --- a/gnu/services/web.scm +++ b/gnu/services/web.scm @@ -19,6 +19,7 @@ ;;; Copyright © 2023 Miguel Ángel Moreno <m...@migalmoreno.com> ;;; Copyright © 2024 Leo Nikkilä <he...@lnikki.la> ;;; Copyright © 2025 Maxim Cournoyer <maxim.courno...@gmail.com> +;;; Copyright © 2025 Rodion Goritskov <rod...@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 431996ede4..419b5f0b5b 100644 --- a/gnu/tests/web.scm +++ b/gnu/tests/web.scm @@ -5,6 +5,7 @@ ;;; Copyright © 2018 Pierre-Antoine Rouby <pierre-antoine.ro...@inria.fr> ;;; Copyright © 2018 Marius Bakke <mba...@fastmail.com> ;;; Copyright © 2024 Maxim Cournoyer <ma...@guixotic.coop> +;;; Copyright © 2025 Rodion Goritskov <rod...@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))))