diff options
author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2023-09-26 18:01:12 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-26 18:01:12 +0000 |
commit | 4bfb61a48c9bc235a242b832719b5f5c2da27ed7 (patch) | |
tree | 5980795d45f2d7eb3ef92f33cf9963aadcc3bdf5 /nixos | |
parent | 2717201e7298b2ecf6521adeb0025a97a677d93d (diff) | |
parent | 8ab2b1221df4ffaafe779f7c2e4880977695d8d5 (diff) | |
download | nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar.gz nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar.bz2 nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar.lz nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar.xz nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.tar.zst nixlib-4bfb61a48c9bc235a242b832719b5f5c2da27ed7.zip |
Merge master into staging-next
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2311.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/audio/castopod.md | 22 | ||||
-rw-r--r-- | nixos/modules/services/audio/castopod.nix | 287 | ||||
-rw-r--r-- | nixos/modules/services/web-apps/peering-manager.nix | 97 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/castopod.nix | 87 |
7 files changed, 464 insertions, 33 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index caa33bf10986..907a586f8dfd 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -46,6 +46,8 @@ - [GoToSocial](https://gotosocial.org/), an ActivityPub social network server, written in Golang. Available as [services.gotosocial](#opt-services.gotosocial.enable). +- [Castopod](https://castopod.org/), an open-source hosting platform made for podcasters who want to engage and interact with their audience. Available as [services.castopod](#opt-services.castopod.enable). + - [Typesense](https://github.com/typesense/typesense), a fast, typo-tolerant search engine for building delightful search experiences. Available as [services.typesense](#opt-services.typesense.enable). * [NS-USBLoader](https://github.com/developersu/ns-usbloader/), an all-in-one tool for managing Nintendo Switch homebrew. Available as [programs.ns-usbloader](#opt-programs.ns-usbloader.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 206d5eaf75de..54fd5c7b0403 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -324,6 +324,7 @@ ./services/amqp/rabbitmq.nix ./services/audio/alsa.nix ./services/audio/botamusique.nix + ./services/audio/castopod.nix ./services/audio/gmediarender.nix ./services/audio/gonic.nix ./services/audio/goxlr-utility.nix diff --git a/nixos/modules/services/audio/castopod.md b/nixos/modules/services/audio/castopod.md new file mode 100644 index 000000000000..ee8590737a7c --- /dev/null +++ b/nixos/modules/services/audio/castopod.md @@ -0,0 +1,22 @@ +# Castopod {#module-services-castopod} + +Castopod is an open-source hosting platform made for podcasters who want to engage and interact with their audience. + +## Quickstart {#module-services-castopod-quickstart} + +Use the following configuration to start a public instance of Castopod on `castopod.example.com` domain: + +```nix +networking.firewall.allowedTCPPorts = [ 80 443 ]; +services.castopod = { + enable = true; + database.createLocally = true; + nginx.virtualHost = { + serverName = "castopod.example.com"; + enableACME = true; + forceSSL = true; + }; +}; +``` + +Go to `https://castopod.example.com/cp-install` to create superadmin account after applying the above configuration. diff --git a/nixos/modules/services/audio/castopod.nix b/nixos/modules/services/audio/castopod.nix new file mode 100644 index 000000000000..b782b5489147 --- /dev/null +++ b/nixos/modules/services/audio/castopod.nix @@ -0,0 +1,287 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.castopod; + fpm = config.services.phpfpm.pools.castopod; + + user = "castopod"; + stateDirectory = "/var/lib/castopod"; + + # https://docs.castopod.org/getting-started/install.html#requirements + phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [ + intl + curl + mbstring + gd + exif + mysqlnd + ] ++ enabled); +in +{ + meta.doc = ./castopod.md; + meta.maintainers = with lib.maintainers; [ alexoundos misuzu ]; + + options.services = { + castopod = { + enable = lib.mkEnableOption (lib.mdDoc "Castopod"); + package = lib.mkOption { + type = lib.types.package; + default = pkgs.castopod; + defaultText = lib.literalMD "pkgs.castopod"; + description = lib.mdDoc "Which Castopod package to use."; + }; + database = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc '' + Create the database and database user locally. + ''; + }; + hostname = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = lib.mdDoc "Database hostname."; + }; + name = lib.mkOption { + type = lib.types.str; + default = "castopod"; + description = lib.mdDoc "Database name."; + }; + user = lib.mkOption { + type = lib.types.str; + default = user; + description = lib.mdDoc "Database user."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/castopod-dbpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + [](#opt-services.castopod.database.user). + ''; + }; + }; + settings = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ str int bool ]); + default = { }; + example = { + "email.protocol" = "smtp"; + "email.SMTPHost" = "localhost"; + "email.SMTPUser" = "myuser"; + "email.fromEmail" = "castopod@example.com"; + }; + description = lib.mdDoc '' + Environment variables used for Castopod. + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) + for available environment variables. + ''; + }; + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/castopod-env"; + description = lib.mdDoc '' + Environment file to inject e.g. secrets into the configuration. + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) + for available environment variables. + ''; + }; + configureNginx = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc "Configure nginx as a reverse proxy for CastoPod."; + }; + localDomain = lib.mkOption { + type = lib.types.str; + example = "castopod.example.org"; + description = lib.mdDoc "The domain serving your CastoPod instance."; + }; + poolSettings = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = "32"; + "pm.start_servers" = "2"; + "pm.min_spare_servers" = "2"; + "pm.max_spare_servers" = "4"; + "pm.max_requests" = "500"; + }; + description = lib.mdDoc '' + Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.castopod.settings = + let + sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null; + baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}"; + in + lib.mapAttrs (name: lib.mkDefault) { + "app.forceGlobalSecureRequests" = sslEnabled; + "app.baseURL" = baseURL; + + "media.baseURL" = "/"; + "media.root" = "media"; + "media.storage" = stateDirectory; + + "admin.gateway" = "admin"; + "auth.gateway" = "auth"; + + "database.default.hostname" = cfg.database.hostname; + "database.default.database" = cfg.database.name; + "database.default.username" = cfg.database.user; + "database.default.DBPrefix" = "cp_"; + + "cache.handler" = "file"; + }; + + services.phpfpm.pools.castopod = { + inherit user; + group = config.services.nginx.group; + phpPackage = phpPackage; + phpOptions = '' + # https://code.castopod.org/adaures/castopod/-/blob/main/docker/production/app/uploads.ini + file_uploads = On + memory_limit = 512M + upload_max_filesize = 500M + post_max_size = 512M + max_execution_time = 300 + max_input_time = 300 + ''; + settings = { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + } // cfg.poolSettings; + }; + + systemd.services.castopod-setup = { + after = lib.optional config.services.mysql.enable "mysql.service"; + requires = lib.optional config.services.mysql.enable "mysql.service"; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.openssl phpPackage ]; + script = + let + envFile = "${stateDirectory}/.env"; + media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}"; + in + '' + mkdir -p ${stateDirectory}/writable/{cache,logs,session,temp,uploads} + + if [ ! -d ${lib.escapeShellArg media} ]; then + cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media} + fi + + if [ ! -f ${stateDirectory}/salt ]; then + openssl rand -base64 33 > ${stateDirectory}/salt + fi + + cat <<'EOF' > ${envFile} + ${lib.generators.toKeyValue { } cfg.settings} + EOF + + echo "analytics.salt=$(cat ${stateDirectory}/salt)" >> ${envFile} + + ${if (cfg.database.passwordFile != null) then '' + echo "database.default.password=$(cat ${lib.escapeShellArg cfg.database.passwordFile})" >> ${envFile} + '' else '' + echo "database.default.password=" >> ${envFile} + ''} + + ${lib.optionalString (cfg.environmentFile != null) '' + cat ${lib.escapeShellArg cfg.environmentFile}) >> ${envFile} + ''} + + php spark castopod:database-update + ''; + serviceConfig = { + StateDirectory = "castopod"; + WorkingDirectory = "${cfg.package}/share/castopod"; + Type = "oneshot"; + RemainAfterExit = true; + User = user; + Group = config.services.nginx.group; + }; + }; + + systemd.services.castopod-scheduled = { + after = [ "castopod-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ phpPackage ]; + script = '' + php public/index.php scheduled-activities + php public/index.php scheduled-websub-publish + php public/index.php scheduled-video-clips + ''; + serviceConfig = { + StateDirectory = "castopod"; + WorkingDirectory = "${cfg.package}/share/castopod"; + Type = "oneshot"; + User = user; + Group = config.services.nginx.group; + }; + }; + + systemd.timers.castopod-scheduled = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* *:*:00"; + Unit = "castopod-scheduled.service"; + }; + }; + + services.mysql = lib.mkIf cfg.database.createLocally { + enable = true; + package = lib.mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [{ + name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + }]; + }; + + services.nginx = lib.mkIf cfg.configureNginx { + enable = true; + virtualHosts."${cfg.localDomain}" = { + root = lib.mkForce "${cfg.package}/share/castopod/public"; + + extraConfig = '' + try_files $uri $uri/ /index.php?$args; + index index.php index.html; + ''; + + locations."^~ /${cfg.settings."media.root"}/" = { + root = cfg.settings."media.storage"; + extraConfig = '' + add_header Access-Control-Allow-Origin "*"; + expires max; + access_log off; + ''; + }; + + locations."~ \.php$" = { + fastcgiParams = { + SERVER_NAME = "$host"; + }; + extraConfig = '' + fastcgi_intercept_errors on; + fastcgi_index index.php; + fastcgi_pass unix:${fpm.socket}; + try_files $uri =404; + fastcgi_read_timeout 3600; + fastcgi_send_timeout 3600; + ''; + }; + }; + }; + + users.users.${user} = lib.mapAttrs (name: lib.mkDefault) { + description = "Castopod user"; + isSystemUser = true; + group = config.services.nginx.group; + }; + }; +} diff --git a/nixos/modules/services/web-apps/peering-manager.nix b/nixos/modules/services/web-apps/peering-manager.nix index 641a3644614f..55bf0da7b7b9 100644 --- a/nixos/modules/services/web-apps/peering-manager.nix +++ b/nixos/modules/services/web-apps/peering-manager.nix @@ -2,40 +2,15 @@ let cfg = config.services.peering-manager; - configFile = pkgs.writeTextFile { - name = "configuration.py"; - text = '' - ALLOWED_HOSTS = ['*'] - DATABASE = { - 'NAME': 'peering-manager', - 'USER': 'peering-manager', - 'HOST': '/run/postgresql', - } - - # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate - # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended - # to use two separate database IDs. - REDIS = { - 'tasks': { - 'UNIX_SOCKET_PATH': '${config.services.redis.servers.peering-manager.unixSocket}', - 'DATABASE': 0, - }, - 'caching': { - 'UNIX_SOCKET_PATH': '${config.services.redis.servers.peering-manager.unixSocket}', - 'DATABASE': 1, - } - } - - with open("${cfg.secretKeyFile}", "r") as file: - SECRET_KEY = file.readline() - '' + lib.optionalString (cfg.peeringdbApiKeyFile != null) '' - with open("${cfg.peeringdbApiKeyFile}", "r") as file: - PEERINGDB_API_KEY = file.readline() - '' + '' - ${cfg.extraConfig} - ''; + pythonFmt = pkgs.formats.pythonVars {}; + settingsFile = pythonFmt.generate "peering-manager-settings.py" cfg.settings; + extraConfigFile = pkgs.writeTextFile { + name = "peering-manager-extraConfig.py"; + text = cfg.extraConfig; }; + configFile = pkgs.concatText "configuration.py" [ settingsFile extraConfigFile ]; + pkg = (pkgs.peering-manager.overrideAttrs (old: { postInstall = '' ln -s ${configFile} $out/opt/peering-manager/peering_manager/configuration.py @@ -106,6 +81,30 @@ in { ''; }; + settings = lib.mkOption { + description = lib.mdDoc '' + Configuration options to set in `configuration.py`. + See the [documentation](https://peering-manager.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options. + ''; + + default = { }; + + type = lib.types.submodule { + freeformType = pythonFmt.type; + + options = { + ALLOWED_HOSTS = lib.mkOption { + type = with lib.types; listOf str; + default = ["*"]; + description = lib.mdDoc '' + A list of valid fully-qualified domain names (FQDNs) and/or IP + addresses that can be used to reach the peering manager service. + ''; + }; + }; + }; + }; + extraConfig = mkOption { type = types.lines; default = ""; @@ -135,7 +134,39 @@ in { }; config = lib.mkIf cfg.enable { - services.peering-manager.plugins = lib.mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]); + services.peering-manager = { + settings = { + DATABASE = { + NAME = "peering-manager"; + USER = "peering-manager"; + HOST = "/run/postgresql"; + }; + + # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate + # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended + # to use two separate database IDs. + REDIS = { + tasks = { + UNIX_SOCKET_PATH = config.services.redis.servers.peering-manager.unixSocket; + DATABASE = 0; + }; + caching = { + UNIX_SOCKET_PATH = config.services.redis.servers.peering-manager.unixSocket; + DATABASE = 1; + }; + }; + }; + + extraConfig = '' + with open("${cfg.secretKeyFile}", "r") as file: + SECRET_KEY = file.readline() + '' + lib.optionalString (cfg.peeringdbApiKeyFile != null) '' + with open("${cfg.peeringdbApiKeyFile}", "r") as file: + PEERINGDB_API_KEY = file.readline() + ''; + + plugins = lib.mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]); + }; system.build.peeringManagerPkg = pkg; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a3e85c337aa2..9fae33a9b347 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -158,6 +158,7 @@ in { cagebreak = handleTest ./cagebreak.nix {}; calibre-web = handleTest ./calibre-web.nix {}; calibre-server = handleTest ./calibre-server.nix {}; + castopod = handleTest ./castopod.nix {}; cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; }; cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; }; cassandra_4 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_4; }; diff --git a/nixos/tests/castopod.nix b/nixos/tests/castopod.nix new file mode 100644 index 000000000000..1d53c3e9a3e6 --- /dev/null +++ b/nixos/tests/castopod.nix @@ -0,0 +1,87 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: +{ + name = "castopod"; + meta = with lib.maintainers; { + maintainers = [ alexoundos misuzu ]; + }; + nodes.castopod = { nodes, ... }: { + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.extraHosts = '' + 127.0.0.1 castopod.example.com + ''; + services.castopod = { + enable = true; + database.createLocally = true; + localDomain = "castopod.example.com"; + }; + environment.systemPackages = + let + username = "admin"; + email = "admin@castood.example.com"; + password = "v82HmEp5"; + testRunner = pkgs.writers.writePython3Bin "test-runner" + { + libraries = [ pkgs.python3Packages.selenium ]; + flakeIgnore = [ + "E501" + ]; + } '' + from selenium.webdriver.common.by import By + from selenium.webdriver import Firefox + from selenium.webdriver.firefox.options import Options + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions as EC + + options = Options() + options.add_argument('--headless') + driver = Firefox(options=options) + try: + driver.implicitly_wait(20) + driver.get('http://castopod.example.com/cp-install') + + wait = WebDriverWait(driver, 10) + + wait.until(EC.title_contains("installer")) + + driver.find_element(By.CSS_SELECTOR, '#username').send_keys( + '${username}' + ) + driver.find_element(By.CSS_SELECTOR, '#email').send_keys( + '${email}' + ) + driver.find_element(By.CSS_SELECTOR, '#password').send_keys( + '${password}' + ) + driver.find_element(By.XPATH, "//button[contains(., 'Finish install')]").click() + + wait.until(EC.title_contains("Auth")) + + driver.find_element(By.CSS_SELECTOR, '#email').send_keys( + '${email}' + ) + driver.find_element(By.CSS_SELECTOR, '#password').send_keys( + '${password}' + ) + driver.find_element(By.XPATH, "//button[contains(., 'Login')]").click() + + wait.until(EC.title_contains("Admin dashboard")) + finally: + driver.close() + driver.quit() + ''; + in + [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ]; + }; + testScript = '' + start_all() + castopod.wait_for_unit("castopod-setup.service") + castopod.wait_for_file("/run/phpfpm/castopod.sock") + castopod.wait_for_unit("nginx.service") + castopod.wait_for_open_port(80) + castopod.wait_until_succeeds("curl -sS -f http://castopod.example.com") + castopod.succeed("curl -s http://localhost/cp-install | grep 'Create your Super Admin account' > /dev/null") + + with subtest("Create superadmin and log in"): + castopod.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner") + ''; +}) |