Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/linkwarden: init module, linkwarden: init at 2.9.3 #347353

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@

- [duckdns](https://www.duckdns.org), free dynamic DNS. Available with [services.duckdns](options.html#opt-services.duckdns.enable)

- [Linkwarden](https://linkwarden.app/), a self-hosted collaborative bookmark manager to collect, organize, and preserve webpages, articles, and more. Available as [services.linkwarden](#opt-services.linkwarden.enable)

- [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay/), This is a nostr relay, written in Rust. Available as [services.nostr-rs-relay](options.html#opt-services.nostr-rs-relay.enable).

- [Actual Budget](https://actualbudget.org/), a local-first personal finance app. Available as [services.actual](#opt-services.actual.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,7 @@
./services/web-apps/lanraragi.nix
./services/web-apps/lemmy.nix
./services/web-apps/limesurvey.nix
./services/web-apps/linkwarden.nix
./services/web-apps/mainsail.nix
./services/web-apps/mastodon.nix
./services/web-apps/matomo.nix
Expand Down
224 changes: 224 additions & 0 deletions nixos/modules/services/web-apps/linkwarden.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
{
lib,
config,
pkgs,
...
}:

let
cfg = config.services.linkwarden;
isPostgresUnixSocket = lib.hasPrefix "/" cfg.database.host;

inherit (lib)
types
mkIf
mkOption
mkEnableOption
;
in
{
options.services.linkwarden = {
enable = mkEnableOption "Linkwarden";
package = lib.mkPackageOption pkgs "linkwarden" { };

storageLocation = mkOption {
type = types.path;
default = "/var/lib/linkwarden";
description = "Directory used to store media files. If it is not the default, the directory has to be created manually such that the linkwarden user is able to read and write to it.";
};
cacheLocation = mkOption {
type = types.path;
default = "/var/cache/linkwarden";
description = "Directory used as cache. If it is not the default, the directory has to be created manually such that the linkwarden user is able to read and write to it.";
};

enableRegistration = mkEnableOption "registration for new users";

environment = mkOption {
type = types.attrsOf types.str;
default = { };
example = {
PAGINATION_TAKE_COUNT = "50";
};
description = ''
Extra configuration environment variables. Refer to the [documentation](https://docs.linkwarden.app/self-hosting/environment-variables) for options.
'';
};

secretsFile = mkOption {
type = types.str // {
# We don't want users to be able to pass a path literal here but
jvanbruegge marked this conversation as resolved.
Show resolved Hide resolved
# it should look like a path.
check = it: lib.isString it && lib.types.path.check it;
};
example = "/run/secrets/linkwarden";
description = ''
Path of a file with extra environment variables to be loaded from disk.
This file is not added to the nix store, so it can be used to pass secrets to linkwarden.
Refer to the [documentation](https://docs.linkwarden.app/self-hosting/environment-variables) for options.

Linkwarden needs at least a nextauth secret. To set a database password use POSTGRES_PASSWORD:
```
NEXTAUTH_SECRET=<secret>
POSTGRES_PASSWORD=<pass>
```
'';
};

host = mkOption {
type = types.str;
default = "localhost";
description = "The host that Linkwarden will listen on.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "The port that Linkwarden will listen on.";
};
openFirewall = mkOption {
jvanbruegge marked this conversation as resolved.
Show resolved Hide resolved
type = types.bool;
default = false;
description = "Whether to open the Linkwarden port in the firewall";
};
user = mkOption {
type = types.str;
default = "linkwarden";
description = "The user Linkwarden should run as.";
};
group = mkOption {
type = types.str;
default = "linkwarden";
description = "The group Linkwarden should run as.";
};

database = {
enable =
mkEnableOption "the postgresql database for use with Linkwarden. See {option}`services.postgresql`"
jvanbruegge marked this conversation as resolved.
Show resolved Hide resolved
// {
default = true;
};
createDB = mkEnableOption "the automatic creation of the database for Linkwarden." // {
jvanbruegge marked this conversation as resolved.
Show resolved Hide resolved
default = true;
};
name = mkOption {
type = types.str;
default = "linkwarden";
description = "The name of the Linkwarden database.";
};
host = mkOption {
type = types.str;
default = "/run/postgresql";
example = "127.0.0.1";
description = "Hostname or address of the postgresql server. If an absolute path is given here, it will be interpreted as a unix socket path.";
};
port = mkOption {
type = types.port;
default = 5432;
description = "Port of the postgresql server.";
};
user = mkOption {
type = types.str;
default = "linkwarden";
description = "The database user for Linkwarden.";
};
};
};

config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.database.createDB -> cfg.database.name == cfg.database.user;
message = "The postgres module requires the database name and the database user name to be the same.";
}
];

services.postgresql = mkIf cfg.database.enable {
enable = true;
ensureDatabases = mkIf cfg.database.createDB [ cfg.database.name ];
ensureUsers = mkIf cfg.database.createDB [
{
name = cfg.database.user;
ensureDBOwnership = true;
ensureClauses.login = true;
}
];
};

networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];

services.linkwarden.environment = {
LINKWARDEN_HOST = cfg.host;
LINKWARDEN_PORT = toString cfg.port;
LINKWARDEN_CACHE_DIR = cfg.cacheLocation;
STORAGE_FOLDER = cfg.storageLocation;
NEXT_PUBLIC_DISABLE_REGISTRATION = mkIf (!cfg.enableRegistration) "true";
DATABASE_URL = mkIf isPostgresUnixSocket "postgresql://${lib.strings.escapeURL cfg.database.user}@localhost/${lib.strings.escapeURL cfg.database.name}?host=${cfg.database.host}";
DATABASE_PORT = toString cfg.database.port;
DATABASE_HOST = mkIf (!isPostgresUnixSocket) cfg.database.host;
DATABASE_NAME = cfg.database.name;
DATABASE_USER = cfg.database.user;
};

systemd.services.linkwarden = {
description = "Linkwarden (Self-hosted collaborative bookmark manager to collect, organize, and preserve webpages, articles, and more...)";
requires = [
"network-online.target"
] ++ lib.optionals cfg.database.enable [ "postgresql.service" ];
jvanbruegge marked this conversation as resolved.
Show resolved Hide resolved
after = [
"network-online.target"
] ++ lib.optionals cfg.database.enable [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
environment = cfg.environment // {
# Required, otherwise chrome dumps core
CHROME_CONFIG_HOME = cfg.cacheLocation;
};

serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 3;

ExecStart = lib.getExe cfg.package;
EnvironmentFile = cfg.secretsFile;
StateDirectory = "linkwarden";
CacheDirectory = "linkwarden";
User = cfg.user;
Group = cfg.group;

# Hardening
CapabilityBoundingSet = "";
NoNewPrivileges = true;
PrivateUsers = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateMounts = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
};
};

users.users = mkIf (cfg.user == "linkwarden") {
linkwarden = {
name = "linkwarden";
group = cfg.group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "linkwarden") { linkwarden = { }; };

meta.maintainers = with lib.maintainers; [ jvanbruegge ];
};
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ in {
lightdm = handleTest ./lightdm.nix {};
lighttpd = handleTest ./lighttpd.nix {};
limesurvey = handleTest ./limesurvey.nix {};
linkwarden = handleTest ./web-apps/linkwarden.nix {};
listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix {};
litestream = handleTest ./litestream.nix {};
lldap = handleTest ./lldap.nix {};
Expand Down
30 changes: 30 additions & 0 deletions nixos/tests/web-apps/linkwarden.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ../make-test-python.nix (
{ ... }:
{
name = "linkwarden-nixos";

nodes.machine =
{ pkgs, ... }:
let
secretsFile = pkgs.writeText "linkwarden-secret-env" ''
NEXTAUTH_SECRET=VERY_SENSITIVE_SECRET
'';
in
{
services.linkwarden = {
enable = true;
enableRegistration = true;
secretsFile = toString secretsFile;
};
};

testScript = ''
machine.wait_for_unit("linkwarden.service")

machine.wait_for_open_port(3000)
machine.succeed("curl --fail -s http://localhost:3000/")

machine.succeed("curl -L --fail -s --data '{\"name\":\"Admin\",\"username\":\"admin\",\"password\":\"adminadmin\"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/api/v1/users")
'';
}
)
Loading