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

plausible: init at 1.3.0 #124055

Merged
merged 9 commits into from
Jun 2, 2021
Merged
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
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,7 @@
./services/web-apps/nextcloud.nix
./services/web-apps/nexus.nix
./services/web-apps/plantuml-server.nix
./services/web-apps/plausible.nix
./services/web-apps/pgpkeyserver-lite.nix
./services/web-apps/matomo.nix
./services/web-apps/moinmoin.nix
Expand Down
273 changes: 273 additions & 0 deletions nixos/modules/services/web-apps/plausible.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
{ lib, pkgs, config, ... }:

with lib;

let
cfg = config.services.plausible;

# FIXME consider using LoadCredential as soon as it actually works.
envSecrets = ''
export ADMIN_USER_PWD="$(<${cfg.adminUser.passwordFile})"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As pretty much 100% of shell scripts, this is probably wrong:

The shell this runs in doesn't have set -e nor any other reasonable way of error propagation, so when the configured file isn't readable by the DynamicUser, this code just prints e.g.

/nix/store/rwfwdk34fyz9qqyvn4vs0lnswscd73x0-plausible-setup: line 2: /var/lib/plausible/plausible-admin-user-password-file: Permission denied
/nix/store/rwfwdk34fyz9qqyvn4vs0lnswscd73x0-plausible-setup: line 3: /var/lib/plausible/plausible-secret-keybase-file: Permission denied

and then happily goes on with its day, with ADMIN_USER_PWD and SECRET_KEY_BASE set to the empty string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if it had set -e, it would still be wrong, in the same way as the scripts of Elixir itself here:

elixir-lang/elixir#11114

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes in PR #130062

export SECRET_KEY_BASE="$(<${cfg.server.secretKeybaseFile})"
${optionalString (cfg.mail.smtp.passwordFile != null) ''
export SMTP_USER_PWD="$(<${cfg.mail.smtp.passwordFile})"
''}
'';
in {
options.services.plausible = {
enable = mkEnableOption "plausible";

adminUser = {
name = mkOption {
default = "admin";
type = types.str;
description = ''
Name of the admin user that plausible will created on initial startup.
'';
};

email = mkOption {
type = types.str;
example = "admin@localhost";
description = ''
Email-address of the admin-user.
'';
};

passwordFile = mkOption {
type = types.either types.str types.path;
description = ''
Path to the file which contains the password of the admin user.
'';
};

activate = mkEnableOption "activating the freshly created admin-user";
};

database = {
clickhouse = {
setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
url = mkOption {
default = "http://localhost:8123/default";
type = types.str;
description = ''
The URL to be used to connect to <package>clickhouse</package>.
'';
};
};
postgres = {
setup = mkEnableOption "creating a postgresql instance" // { default = true; };
dbname = mkOption {
default = "plausible";
type = types.str;
description = ''
Name of the database to use.
'';
};
socket = mkOption {
default = "/run/postgresql";
type = types.str;
description = ''
Path to the UNIX domain-socket to communicate with <package>postgres</package>.
'';
};
};
};

server = {
disableRegistration = mkOption {
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
default = true;
type = types.bool;
description = ''
Whether to prohibit creating an account in plausible's UI.
'';
};
secretKeybaseFile = mkOption {
type = types.either types.path types.str;
description = ''
Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
how to generate one are documented in the
<link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
framework docs</link>.
'';
};
port = mkOption {
default = 8000;
type = types.port;
description = ''
Port where the service should be available.
'';
};
baseUrl = mkOption {
type = types.str;
description = ''
Public URL where plausible is available.
'';
};
};

mail = {
email = mkOption {
default = "hello@plausible.local";
type = types.str;
description = ''
The email id to use for as <emphasis>from</emphasis> address of all communications
from Plausible.
'';
};
smtp = {
hostAddr = mkOption {
default = "localhost";
type = types.str;
description = ''
The host address of your smtp server.
'';
};
hostPort = mkOption {
default = 25;
type = types.port;
description = ''
The port of your smtp server.
'';
};
user = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
The username/email in case SMTP auth is enabled.
'';
};
passwordFile = mkOption {
default = null;
type = with types; nullOr (either str path);
description = ''
The path to the file with the password in case SMTP auth is enabled.
'';
};
enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
retries = mkOption {
type = types.ints.unsigned;
default = 2;
description = ''
Number of retries to make until mailer gives up.
'';
};
};
};
};

config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
message = ''
Unable to automatically activate the admin-user if no locally managed DB for
postgres (`services.plausible.database.postgres.setup') is enabled!
'';
}
];

services.postgresql = mkIf cfg.database.postgres.setup {
enable = true;
};

services.clickhouse = mkIf cfg.database.clickhouse.setup {
enable = true;
happysalada marked this conversation as resolved.
Show resolved Hide resolved
};

systemd.services = mkMerge [
{
plausible = {
inherit (pkgs.plausible.meta) description;
documentation = [ "https://plausible.io/docs/self-hosting" ];
wantedBy = [ "multi-user.target" ];
after = optional cfg.database.postgres.setup "plausible-postgres.service";
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
requires = optional cfg.database.clickhouse.setup "clickhouse.service"
++ optionals cfg.database.postgres.setup [
"postgresql.service"
"plausible-postgres.service"
];

environment = {
# NixOS specific option to avoid that it's trying to write into its store-path.
# See also https://github.com/lau/tzdata#data-directory-and-releases
TZDATA_DIR = "/var/lib/plausible/elixir_tzdata";

# Configuration options from
# https://plausible.io/docs/self-hosting-configuration
PORT = toString cfg.server.port;
DISABLE_REGISTRATION = boolToString cfg.server.disableRegistration;

RELEASE_TMP = "/var/lib/plausible/tmp";

ADMIN_USER_NAME = cfg.adminUser.name;
ADMIN_USER_EMAIL = cfg.adminUser.email;

DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
DATABASE_NAME = cfg.database.postgres.dbname;
CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;

BASE_URL = cfg.server.baseUrl;

MAILER_EMAIL = cfg.mail.email;
SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
SMTP_RETRIES = toString cfg.mail.smtp.retries;
SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;

SELFHOST = "true";
} // (optionalAttrs (cfg.mail.smtp.user != null) {
SMTP_USER_NAME = cfg.mail.smtp.user;
});

path = [ pkgs.plausible ]
++ optional cfg.database.postgres.setup config.services.postgresql.package;

serviceConfig = {
DynamicUser = true;
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
PrivateTmp = true;
WorkingDirectory = "/var/lib/plausible";
StateDirectory = "plausible";
ExecStartPre = "@${pkgs.writeShellScript "plausible-setup" ''
${envSecrets}
${pkgs.plausible}/createdb.sh
${pkgs.plausible}/migrate.sh
${optionalString cfg.adminUser.activate ''
if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
psql -d plausible <<< "UPDATE users SET email_verified=true;"
fi
''}
''} plausible-setup";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the argument to this shell script for?

It calls e.g.

/nix/store/skpa6a2l84ydidgxnpv63fnc1fsxyv6k-plausible-setup plausible-setup

But the shell script defined directly above does not use this argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To quote systemd.service(5):

If the executable path is prefixed with "@", the second specified
token will be passed as "argv[0]" to the executed process (instead of
the actual filename), followed by the further arguments specified.

A similar approach for shorter names is done in e.g. the module of Hydra or the firewall :)

Does that answer your question?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wanted to ask you why you used @ in this case? (rather without)
You don't actually use the argument in this case right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ma27 Ah yes, thank you! Is the intent to make it look better in ps/htop?

ExecStart = "@${pkgs.writeShellScript "plausible" ''
Ma27 marked this conversation as resolved.
Show resolved Hide resolved
${envSecrets}
plausible start
''} plausible";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be a need to add a service for a remote shell ?
In order to be able to connect to a running instance, you need to /bin/plausible remote with the necessary environment variable.
I'm just thinking it might be a very nice convenience in order to debug or check what is happening. It definitely might not be useful for the average user.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I guess it's nicer though to have a wrapper-script for that which can be executed from the shell. An example for what I mean would be nextcloud-occ.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good! I'm not sure I understand the way secrets are managed, but otherwise it looks good!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm even if the environment is correctly set up, plausible remote complains about a missing HOME dir even though it's set (checked by looking at /proc/pid/environ)... will take a look at this later.

(mkIf cfg.database.postgres.setup {
# `plausible' requires the `citext'-extension.
plausible-postgres = {
after = [ "postgresql.service" ];
bindsTo = [ "postgresql.service" ];
requiredBy = [ "plausible.service" ];
partOf = [ "plausible.service" ];
serviceConfig.Type = "oneshot";
unitConfig.ConditionPathExists = "!/var/lib/plausible/.db-setup";
script = ''
mkdir -p /var/lib/plausible/
PSQL() {
/run/wrappers/bin/sudo -Hu postgres ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
}
PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
PSQL -tAc "CREATE DATABASE plausible WITH OWNER plausible;"
PSQL -d plausible -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
touch /var/lib/plausible/.db-setup
'';
};
})
];
};

meta.maintainers = with maintainers; [ ma27 ];
meta.doc = ./plausible.xml;
}
51 changes: 51 additions & 0 deletions nixos/modules/services/web-apps/plausible.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<chapter xmlns="http://docbook.org/ns/docbook"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please convert nixos docs to markdown.

xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-services-plausible">
<title>Plausible</title>
<para>
<link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
Google analytics.
</para>
<section xml:id="module-services-plausible-basic-usage">
<title>Basic Usage</title>
<para>
At first, a secret key is needed to be generated. This can be done with e.g.
<screen><prompt>$ </prompt>openssl rand -base64 64</screen>
</para>
<para>
After that, <package>plausible</package> can be deployed like this:
<programlisting>{
services.plausible = {
<link linkend="opt-services.plausible.enable">enable</link> = true;
adminUser = {
<link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
<link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
<link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
};
server = {
<link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
<link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
};
};
}</programlisting>
<calloutlist>
<callout arearefs='ex-plausible-cfg-activate'>
<para>
<varname>activate</varname> is used to skip the email verification of the admin-user that's
automatically created by <package>plausible</package>. This is only supported if
<package>postgresql</package> is configured by the module. This is done by default, but
can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
</para>
</callout>
<callout arearefs='ex-plausible-cfg-secretbase'>
<para>
<varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
with <package>openssl</package> as described above.
</para>
</callout>
</calloutlist>
</para>
</section>
</chapter>
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ in
php80 = handleTest ./php { php = pkgs.php80; };
pinnwand = handleTest ./pinnwand.nix {};
plasma5 = handleTest ./plasma5.nix {};
plausible = handleTest ./plausible.nix {};
pleroma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./pleroma.nix {};
plikd = handleTest ./plikd.nix {};
plotinus = handleTest ./plotinus.nix {};
Expand Down
46 changes: 46 additions & 0 deletions nixos/tests/plausible.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ./make-test-python.nix ({ pkgs, lib, ... }: {
name = "plausible";
meta = with lib.maintainers; {
maintainers = [ ma27 ];
};

machine = { pkgs, ... }: {
virtualisation.memorySize = 4096;
services.plausible = {
enable = true;
adminUser = {
email = "admin@example.org";
passwordFile = "${pkgs.writeText "pwd" "foobar"}";
activate = true;
};
server = {
baseUrl = "http://localhost:8000";
secretKeybaseFile = "${pkgs.writeText "dont-try-this-at-home" "nannannannannannannannannannannannannannannannannannannan_batman!"}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

};
};
};

testScript = ''
start_all()
machine.wait_for_unit("plausible.service")
machine.wait_for_open_port(8000)

machine.succeed("curl -f localhost:8000 >&2")

csrf_token = machine.succeed(
"curl -c /tmp/cookies localhost:8000/login | grep '_csrf_token' | sed -E 's,.*value=\"(.*)\".*,\\1,g'"
)

machine.succeed(
f"curl -b /tmp/cookies -f -X POST localhost:8000/login -F email=admin@example.org -F password=foobar -F _csrf_token={csrf_token.strip()} -D headers"
)

# By ensuring that the user is redirected to the dashboard after login, we
# also make sure that the automatic verification of the module works.
machine.succeed(
"[[ $(grep 'location: ' headers | cut -d: -f2- | xargs echo) == /sites* ]]"
)

machine.shutdown()
'';
})
Loading