From 8d374cebcd8736d19c289e6d1166ab0b7428adc7 Mon Sep 17 00:00:00 2001 From: emilylange Date: Sun, 6 Aug 2023 18:40:02 +0200 Subject: [PATCH 1/3] nixos/forgejo: init Following a decicion from both the gitea and forgejo maintainers in nixpkgs. This means, that forgejo will no longer co-use the nixos/gitea module via `services.gitea.package = pkgs.forgejo`. --- nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/forgejo.nix | 668 ++++++++++++++++++++++++ 2 files changed, 669 insertions(+) create mode 100644 nixos/modules/services/misc/forgejo.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 29fcabaefad51e..6ea859b201e475 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -640,6 +640,7 @@ ./services/misc/etesync-dav.nix ./services/misc/evdevremapkeys.nix ./services/misc/felix.nix + ./services/misc/forgejo.nix ./services/misc/freeswitch.nix ./services/misc/fstrim.nix ./services/misc/gammu-smsd.nix diff --git a/nixos/modules/services/misc/forgejo.nix b/nixos/modules/services/misc/forgejo.nix new file mode 100644 index 00000000000000..f26658b7bcb440 --- /dev/null +++ b/nixos/modules/services/misc/forgejo.nix @@ -0,0 +1,668 @@ +{ config, lib, options, pkgs, ... }: + +let + cfg = config.services.forgejo; + opt = options.services.forgejo; + format = pkgs.formats.ini { }; + + exe = lib.getExe cfg.package; + + pg = config.services.postgresql; + useMysql = cfg.database.type == "mysql"; + usePostgresql = cfg.database.type == "postgres"; + useSqlite = cfg.database.type == "sqlite3"; + + inherit (lib) + literalExpression + mdDoc + mkChangedOptionModule + mkDefault + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOptionMD + mkRemovedOptionModule + mkRenamedOptionModule + optionalAttrs + optionals + optionalString + types + ; +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "forgejo" "appName" ] [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ]) + (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ] "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead") + (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ] "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead") + + # copied from services.gitea; remove at some point + (mkRenamedOptionModule [ "services" "forgejo" "cookieSecure" ] [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ]) + (mkRenamedOptionModule [ "services" "forgejo" "disableRegistration" ] [ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ]) + (mkRenamedOptionModule [ "services" "forgejo" "domain" ] [ "services" "forgejo" "settings" "server" "DOMAIN" ]) + (mkRenamedOptionModule [ "services" "forgejo" "httpAddress" ] [ "services" "forgejo" "settings" "server" "HTTP_ADDR" ]) + (mkRenamedOptionModule [ "services" "forgejo" "httpPort" ] [ "services" "forgejo" "settings" "server" "HTTP_PORT" ]) + (mkRenamedOptionModule [ "services" "forgejo" "log" "level" ] [ "services" "forgejo" "settings" "log" "LEVEL" ]) + (mkRenamedOptionModule [ "services" "forgejo" "log" "rootPath" ] [ "services" "forgejo" "settings" "log" "ROOT_PATH" ]) + (mkRenamedOptionModule [ "services" "forgejo" "rootUrl" ] [ "services" "forgejo" "settings" "server" "ROOT_URL" ]) + (mkRenamedOptionModule [ "services" "forgejo" "ssh" "clonePort" ] [ "services" "forgejo" "settings" "server" "SSH_PORT" ]) + (mkRenamedOptionModule [ "services" "forgejo" "staticRootPath" ] [ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ]) + (mkChangedOptionModule [ "services" "forgejo" "enableUnixSocket" ] [ "services" "forgejo" "settings" "server" "PROTOCOL" ] ( + config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http" + )) + (mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ] "services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted") + ]; + + options = { + services.forgejo = { + enable = mkEnableOption (mdDoc "Forgejo"); + + package = mkPackageOptionMD pkgs "forgejo" { }; + + useWizard = mkOption { + default = false; + type = types.bool; + description = mdDoc '' + Whether to use the built-in installation wizard instead of + declaratively managing the {file}`app.ini` config file in nix. + ''; + }; + + stateDir = mkOption { + default = "/var/lib/forgejo"; + type = types.str; + description = mdDoc "Forgejo data directory."; + }; + + customDir = mkOption { + default = "${cfg.stateDir}/custom"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; + type = types.str; + description = mdDoc '' + Base directory for custom templates and other options. + + If {option}`${opt.useWizard}` is disabled (default), this directory will also + hold secrets and the resulting {file}`app.ini` config at runtime. + ''; + }; + + user = mkOption { + type = types.str; + default = "forgejo"; + description = mdDoc "User account under which Forgejo runs."; + }; + + group = mkOption { + type = types.str; + default = "forgejo"; + description = mdDoc "Group under which Forgejo runs."; + }; + + database = { + type = mkOption { + type = types.enum [ "sqlite3" "mysql" "postgres" ]; + example = "mysql"; + default = "sqlite3"; + description = mdDoc "Database engine to use."; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc "Database host address."; + }; + + port = mkOption { + type = types.port; + default = if !usePostgresql then 3306 else pg.port; + defaultText = literalExpression '' + if config.${opt.database.type} != "postgresql" + then 3306 + else config.${options.services.postgresql.port} + ''; + description = mdDoc "Database host port."; + }; + + name = mkOption { + type = types.str; + default = "forgejo"; + description = mdDoc "Database name."; + }; + + user = mkOption { + type = types.str; + default = "forgejo"; + description = mdDoc "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/forgejo-dbpassword"; + description = mdDoc '' + A file containing the password corresponding to + {option}`${opt.database.user}`. + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null; + defaultText = literalExpression "null"; + example = "/run/mysqld/mysqld.sock"; + description = mdDoc "Path to the unix socket file to use for authentication."; + }; + + path = mkOption { + type = types.str; + default = "${cfg.stateDir}/data/forgejo.db"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"''; + description = mdDoc "Path to the sqlite3 database file."; + }; + + createDatabase = mkOption { + type = types.bool; + default = true; + description = mdDoc "Whether to create a local database automatically."; + }; + }; + + dump = { + enable = mkEnableOption (mdDoc "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)"); + + interval = mkOption { + type = types.str; + default = "04:31"; + example = "hourly"; + description = mdDoc '' + Run a Forgejo dump at this interval. Runs by default at 04:31 every day. + + The format is described in + {manpage}`systemd.time(7)`. + ''; + }; + + backupDir = mkOption { + type = types.str; + default = "${cfg.stateDir}/dump"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"''; + description = mdDoc "Path to the directory where the dump archives will be stored."; + }; + + type = mkOption { + type = types.enum [ "zip" "tar" "tar.sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" "tar.zst" ]; + default = "zip"; + description = mdDoc "Archive format used to store the dump file."; + }; + + file = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc "Filename to be used for the dump. If `null` a default name is chosen by forgejo."; + example = "forgejo-dump"; + }; + }; + + lfs = { + enable = mkOption { + type = types.bool; + default = false; + description = mdDoc "Enables git-lfs support."; + }; + + contentDir = mkOption { + type = types.str; + default = "${cfg.stateDir}/data/lfs"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"''; + description = mdDoc "Where to store LFS files."; + }; + }; + + repositoryRoot = mkOption { + type = types.str; + default = "${cfg.stateDir}/repositories"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"''; + description = mdDoc "Path to the git repositories."; + }; + + mailerPasswordFile = mkOption { + type = types.nullOr types.str; + default = null; + example = "/run/keys/forgejo-mailpw"; + description = mdDoc "Path to a file containing the SMTP password."; + }; + + settings = mkOption { + default = { }; + description = mdDoc '' + Free-form settings written directly to the `app.ini` configfile file. + Refer to for supported values. + ''; + example = literalExpression '' + { + DEFAULT = { + RUN_MODE = "dev"; + }; + "cron.sync_external_users" = { + RUN_AT_START = true; + SCHEDULE = "@every 24h"; + UPDATE_EXISTING = true; + }; + mailer = { + ENABLED = true; + MAILER_TYPE = "sendmail"; + FROM = "do-not-reply@example.org"; + SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail"; + }; + other = { + SHOW_FOOTER_VERSION = false; + }; + } + ''; + type = types.submodule { + freeformType = format.type; + options = { + log = { + ROOT_PATH = mkOption { + default = "${cfg.stateDir}/log"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"''; + type = types.str; + description = mdDoc "Root path for log files."; + }; + LEVEL = mkOption { + default = "Info"; + type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ]; + description = mdDoc "General log level."; + }; + }; + + server = { + PROTOCOL = mkOption { + type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ]; + default = "http"; + description = mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."''; + }; + + HTTP_ADDR = mkOption { + type = types.either types.str types.path; + default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"; + defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"''; + description = mdDoc "Listen address. Must be a path when using a unix socket."; + }; + + HTTP_PORT = mkOption { + type = types.port; + default = 3000; + description = mdDoc "Listen port. Ignored when using a unix socket."; + }; + + DOMAIN = mkOption { + type = types.str; + default = "localhost"; + description = mdDoc "Domain name of your server."; + }; + + ROOT_URL = mkOption { + type = types.str; + default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; + defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"''; + description = mdDoc "Full public URL of Forgejo server."; + }; + + STATIC_ROOT_PATH = mkOption { + type = types.either types.str types.path; + default = cfg.package.data; + defaultText = literalExpression "config.${opt.package}.data"; + example = "/var/lib/forgejo/data"; + description = mdDoc "Upper level of template and static files path."; + }; + + DISABLE_SSH = mkOption { + type = types.bool; + default = false; + description = mdDoc "Disable external SSH feature."; + }; + + SSH_PORT = mkOption { + type = types.port; + default = 22; + example = 2222; + description = mdDoc '' + SSH port displayed in clone URL. + The option is required to configure a service when the external visible port + differs from the local listening port i.e. if port forwarding is used. + ''; + }; + }; + + session = { + COOKIE_SECURE = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Marks session cookies as "secure" as a hint for browsers to only send + them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS. + ''; + }; + }; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; + message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned"; + } + ]; + + services.forgejo.settings = { + DEFAULT = { + RUN_MODE = mkDefault "prod"; + RUN_USER = mkDefault cfg.user; + WORK_PATH = mkDefault cfg.stateDir; + }; + + database = mkMerge [ + { + DB_TYPE = cfg.database.type; + } + (mkIf (useMysql || usePostgresql) { + HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port; + NAME = cfg.database.name; + USER = cfg.database.user; + PASSWD = "#dbpass#"; + }) + (mkIf useSqlite { + PATH = cfg.database.path; + }) + (mkIf usePostgresql { + SSL_MODE = "disable"; + }) + ]; + + repository = { + ROOT = cfg.repositoryRoot; + }; + + server = mkIf cfg.lfs.enable { + LFS_START_SERVER = true; + LFS_JWT_SECRET = "#lfsjwtsecret#"; + }; + + session = { + COOKIE_NAME = mkDefault "session"; + }; + + security = { + SECRET_KEY = "#secretkey#"; + INTERNAL_TOKEN = "#internaltoken#"; + INSTALL_LOCK = true; + }; + + mailer = mkIf (cfg.mailerPasswordFile != null) { + PASSWD = "#mailerpass#"; + }; + + oauth2 = { + JWT_SECRET = "#oauth2jwtsecret#"; + }; + + lfs = mkIf cfg.lfs.enable { + PATH = cfg.lfs.contentDir; + }; + }; + + services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { + enable = mkDefault true; + + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + } + ]; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + + # If we have a folder or symlink with Forgejo locales, remove it + # And symlink the current Forgejo locales in place + "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" + + ] ++ optionals cfg.lfs.enable [ + "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.forgejo = { + description = "Forgejo (Beyond coding. We forge.)"; + after = [ + "network.target" + ] ++ optionals usePostgresql [ + "postgresql.service" + ] ++ optionals useMysql [ + "mysql.service" + ]; + requires = optionals (cfg.database.createDatabase && usePostgresql) [ + "postgresql.service" + ] ++ optionals (cfg.database.createDatabase && useMysql) [ + "mysql.service" + ]; + wantedBy = [ "multi-user.target" ]; + path = [ cfg.package pkgs.git pkgs.gnupg ]; + + # In older versions the secret naming for JWT was kind of confusing. + # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET + # wasn't persistent at all. + # To fix that, there is now the file oauth2_jwt_secret containing the + # values for JWT_SECRET and the file jwt_secret gets renamed to + # lfs_jwt_secret. + # We have to consider this to stay compatible with older installations. + preStart = + let + runConfig = "${cfg.customDir}/conf/app.ini"; + secretKey = "${cfg.customDir}/conf/secret_key"; + oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; + oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET + lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET + internalToken = "${cfg.customDir}/conf/internal_token"; + replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; + in + '' + # copy custom configuration and generate random secrets if needed + ${lib.optionalString (!cfg.useWizard) '' + function forgejo_setup { + cp -f '${format.generate "app.ini" cfg.settings}' '${runConfig}' + + if [ ! -s '${secretKey}' ]; then + ${exe} generate secret SECRET_KEY > '${secretKey}' + fi + + # Migrate LFS_JWT_SECRET filename + if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then + mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' + fi + + if [ ! -s '${oauth2JwtSecret}' ]; then + ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' + fi + + ${optionalString cfg.lfs.enable '' + if [ ! -s '${lfsJwtSecret}' ]; then + ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' + fi + ''} + + if [ ! -s '${internalToken}' ]; then + ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' + fi + + chmod u+w '${runConfig}' + ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' + ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' + ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' + + ${optionalString cfg.lfs.enable '' + ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' + ''} + + ${optionalString (cfg.database.passwordFile != null) '' + ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' + ''} + + ${optionalString (cfg.mailerPasswordFile != null) '' + ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' + ''} + chmod u-w '${runConfig}' + } + (umask 027; forgejo_setup) + ''} + + # run migrations/init the database + ${exe} migrate + + # update all hooks' binary paths + ${exe} admin regenerate hooks + + # update command option in authorized_keys + if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] + then + ${exe} admin regenerate keys + fi + ''; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.stateDir; + ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid"; + Restart = "always"; + # Runtime directory and mode + RuntimeDirectory = "forgejo"; + RuntimeDirectoryMode = "0755"; + # Proc filesystem + ProcSubset = "pid"; + ProtectProc = "invisible"; + # Access write directories + ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; + UMask = "0027"; + # Capabilities + CapabilityBoundingSet = ""; + # Security + NoNewPrivileges = true; + # Sandboxing + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + # System Call Filtering + SystemCallArchitectures = "native"; + SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" "setrlimit" ]; + }; + + environment = { + USER = cfg.user; + HOME = cfg.stateDir; + # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497 + # is resolved. + GITEA_WORK_DIR = cfg.stateDir; + GITEA_CUSTOM = cfg.customDir; + }; + }; + + users.users = mkIf (cfg.user == "forgejo") { + forgejo = { + home = cfg.stateDir; + useDefaultShell = true; + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "forgejo") { + forgejo = { }; + }; + + systemd.services.forgejo-dump = mkIf cfg.dump.enable { + description = "forgejo dump"; + after = [ "forgejo.service" ]; + path = [ cfg.package ]; + + environment = { + USER = cfg.user; + HOME = cfg.stateDir; + # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497 + # is resolved. + GITEA_WORK_DIR = cfg.stateDir; + GITEA_CUSTOM = cfg.customDir; + }; + + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + ExecStart = "${exe} dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; + WorkingDirectory = cfg.dump.backupDir; + }; + }; + + systemd.timers.forgejo-dump = mkIf cfg.dump.enable { + description = "Forgejo dump timer"; + partOf = [ "forgejo-dump.service" ]; + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = cfg.dump.interval; + }; + }; + + meta.maintainers = with lib.maintainers; [ bendlas emilylange ]; +} From 02601e17a53eadd488bd8ca16dbb656fd46d1764 Mon Sep 17 00:00:00 2001 From: emilylange Date: Sun, 6 Aug 2023 18:41:37 +0200 Subject: [PATCH 2/3] nixosTests.forgejo: fork from nixosTests.gitea --- nixos/tests/all-tests.nix | 2 +- nixos/tests/forgejo.nix | 157 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/forgejo.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3b4a39f5ff96b8..d9aa9eccac02d2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -280,7 +280,7 @@ in { fluentd = handleTest ./fluentd.nix {}; fluidd = handleTest ./fluidd.nix {}; fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {}; - forgejo = handleTest ./gitea.nix { giteaPackage = pkgs.forgejo; }; + forgejo = handleTest ./forgejo.nix { }; freenet = handleTest ./freenet.nix {}; freeswitch = handleTest ./freeswitch.nix {}; freshrss-sqlite = handleTest ./freshrss-sqlite.nix {}; diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix new file mode 100644 index 00000000000000..b326819e319064 --- /dev/null +++ b/nixos/tests/forgejo.nix @@ -0,0 +1,157 @@ +{ system ? builtins.currentSystem +, config ? { } +, pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; +with pkgs.lib; + +let + ## gpg --faked-system-time='20230301T010000!' --quick-generate-key snakeoil ed25519 sign + signingPrivateKey = '' + -----BEGIN PGP PRIVATE KEY BLOCK----- + + lFgEY/6jkBYJKwYBBAHaRw8BAQdADXiZRV8RJUyC9g0LH04wLMaJL9WTc+szbMi7 + 5fw4yP8AAQCl8EwGfzSLm/P6fCBfA3I9znFb3MEHGCCJhJ6VtKYyRw7ktAhzbmFr + ZW9pbIiUBBMWCgA8FiEE+wUM6VW/NLtAdSixTWQt6LZ4x50FAmP+o5ACGwMFCQPC + ZwAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJEE1kLei2eMedFTgBAKQs1oGFZrCI + TZP42hmBTKxGAI1wg7VSdDEWTZxut/2JAQDGgo2sa4VHMfj0aqYGxrIwfP2B7JHO + GCqGCRf9O/hzBA== + =9Uy3 + -----END PGP PRIVATE KEY BLOCK----- + ''; + signingPrivateKeyId = "4D642DE8B678C79D"; + + supportedDbTypes = [ "mysql" "postgres" "sqlite3" ]; + makeGForgejoTest = type: nameValuePair type (makeTest { + name = "forgejo-${type}"; + meta.maintainers = with maintainers; [ bendlas emilylange ]; + + nodes = { + server = { config, pkgs, ... }: { + virtualisation.memorySize = 2047; + services.forgejo = { + enable = true; + database = { inherit type; }; + settings.service.DISABLE_REGISTRATION = true; + settings."repository.signing".SIGNING_KEY = signingPrivateKeyId; + settings.actions.ENABLED = true; + }; + environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq ]; + services.openssh.enable = true; + + specialisation.runner = { + inheritParentConfig = true; + configuration.services.gitea-actions-runner.instances."test" = { + enable = true; + name = "ci"; + url = "http://localhost:3000"; + labels = [ + # don't require docker/podman + "native:host" + ]; + tokenFile = "/var/lib/forgejo/runner_token"; + }; + }; + }; + client1 = { config, pkgs, ... }: { + environment.systemPackages = [ pkgs.git ]; + }; + client2 = { config, pkgs, ... }: { + environment.systemPackages = [ pkgs.git ]; + }; + }; + + testScript = { nodes, ... }: + let + inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + serverSystem = nodes.server.system.build.toplevel; + in + '' + GIT_SSH_COMMAND = "ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no" + REPO = "forgejo@server:test/repo" + PRIVK = "${snakeOilPrivateKey}" + + start_all() + + client1.succeed("mkdir /tmp/repo") + client1.succeed("mkdir -p $HOME/.ssh") + client1.succeed(f"cat {PRIVK} > $HOME/.ssh/privk") + client1.succeed("chmod 0400 $HOME/.ssh/privk") + client1.succeed("git -C /tmp/repo init") + client1.succeed("echo hello world > /tmp/repo/testfile") + client1.succeed("git -C /tmp/repo add .") + client1.succeed("git config --global user.email test@localhost") + client1.succeed("git config --global user.name test") + client1.succeed("git -C /tmp/repo commit -m 'Initial import'") + client1.succeed(f"git -C /tmp/repo remote add origin {REPO}") + + server.wait_for_unit("forgejo.service") + server.wait_for_open_port(3000) + server.wait_for_open_port(22) + server.succeed("curl --fail http://localhost:3000/") + + server.succeed( + "su -l forgejo -c 'gpg --homedir /var/lib/forgejo/data/home/.gnupg " + + "--import ${toString (pkgs.writeText "forgejo.key" signingPrivateKey)}'" + ) + + assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg") + + server.succeed( + "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. " + + "Please contact your site administrator.'" + ) + server.succeed( + "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea admin user create " + + "--username test --password totallysafe --email test@localhost'" + ) + + api_token = server.succeed( + "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens " + + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d " + + "'{\"name\":\"token\",\"scopes\":[\"all\"]}' | jq '.sha1' | xargs echo -n" + ) + + server.succeed( + "curl --fail -X POST http://localhost:3000/api/v1/user/repos " + + "-H 'Accept: application/json' -H 'Content-Type: application/json' " + + f"-H 'Authorization: token {api_token}'" + + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\''' + ) + + server.succeed( + "curl --fail -X POST http://localhost:3000/api/v1/user/keys " + + "-H 'Accept: application/json' -H 'Content-Type: application/json' " + + f"-H 'Authorization: token {api_token}'" + + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\''' + ) + + client1.succeed( + f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git -C /tmp/repo push origin master" + ) + + client2.succeed("mkdir -p $HOME/.ssh") + client2.succeed(f"cat {PRIVK} > $HOME/.ssh/privk") + client2.succeed("chmod 0400 $HOME/.ssh/privk") + client2.succeed(f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git clone {REPO}") + client2.succeed('test "$(cat repo/testfile | xargs echo -n)" = "hello world"') + + server.wait_until_succeeds( + 'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits ' + + '-H "Accept: application/json" | jq length)" = "1"', + timeout=10 + ) + + with subtest("Testing runner registration"): + server.succeed( + "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token" + ) + server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test") + server.wait_for_unit("gitea-runner-test.service") + server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'") + ''; + }); +in + +listToAttrs (map makeGForgejoTest supportedDbTypes) From 7b786b39cb0d42949720482b78c31fcfe35b41c7 Mon Sep 17 00:00:00 2001 From: emilylange Date: Sun, 6 Aug 2023 18:43:08 +0200 Subject: [PATCH 3/3] CODEOWNERS: init forgejo --- .github/CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 98a7022088eb22..829ce356f9db37 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -288,6 +288,10 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt /nixos/modules/services/misc/matrix-conduit.nix @piegamesde /nixos/tests/matrix-conduit.nix @piegamesde +# Forgejo +nixos/modules/services/misc/forgejo.nix @bendlas @emilylange +pkgs/applications/version-management/forgejo @bendlas @emilylange + # Dotnet /pkgs/build-support/dotnet @IvarWithoutBones /pkgs/development/compilers/dotnet @IvarWithoutBones