From 7bba5c1e980b4c0561333b7f800fbfb8227fb7a3 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Mon, 30 Mar 2026 17:54:55 +0200 Subject: [PATCH] Add Tribes packaging and system modules --- README.md | 18 +++ nbde/system/installed-base.scm | 56 +++++++ tribes/packages/otp.scm | 71 +++++++++ tribes/packages/release.scm | 35 +++++ tribes/packages/source.scm | 252 ++++++++++++++++++++++++++++++ tribes/services/tribes.scm | 272 +++++++++++++++++++++++++++++++++ tribes/system/installer.scm | 114 ++++++++++++++ tribes/system/node.scm | 37 +++++ 8 files changed, 855 insertions(+) create mode 100644 nbde/system/installed-base.scm create mode 100644 tribes/packages/otp.scm create mode 100644 tribes/packages/release.scm create mode 100644 tribes/packages/source.scm create mode 100644 tribes/services/tribes.scm create mode 100644 tribes/system/installer.scm create mode 100644 tribes/system/node.scm diff --git a/README.md b/README.md index f26f2c6..d98e9c3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,24 @@ encryption: Minimal reference system using the Clevis-backed mapped-device kind and custom initrd. +It now also carries the first Tribes deployment substrate: + +- `tribes/packages/release.scm` + A deployment-bridge package wrapper for a prebuilt Tribes release tree. +- `tribes/packages/source.scm` + A real source-built Tribes package that produces a production release from a + vendored Mix dependency tree plus local Parrhesia source. +- `tribes/services/tribes.scm` + Shepherd service, runtime environment wiring, and account/activation setup + for a Tribes node. +- `tribes/system/node.scm` + A higher-level service bundle that wires PostgreSQL plus the Tribes service. +- `tribes/system/installer.scm` + Installer-facing OS constructor for NBDE-installed Tribes nodes. +- `nbde/system/installed-base.scm` + Shared base installed-system constructor used by both the minimal NBDE flow + and the Tribes-specific installer path. + Current development status: 1. `luksmeta`, `tang`, and `clevis` build successfully on `pguix`. diff --git a/nbde/system/installed-base.scm b/nbde/system/installed-base.scm new file mode 100644 index 0000000..f8f8287 --- /dev/null +++ b/nbde/system/installed-base.scm @@ -0,0 +1,56 @@ +(define-module (nbde system installed-base) + #:use-module (gnu) + #:use-module (gnu services base) + #:use-module (gnu services desktop) + #:use-module (gnu services networking) + #:use-module (gnu services ssh) + #:use-module (nbde system boot-store) + #:export (nbde-installed-operating-system)) + +(define* (nbde-installed-operating-system #:key + host-name + bootloader + mapped-devices + file-systems + initrd + interface + authorized-keys-file + (timezone "Etc/UTC") + (locale "en_US.UTF-8") + (kernel-arguments + (list "console=tty0" + "console=ttyS0,115200n8")) + (initrd-modules + (append '("nvme" "sd_mod" "virtio_scsi") + %base-initrd-modules)) + (extra-services '())) + "Return a base installed Guix system for the NBDE flow, parameterized by the +runtime-discovered boot and filesystem values from the installer." + (operating-system + (host-name host-name) + (timezone timezone) + (locale locale) + (keyboard-layout (keyboard-layout "us")) + (kernel-arguments kernel-arguments) + (initrd-modules initrd-modules) + (initrd initrd) + (bootloader bootloader) + (mapped-devices mapped-devices) + (file-systems file-systems) + (services + (append + extra-services + (list (service dhcpcd-service-type) + (boot-store-staging-service) + (service elogind-service-type) + (service agetty-service-type + (agetty-configuration + (tty "ttyS0") + (term "vt100"))) + (service openssh-service-type + (openssh-configuration + (permit-root-login 'prohibit-password) + (authorized-keys + (list + (list "root" authorized-keys-file)))))) + %base-services)))) diff --git a/tribes/packages/otp.scm b/tribes/packages/otp.scm new file mode 100644 index 0000000..f744152 --- /dev/null +++ b/tribes/packages/otp.scm @@ -0,0 +1,71 @@ +(define-module (tribes packages otp) + #:use-module (guix download) + #:use-module (guix git-download) + #:use-module (guix packages) + #:use-module (guix utils) + #:use-module (gnu packages) + #:use-module (gnu packages bash) + #:use-module (gnu packages elixir) + #:use-module (gnu packages erlang) + #:use-module (gnu packages perl) + #:use-module (gnu packages version-control) + #:export (erlang-28 + elixir-otp28 + elixir-hex-otp28)) + +(define-public erlang-28 + (package + (inherit erlang) + (name "erlang-28") + (version "28.4.1") + (source + (origin + (method git-fetch) + (uri (git-reference + (url "https://github.com/erlang/otp") + (commit (string-append "OTP-" version)))) + (file-name (git-file-name name version)) + (sha256 + (base32 + "1lsbmjfraw03d0kcdzmjdjad8b95d630d1jmg8hjklmivc13l6pa")) + (patches (search-patches "erlang-man-path.patch")))) + (native-inputs + `(("perl" ,perl) + ("erlang-manpages" + ,(origin + (method url-fetch) + (uri (string-append + "https://github.com/erlang/otp/releases/download" + "/OTP-" version "/otp_doc_man_" version ".tar.gz")) + (sha256 + (base32 + "00simi301qz3ssn71r77jmsyfz8sb61wp7k92j3gh7pq7gmmc40j")))))))) + +(define-public elixir-otp28 + (package + (inherit elixir) + (name "elixir-otp28") + (version "1.19.5") + (source + (origin + (method git-fetch) + (uri (git-reference + (url "https://github.com/elixir-lang/elixir") + (commit (string-append "v" version)))) + (file-name (git-file-name name version)) + (sha256 + (base32 + "1i10a5d7mlcrav47k7qirqvrqn2kbl5265fbcp8fzavr86xz67m6")) + (patches (search-patches "elixir-path-length.patch")))) + (inputs + `(("bash-minimal" ,bash-minimal) + ("erlang" ,erlang-28) + ("rebar3" ,rebar3) + ("git" ,git))))) + +(define-public elixir-hex-otp28 + (package + (inherit elixir-hex) + (name "elixir-hex-otp28") + (inputs + `(("elixir" ,elixir-otp28))))) diff --git a/tribes/packages/release.scm b/tribes/packages/release.scm new file mode 100644 index 0000000..ca660b3 --- /dev/null +++ b/tribes/packages/release.scm @@ -0,0 +1,35 @@ +(define-module (tribes packages release) + #:use-module ((guix licenses) #:prefix license:) + #:use-module (guix build-system trivial) + #:use-module (guix gexp) + #:use-module (guix packages) + #:export (tribes-release-package)) + +(define* (tribes-release-package source + #:key + (name "tribes-release") + (version "0.0.0") + (synopsis "Tribes release bundle") + (description + "Packaged Tribes release directory for deployment.") + (home-page "https://git.teralink.net/self/tribes")) + "Return a package that copies SOURCE, expected to be a built Tribes release +directory, into the store unchanged. This is intended as a deployment bridge +until the full native Guix package graph for Tribes is available." + (package + (name name) + (version version) + (source source) + (build-system trivial-build-system) + (arguments + (list + #:modules '((guix build utils)) + #:builder + #~(begin + (use-modules (guix build utils)) + (mkdir-p #$output) + (copy-recursively #+source #$output #:follow-symlinks? #t)))) + (synopsis synopsis) + (description description) + (home-page home-page) + (license license:asl2.0))) diff --git a/tribes/packages/source.scm b/tribes/packages/source.scm new file mode 100644 index 0000000..32c83b4 --- /dev/null +++ b/tribes/packages/source.scm @@ -0,0 +1,252 @@ +(define-module (tribes packages source) + #:use-module ((guix licenses) #:prefix license:) + #:use-module (guix build-system trivial) + #:use-module (guix git-download) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (guix utils) + #:use-module (gnu packages admin) + #:use-module (gnu packages autotools) + #:use-module (gnu packages bash) + #:use-module (gnu packages base) + #:use-module (gnu packages commencement) + #:use-module (gnu packages erlang) + #:use-module (gnu packages gawk) + #:use-module (gnu packages linux) + #:use-module (gnu packages m4) + #:use-module (gnu packages nss) + #:use-module (gnu packages perl) + #:use-module (gnu packages pkg-config) + #:use-module (gnu packages version-control) + #:use-module (tribes packages otp) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-13) + #:export (tribes-source-package + tribes-source-directory->local-file)) + +(define %excluded-root-basenames + '(".cache" + ".claude" + ".direnv" + ".elixir_ls" + ".git" + ".hex" + ".home" + ".mix-home" + ".pre-commit-config.yaml" + "_build" + "deps" + "erl_crash.dump" + "node_modules" + "result")) + +(define (root-artifact-path? root file entry) + (let ((entry-path (string-append root "/" entry))) + (or (string=? file entry-path) + (string-prefix? (string-append entry-path "/") file)))) + +(define (transient-source-file? root file) + (let ((base (basename file))) + (or (any (lambda (entry) + (root-artifact-path? root file entry)) + %excluded-root-basenames) + (root-artifact-path? root file ".env") + (string-prefix? ".devenv" base) + (string=? base ".env") + (string-suffix? ".log" base) + (string-suffix? ".tsbuildinfo" base) + (string-suffix? ".db" base) + (string-suffix? ".db-shm" base) + (string-suffix? ".db-wal" base)))) + +(define (tribes-source-select? root file stat) + (or (string=? file root) + (not (transient-source-file? root file)))) + +(define (tribes-source-directory->local-file directory) + "Return DIRECTORY as a recursively copied local-file, excluding transient +build artifacts and, when available, anything not tracked in the enclosing Git +checkout." + (let ((git-select? (git-predicate directory))) + (local-file directory + #:recursive? #t + #:select? + (if git-select? + (lambda (file stat) + (and (git-select? file stat) + (tribes-source-select? directory file stat))) + (lambda (file stat) + (tribes-source-select? directory file stat)))))) + +(define* (tribes-source-package source + mix-deps + parrhesia-source + #:key + (name "tribes") + (version "0.2.0") + (home-page "https://git.teralink.net/self/tribes") + (synopsis "Tribes social app") + (description + "Tribes social application built from source as a +production Elixir release using a vendored Mix dependency tree.")) + "Return a Guix package that builds a production Tribes release from SOURCE, +using MIX-DEPS as the pre-fetched Mix dependency tree and PARRHESIA-SOURCE as +the local Parrhesia path dependency." + (package + (name name) + (version version) + (source source) + (build-system trivial-build-system) + (native-inputs + (list autoconf + autoconf-wrapper + automake + bash-minimal + coreutils + elixir-otp28 + elixir-hex-otp28 + findutils + gcc-toolchain + gawk + git-minimal + grep + gnu-make + libtool + linux-libre-headers + m4 + nss-certs + perl + pkg-config + sed + rebar3)) + (arguments + (list + #:modules '((guix build utils) + (ice-9 ftw) + (ice-9 textual-ports) + (srfi srfi-1)) + #:builder + #~(begin + (use-modules (guix build utils) + (ice-9 ftw) + (ice-9 textual-ports) + (srfi srfi-1)) + + (define out #$output) + (define work (string-append (getcwd) "/build")) + (define app-dir (string-append work "/tribes")) + (define parrhesia-dir (string-append work "/parrhesia")) + (define deps-dir (string-append app-dir "/deps")) + (define cert-file + #$(file-append nss-certs "/etc/ssl/certs/ca-certificates.crt")) + (define hex-lib-dir + #$(file-append elixir-hex-otp28 + "/lib/elixir/" + (version-major+minor + (package-version elixir-otp28)))) + (define kernel-headers-dir + #$(file-append linux-libre-headers "/include")) + (define sh-bin + #$(file-append bash-minimal "/bin/sh")) + (define path + (string-join + (list #$(file-append elixir-otp28 "/bin") + #$(file-append elixir-hex-otp28 "/bin") + #$(file-append rebar3 "/bin") + #$(file-append gcc-toolchain "/bin") + #$(file-append gnu-make "/bin") + #$(file-append autoconf-wrapper "/bin") + #$(file-append autoconf "/bin") + #$(file-append automake "/bin") + #$(file-append bash-minimal "/bin") + #$(file-append coreutils "/bin") + #$(file-append findutils "/bin") + #$(file-append libtool "/bin") + #$(file-append gawk "/bin") + #$(file-append grep "/bin") + #$(file-append m4 "/bin") + #$(file-append perl "/bin") + #$(file-append pkg-config "/bin") + #$(file-append sed "/bin") + #$(file-append git-minimal "/bin") + (or (getenv "PATH") "")) + ":")) + + (mkdir-p work) + (copy-recursively #+source app-dir #:follow-symlinks? #t) + (copy-recursively #+parrhesia-source parrhesia-dir #:follow-symlinks? #t) + (copy-recursively #+mix-deps deps-dir #:follow-symlinks? #t) + (invoke #$(file-append coreutils "/bin/chmod") + "-R" "u+w" + app-dir + parrhesia-dir + deps-dir) + + (substitute* (string-append app-dir "/mix.exs") + (("\\{:parrhesia, github: \"serpent213/parrhesia\", branch: \"master\"\\}") + "{:parrhesia, path: \"../parrhesia\"}")) + (let ((mix-lock (string-append app-dir "/mix.lock"))) + (when (file-exists? mix-lock) + (substitute* mix-lock + (("^ \"parrhesia\": \\{:git, \"https://github\\.com/serpent213/parrhesia\\.git\", \".*\", \\[branch: \"master\"\\]\\},$") + "")))) + (let ((libsecp-makefile + (string-append deps-dir "/lib_secp256k1/Makefile"))) + (when (file-exists? libsecp-makefile) + (substitute* libsecp-makefile + (("\\./autogen\\.sh") + (string-append sh-bin " ./autogen.sh"))))) + + (setenv "PATH" path) + (setenv "HOME" (string-append app-dir "/.home")) + (setenv "MIX_HOME" (string-append app-dir "/.mix-home")) + (setenv "HEX_HOME" (string-append app-dir "/.hex")) + (setenv "XDG_CACHE_HOME" (string-append app-dir "/.cache")) + (let ((existing-elixir-libs (getenv "GUIX_ELIXIR_LIBS"))) + (setenv "GUIX_ELIXIR_LIBS" + (if existing-elixir-libs + (string-append hex-lib-dir ":" existing-elixir-libs) + hex-lib-dir))) + (setenv "MIX_ENV" "prod") + (setenv "MIX_REBAR3" #$(file-append rebar3 "/bin/rebar3")) + (setenv "SSL_CERT_FILE" cert-file) + (let ((existing-cpath (getenv "CPATH"))) + (setenv "CPATH" + (if existing-cpath + (string-append kernel-headers-dir ":" existing-cpath) + kernel-headers-dir))) + (let ((existing-c-include-path (getenv "C_INCLUDE_PATH"))) + (setenv "C_INCLUDE_PATH" + (if existing-c-include-path + (string-append kernel-headers-dir ":" existing-c-include-path) + kernel-headers-dir))) + (setenv "CC" #$(file-append gcc-toolchain "/bin/gcc")) + (setenv "CXX" #$(file-append gcc-toolchain "/bin/g++")) + (mkdir-p (getenv "HOME")) + (mkdir-p (getenv "MIX_HOME")) + (mkdir-p (getenv "HEX_HOME")) + (mkdir-p (getenv "XDG_CACHE_HOME")) + + (with-directory-excursion app-dir + ;; Absinthe has been the last fragile dependency in Guix builds so + ;; far. Compile its immediate deps first, then compile Absinthe in + ;; its own pass before compiling the remaining dependency graph. + (invoke "mix" "deps.compile" "nimble_parsec" "telemetry" "decimal") + (let ((existing-erl-flags (getenv "ERL_FLAGS"))) + ;; Keep the scheduler count pinned while compiling Absinthe so + ;; toolchain-sensitive ordering issues stay deterministic. + (setenv "ERL_FLAGS" + (if existing-erl-flags + (string-append existing-erl-flags " +S 1:1") + "+S 1:1")) + (invoke "mix" "deps.compile" "absinthe" "--force") + (if existing-erl-flags + (setenv "ERL_FLAGS" existing-erl-flags) + (unsetenv "ERL_FLAGS"))) + (invoke "mix" "deps.compile") + (invoke "mix" "compile") + (invoke "mix" "release" "--path" out))))) + (home-page home-page) + (synopsis synopsis) + (description description) + (license license:asl2.0))) diff --git a/tribes/services/tribes.scm b/tribes/services/tribes.scm new file mode 100644 index 0000000..55f3d37 --- /dev/null +++ b/tribes/services/tribes.scm @@ -0,0 +1,272 @@ +(define-module (tribes services tribes) + #:use-module (gnu services) + #:use-module (gnu services base) + #:use-module (gnu services databases) + #:use-module (gnu services shepherd) + #:use-module (gnu packages admin) + #:use-module (gnu system shadow) + #:use-module (guix gexp) + #:use-module (guix records) + #:use-module (ice-9 match) + #:use-module (srfi srfi-13) + #:export (tribes-configuration + tribes-configuration? + tribes-configuration-package + tribes-configuration-user + tribes-configuration-group + tribes-configuration-working-directory + tribes-configuration-plugin-directory + tribes-configuration-host + tribes-configuration-scheme + tribes-configuration-port + tribes-configuration-relay-url + tribes-configuration-host-manifest + tribes-configuration-admin-pubkeys + tribes-configuration-sync-overlap-seconds + tribes-configuration-database-user + tribes-configuration-database-name + tribes-configuration-parrhesia-database-name + tribes-configuration-database-host + tribes-configuration-secret-key-base-file + tribes-configuration-token-signing-secret-file + tribes-configuration-dns-cluster-query + tribes-configuration-extra-environment-variables + tribes-configuration-log-file + tribes-service-type)) + +(define-record-type* + tribes-configuration make-tribes-configuration + tribes-configuration? + (package tribes-configuration-package + (default #f)) + (user tribes-configuration-user + (default "tribes")) + (group tribes-configuration-group + (default "tribes")) + (working-directory tribes-configuration-working-directory + (default "/var/lib/tribes")) + (plugin-directory tribes-configuration-plugin-directory + (default "/var/lib/tribes/plugins")) + (host tribes-configuration-host + (default "localhost")) + (scheme tribes-configuration-scheme + (default "http")) + (port tribes-configuration-port + (default 4000)) + (relay-url tribes-configuration-relay-url + (default #f)) + (host-manifest tribes-configuration-host-manifest + (default #f)) + (admin-pubkeys tribes-configuration-admin-pubkeys + (default '())) + (sync-overlap-seconds tribes-configuration-sync-overlap-seconds + (default 300)) + (database-user tribes-configuration-database-user + (default "tribes")) + (database-name tribes-configuration-database-name + (default "tribes")) + (parrhesia-database-name tribes-configuration-parrhesia-database-name + (default "parrhesia")) + (database-host tribes-configuration-database-host + (default "/var/run/postgresql")) + (secret-key-base-file tribes-configuration-secret-key-base-file + (default "/var/lib/tribes/secrets/secret_key_base")) + (token-signing-secret-file tribes-configuration-token-signing-secret-file + (default "/var/lib/tribes/secrets/token_signing_secret")) + (dns-cluster-query tribes-configuration-dns-cluster-query + (default #f)) + (extra-environment-variables tribes-configuration-extra-environment-variables + (default '())) + (log-file tribes-configuration-log-file + (default "/var/log/tribes/tribes.log"))) + +(define (tribes-accounts config) + (list + (user-group + (name (tribes-configuration-group config)) + (system? #t)) + (user-account + (name (tribes-configuration-user config)) + (group (tribes-configuration-group config)) + (system? #t) + (comment "Tribes service user") + (home-directory (tribes-configuration-working-directory config)) + (shell (file-append shadow "/sbin/nologin"))))) + +(define (tribes-relay-url config) + (or (tribes-configuration-relay-url config) + (let* ((scheme (tribes-configuration-scheme config)) + (host (tribes-configuration-host config)) + (port (tribes-configuration-port config)) + (ws-scheme (if (string=? scheme "https") "wss" "ws")) + (default-port? + (or (and (string=? scheme "https") (= port 443)) + (and (string=? scheme "http") (= port 80))))) + (if default-port? + (string-append ws-scheme "://" host "/nostr/relay") + (string-append ws-scheme "://" host ":" (number->string port) + "/nostr/relay"))))) + +(define (tribes-database-url config database-name) + (string-append "ecto://" + (tribes-configuration-database-user config) + "@/" + database-name + "?host=" + (tribes-configuration-database-host config))) + +(define (tribes-launcher config command args) + (define package + (tribes-configuration-package config)) + (define env-setters + (append + (list + #~(setenv "HOME" #$(tribes-configuration-working-directory config)) + #~(setenv "PHX_SERVER" "true") + #~(setenv "PORT" #$(number->string (tribes-configuration-port config))) + #~(setenv "PHX_HOST" #$(tribes-configuration-host config)) + #~(setenv "DATABASE_URL" + #$(tribes-database-url + config + (tribes-configuration-database-name config))) + #~(setenv "PARRHESIA_DATABASE_URL" + #$(tribes-database-url + config + (tribes-configuration-parrhesia-database-name config))) + #~(setenv "PARRHESIA_RELAY_URL" #$(tribes-relay-url config)) + #~(setenv "TRIBES_PLUGIN_DIR" #$(tribes-configuration-plugin-directory config)) + #~(setenv "TRIBES_SYNC_OVERLAP_SECONDS" + #$(number->string + (tribes-configuration-sync-overlap-seconds config))) + #~(setenv "TRIBES_ADMIN_PUBKEYS" + #$(string-join + (tribes-configuration-admin-pubkeys config) + ",")) + #~(setenv "SSL_CERT_DIR" "/etc/ssl/certs") + #~(setenv "SSL_CERT_FILE" "/etc/ssl/certs/ca-certificates.crt")) + (if (tribes-configuration-host-manifest config) + (list #~(setenv "TRIBES_HOST_MANIFEST" + #$(tribes-configuration-host-manifest config))) + '()) + (if (tribes-configuration-dns-cluster-query config) + (list #~(setenv "DNS_CLUSTER_QUERY" + #$(tribes-configuration-dns-cluster-query config))) + '()) + (map (lambda (entry) + #~(let* ((entry #$entry) + (parts (string-split entry #\=)) + (name (car parts)) + (value (string-join (cdr parts) "="))) + (setenv name value))) + (tribes-configuration-extra-environment-variables config)))) + (program-file + (string-append "tribes-" command) + #~(begin + (use-modules (ice-9 textual-ports) + (srfi srfi-13)) + + (define (read-secret path) + (string-trim-right (call-with-input-file path get-string-all))) + + (define secret-key-file + #$(tribes-configuration-secret-key-base-file config)) + (define token-file + #$(tribes-configuration-token-signing-secret-file config)) + + (unless (file-exists? secret-key-file) + (format (current-error-port) + "missing Tribes secret file: ~a~%" + secret-key-file) + (exit 1)) + + (unless (file-exists? token-file) + (format (current-error-port) + "missing Tribes token-signing secret file: ~a~%" + token-file) + (exit 1)) + + (setenv "SECRET_KEY_BASE" (read-secret secret-key-file)) + (setenv "TOKEN_SIGNING_SECRET" (read-secret token-file)) + #$@env-setters + (apply execl + #$(file-append package "/bin/tribes") + "tribes" + (cons #$command '#$args))))) + +(define (tribes-activation config) + #~(begin + (use-modules (guix build utils) + (ice-9 match) + (srfi srfi-1)) + (let* ((user (getpwnam #$(tribes-configuration-user config))) + (uid (passwd:uid user)) + (gid (passwd:gid user)) + (dirs (list #$(tribes-configuration-working-directory config) + #$(tribes-configuration-plugin-directory config) + (dirname #$(tribes-configuration-log-file config)) + (dirname #$(tribes-configuration-secret-key-base-file config)) + (dirname #$(tribes-configuration-token-signing-secret-file config))))) + (for-each + (lambda (dir) + (mkdir-p dir) + (chown dir uid gid)) + dirs)))) + +(define (tribes-migrations-shepherd-service config) + (let ((launcher (tribes-launcher + config + "eval" + '("Tribes.Release.migrate_with_storage_up()")))) + (list + (shepherd-service + (documentation "Run Tribes database migrations.") + (provision '(tribes-migrations)) + (requirement '(postgres user-processes)) + (one-shot? #t) + (start + #~(lambda _ + (zero? (spawn-command + (list #$launcher) + #:user #$(tribes-configuration-user config) + #:group #$(tribes-configuration-group config))))) + (respawn? #f))))) + +(define (tribes-shepherd-service config) + (let ((launcher (tribes-launcher config "start" '()))) + (list + (shepherd-service + (documentation "Run the Tribes application service.") + (provision '(tribes)) + (requirement '(tribes-migrations networking user-processes)) + (start + #~(make-forkexec-constructor + (list #$launcher) + #:user #$(tribes-configuration-user config) + #:group #$(tribes-configuration-group config) + #:log-file #$(tribes-configuration-log-file config))) + (stop #~(make-kill-destructor)) + (respawn? #f))))) + +(define (tribes-profile-packages config) + (match (tribes-configuration-package config) + (#f '()) + (package (list package)))) + +(define tribes-service-type + (service-type + (name 'tribes) + (extensions + (list (service-extension account-service-type + tribes-accounts) + (service-extension activation-service-type + tribes-activation) + (service-extension shepherd-root-service-type + tribes-migrations-shepherd-service) + (service-extension shepherd-root-service-type + tribes-shepherd-service) + (service-extension profile-service-type + tribes-profile-packages))) + (default-value (tribes-configuration)) + (description + "Run a Tribes release with PostgreSQL-backed storage, Parrhesia, and +cluster runtime configuration sourced from files and service settings."))) diff --git a/tribes/system/installer.scm b/tribes/system/installer.scm new file mode 100644 index 0000000..7e89d74 --- /dev/null +++ b/tribes/system/installer.scm @@ -0,0 +1,114 @@ +(define-module (tribes system installer) + #:use-module (gnu packages databases) + #:use-module (gnu services databases) + #:use-module (guix gexp) + #:use-module (nbde system installed-base) + #:use-module (tribes packages release) + #:use-module (tribes packages source) + #:use-module (tribes services tribes) + #:use-module (tribes system node) + #:use-module (srfi srfi-13) + #:export (tribes-installer-operating-system)) + +(define (getenv/default name default) + (or (getenv name) default)) + +(define (getenv/integer name default) + (let ((value (getenv name))) + (if value + (or (string->number value) + (error "invalid integer environment variable" name value)) + default))) + +(define (comma-list value) + (if (or (not value) (string-null? value)) + '() + (map string-trim-both + (filter (lambda (item) (not (string-null? item))) + (string-split value #\,))))) + +(define* (tribes-installer-operating-system #:key + host-name + bootloader + mapped-devices + file-systems + initrd + interface + authorized-keys-file) + "Return an installed NBDE operating-system extended with PostgreSQL and the +Tribes service. The package source is provided through +TRIBES_RELEASE_DIRECTORY and wrapped as a Guix package at evaluation time." + (let* ((release-directory + (getenv "TRIBES_RELEASE_DIRECTORY")) + (source-directory (getenv "TRIBES_SOURCE_DIRECTORY")) + (mix-deps-directory (getenv "TRIBES_MIX_DEPS_DIRECTORY")) + (parrhesia-directory (getenv "TRIBES_PARRHESIA_DIRECTORY")) + (service-user (getenv/default "TRIBES_SERVICE_USER" "tribes")) + (service-group (getenv/default "TRIBES_SERVICE_GROUP" service-user)) + (database-user (getenv/default "TRIBES_DATABASE_USER" service-user)) + (package + (cond + ((and source-directory mix-deps-directory parrhesia-directory) + (tribes-source-package + (tribes-source-directory->local-file source-directory) + (tribes-source-directory->local-file mix-deps-directory) + (tribes-source-directory->local-file parrhesia-directory) + #:version (getenv/default "TRIBES_RELEASE_VERSION" "dev"))) + (release-directory + (tribes-release-package + (local-file release-directory #:recursive? #t) + #:version (getenv/default "TRIBES_RELEASE_VERSION" "dev"))) + (else + (error "missing Tribes package input; set TRIBES_RELEASE_DIRECTORY or " + "TRIBES_SOURCE_DIRECTORY, TRIBES_MIX_DEPS_DIRECTORY, and " + "TRIBES_PARRHESIA_DIRECTORY")))) + (tribes-config + (tribes-configuration + (package package) + (user service-user) + (group service-group) + (working-directory + (getenv/default "TRIBES_WORKING_DIRECTORY" "/var/lib/tribes")) + (plugin-directory + (getenv/default "TRIBES_PLUGIN_DIRECTORY" "/var/lib/tribes/plugins")) + (host (getenv/default "TRIBES_PUBLIC_HOST" host-name)) + (scheme (getenv/default "TRIBES_SCHEME" "http")) + (port (getenv/integer "TRIBES_HTTP_PORT" 4000)) + (relay-url (getenv "TRIBES_RELAY_URL")) + (host-manifest (getenv "TRIBES_HOST_MANIFEST")) + (admin-pubkeys + (comma-list (getenv "TRIBES_ADMIN_PUBKEYS"))) + (sync-overlap-seconds + (getenv/integer "TRIBES_SYNC_OVERLAP_SECONDS" 300)) + (database-user database-user) + (database-name + (getenv/default "TRIBES_DATABASE_NAME" "tribes")) + (parrhesia-database-name + (getenv/default "TRIBES_PARRHESIA_DATABASE_NAME" "parrhesia")) + (database-host + (getenv/default "TRIBES_DATABASE_HOST" "/var/run/postgresql")) + (secret-key-base-file + (getenv/default "TRIBES_SECRET_KEY_BASE_FILE" + "/var/lib/tribes/secrets/secret_key_base")) + (token-signing-secret-file + (getenv/default "TRIBES_TOKEN_SIGNING_SECRET_FILE" + "/var/lib/tribes/secrets/token_signing_secret")) + (dns-cluster-query (getenv "TRIBES_DNS_CLUSTER_QUERY")) + (extra-environment-variables + (comma-list (getenv "TRIBES_EXTRA_ENV"))) + (log-file + (getenv/default "TRIBES_LOG_FILE" "/var/log/tribes/tribes.log")))) + (node-config + (tribes-node-configuration + (postgresql (postgresql-configuration + (postgresql postgresql))) + (tribes tribes-config)))) + (nbde-installed-operating-system + #:host-name host-name + #:bootloader bootloader + #:mapped-devices mapped-devices + #:file-systems file-systems + #:initrd initrd + #:interface interface + #:authorized-keys-file authorized-keys-file + #:extra-services (tribes-node-services node-config)))) diff --git a/tribes/system/node.scm b/tribes/system/node.scm new file mode 100644 index 0000000..bc8bd04 --- /dev/null +++ b/tribes/system/node.scm @@ -0,0 +1,37 @@ +(define-module (tribes system node) + #:use-module (gnu packages databases) + #:use-module (gnu services) + #:use-module (gnu services databases) + #:use-module (guix records) + #:use-module (tribes services tribes) + #:export (tribes-node-configuration + tribes-node-configuration? + tribes-node-configuration-postgresql + tribes-node-configuration-tribes + tribes-node-services)) + +(define-record-type* + tribes-node-configuration make-tribes-node-configuration + tribes-node-configuration? + (postgresql tribes-node-configuration-postgresql + (default (postgresql-configuration + (postgresql postgresql)))) + (tribes tribes-node-configuration-tribes + (default (tribes-configuration)))) + +(define (tribes-node-postgresql-roles config) + (let ((tribes (tribes-node-configuration-tribes config))) + (list + (postgresql-role + (name (tribes-configuration-database-user tribes)) + (permissions '(createdb login)))))) + +(define (tribes-node-services config) + (list + (service postgresql-service-type + (tribes-node-configuration-postgresql config)) + (simple-service 'tribes-postgresql-roles + postgresql-role-service-type + (tribes-node-postgresql-roles config)) + (service tribes-service-type + (tribes-node-configuration-tribes config))))