WIP funkwhale, DO NOT MERGE

This commit is contained in:
hexchen 2021-01-02 10:36:20 +00:00
parent da6dffbcff
commit 05c4fe4823
6 changed files with 668 additions and 0 deletions

View file

@ -15,6 +15,7 @@
# ./k8s.nix
./services/docker.nix
./services/gitlab-runner.nix
./services/funkwhale.nix
];
boot.loader.grub.enable = true;
boot.loader.grub.version = 2;

View file

@ -0,0 +1,55 @@
{ config, lib, pkgs, ... }:
{
containers.funkwhale = {
inherit pkgs;
privateNetwork = true;
hostAddress = "192.168.100.1";
localAddress = "192.168.100.4";
autoStart = true;
config = { config, lib, pkgs, ... }: {
imports = [
../../../../modules
];
services.coredns = {
enable = true;
config = ''
.:53 {
forward . 1.1.1.1
}
'';
};
networking.firewall.enable = false;
services.funkwhale = {
enable = true;
apiIp = "192.168.100.4";
hostname = "funkwhale.hacc.media";
protocol = "https";
defaultFromEmail = "funkwhale@hacc.media";
api.djangoSecretKey = "TwsgANNKid+HZ0HwhR/FgTcxFIW6sZ8s4n7HxV6zPdU=";
};
services.nginx.virtualHosts."funkwhale.hacc.media" = {
enableACME = lib.mkForce false;
forceSSL = lib.mkForce false;
};
};
};
services.nginx.virtualHosts."funkwhale.hacc.media" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://192.168.100.4";
extraConfig = ''
proxy_pass_request_headers on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_buffering off;
'';
};
};
}

View file

@ -8,5 +8,6 @@ let
in {
imports = [
"${immaeNix}/modules/webapps/peertube.nix"
./funkwhale
];
}

View file

@ -0,0 +1,563 @@
{config, lib, pkgs, ...}:
with lib;
let
django-cacheops = with final; with pkgs.python3.pkgs; ( buildPythonPackage rec {
pname = "django-cacheops";
version = "5.1";
src = fetchPypi {
inherit pname version;
sha256 = "sha256-1YUc178whzhKH87PqN3bj1UDDu39b98SciW3W8oPmd0=";
};
propagatedBuildInputs = [ django redis six funcy ];
doCheck = false;
});
pythonEnv = (pkgs.python3.override {
packageOverrides = self: super: rec {
django = self.django_2_2;
};
}).withPackages (ps: [
django-cacheops
ps.aioredis
ps.aiohttp
ps.arrow
ps.autobahn
ps.av
ps.bleach
ps.boto3
ps.celery
ps.channels
ps.channels-redis
ps.click
ps.django
ps.django-allauth
ps.django-auth-ldap
ps.django-oauth-toolkit
ps.django-cleanup
ps.django-cors-headers
ps.django-dynamic-preferences
ps.django_environ
ps.django-filter
ps.django_redis
ps.django-rest-auth
ps.djangorestframework
ps.djangorestframework-jwt
ps.django-storages
ps.django_taggit
ps.django-versatileimagefield
ps.feedparser
ps.gunicorn
ps.kombu
ps.ldap
ps.markdown
ps.mutagen
ps.musicbrainzngs
ps.pillow
ps.pendulum
ps.persisting-theory
ps.psycopg2
ps.pyacoustid
ps.pydub
ps.PyLD
ps.pymemoize
ps.pyopenssl
ps.python_magic
ps.pytz
ps.redis
ps.requests
ps.requests-http-signature
ps.service-identity
ps.unidecode
ps.unicode-slugify
ps.uvicorn
ps.watchdog
]);
cfg = config.services.funkwhale;
databasePassword = if (cfg.database.passwordFile != null)
then builtins.readFile cfg.database.passwordFile
else cfg.database.password;
databaseUrl = if (cfg.database.createLocally && cfg.database.socket != null)
then "postgresql:///${cfg.database.name}?host=${cfg.database.socket}"
else "postgresql://${cfg.database.user}:${databasePassword}@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}";
funkwhaleEnvironment = [
"FUNKWHALE_URL=${cfg.hostname}"
"FUNKWHALE_HOSTNAME=${cfg.hostname}"
"FUNKWHALE_PROTOCOL=${cfg.protocol}"
"EMAIL_CONFIG=${cfg.emailConfig}"
"DEFAULT_FROM_EMAIL=${cfg.defaultFromEmail}"
"REVERSE_PROXY_TYPE=nginx"
"DATABASE_URL=${databaseUrl}"
"CACHE_URL=redis://localhost:${toString config.services.redis.port}/0"
"MEDIA_ROOT=${cfg.api.mediaRoot}"
"STATIC_ROOT=${cfg.api.staticRoot}"
"DJANGO_SECRET_KEY=${cfg.api.djangoSecretKey}"
"RAVEN_ENABLED=${boolToString cfg.enableRaven}"
"RAVEN_DSN=${cfg.ravenDsn}"
"MUSIC_DIRECTORY_PATH=${cfg.musicDirectoryPath}"
"MUSIC_DIRECTORY_SERVE_PATH=${cfg.musicDirectoryPath}"
"FUNKWHALE_FRONTEND_PATH=${cfg.dataDir}/front/dist"
];
funkwhaleEnvFileData = builtins.concatStringsSep "\n" funkwhaleEnvironment;
funkwhaleEnvScriptData = builtins.concatStringsSep " " funkwhaleEnvironment;
funkwhaleEnvFile = pkgs.writeText "funkwhale.env" funkwhaleEnvFileData;
funkwhaleEnv = {
ENV_FILE = "${funkwhaleEnvFile}";
};
in
{
options = {
services.funkwhale = {
enable = mkEnableOption "funkwhale";
user = mkOption {
type = types.str;
default = "funkwhale";
description = "User under which Funkwhale is ran.";
};
group = mkOption {
type = types.str;
default = "funkwhale";
description = "Group under which Funkwhale is ran.";
};
database = {
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host address.";
};
port = mkOption {
type = types.int;
default = 5432;
defaultText = "5432";
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "funkwhale";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "funkwhale";
description = "Database user.";
};
password = mkOption {
type = types.str;
default = "";
description = ''
The password corresponding to <option>database.user</option>.
Warning: this is stored in cleartext in the Nix store!
Use <option>database.passwordFile</option> instead.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/funkwhale-dbpassword";
description = ''
A file containing the password corresponding to
<option>database.user</option>.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default = "/run/postgresql";
defaultText = "/run/postgresql";
example = "/run/postgresql";
description = "Path to the unix socket file to use for authentication for local connections.";
};
createLocally = mkOption {
type = types.bool;
default = true;
description = "Create the database and database user locally.";
};
};
dataDir = mkOption {
type = types.str;
default = "/srv/funkwhale";
description = ''
Where to keep the funkwhale data.
'';
};
apiIp = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Funkwhale API IP.
'';
};
webWorkers = mkOption {
type = types.int;
default = 1;
description = ''
Funkwhale number of web workers.
'';
};
apiPort = mkOption {
type = types.port;
default = 5000;
description = ''
Funkwhale API Port.
'';
};
hostname = mkOption {
type = types.str;
description = ''
The definitive, public domain you will use for your instance.
'';
example = "funkwhale.yourdomain.net";
};
protocol = mkOption {
type = types.enum [ "http" "https" ];
default = "https";
description = ''
Web server protocol.
'';
};
emailConfig = mkOption {
type = types.str;
default = "consolemail://";
description = ''
Configure email sending. By default, it outputs emails to console instead of sending them. See https://docs.funkwhale.audio/configuration.html#email-config for details.
'';
example = "smtp+ssl://user@:password@youremail.host:465";
};
defaultFromEmail = mkOption {
type = types.str;
description = ''
The email address to use to send system emails.
'';
example = "funkwhale@yourdomain.net";
};
api = {
mediaRoot = mkOption {
type = types.str;
default = "/srv/funkwhale/media";
description = ''
Where media files (such as album covers or audio tracks) should be stored on your system ? Ensure this directory actually exists.
'';
};
staticRoot = mkOption {
type = types.str;
default = "/srv/funkwhale/static";
description = ''
Where static files (such as API css or icons) should be compiled on your system ? Ensure this directory actually exists.
'';
};
djangoSecretKey = mkOption {
type = types.str;
description = ''
Django secret key. Generate one using `openssl rand -base64 45` for example.
'';
example = "6VhAWVKlqu/dJSdz6TVgEJn/cbbAidwsFvg9ddOwuPRssEs0OtzAhJxLcLVC";
};
};
musicDirectoryPath = mkOption {
type = types.str;
default = "/srv/funkwhale/music";
description = ''
In-place import settings.
'';
};
enableRaven = mkOption {
type = types.bool;
default = false;
description = ''
Sentry/Raven error reporting (server side).
Enable Raven if you want to help improve funkwhale by
automatically sending error reports to the funkwhale developers Sentry instance.
This will help them detect and correct bugs.
'';
};
ravenDsn = mkOption {
type = types.str;
default = "https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5";
description = ''
Sentry/Raven DSN.
The default is the Funkwhale developers instance DSN.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.database.passwordFile != null || cfg.database.password != "" || cfg.database.socket != null;
message = "one of services.funkwhale.database.socket, services.funkwhale.database.passwordFile, or services.funkwhale.database.password must be set";
}
{ assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
message = "services.funkwhale.database.user must be set to ${cfg.user} if services.funkwhale.database.createLocally is set true";
}
{ assertion = cfg.database.createLocally -> cfg.database.socket != null;
message = "services.funkwhale.database.socket must be set if services.funkwhale.database.createLocally is set to true";
}
{ assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
message = "services.funkwhale.database.host must be set to localhost if services.funkwhale.database.createLocally is set to true";
}
];
users.users.funkwhale = mkIf (cfg.user == "funkwhale")
{ name = "funkwhale";
group = cfg.group;
};
users.groups.funkwhale = mkIf (cfg.group == "funkwhale") { name = "funkwhale"; };
services.postgresql = mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{ name = cfg.database.user;
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
}
];
};
services.redis.enable = true;
services.nginx = {
enable = true;
appendHttpConfig = ''
upstream funkwhale-api {
server ${cfg.apiIp}:${toString cfg.apiPort};
}
'';
virtualHosts =
let proxyConfig = ''
# global proxy conf
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Port $server_port;
proxy_redirect off;
# websocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
'';
withSSL = cfg.protocol == "https";
in {
"${cfg.hostname}" = {
enableACME = withSSL;
forceSSL = withSSL;
root = "${pkgs.funkwhale}/front";
# gzip config is nixos nginx recommendedGzipSettings with gzip_types from funkwhale doc (https://docs.funkwhale.audio/changelog.html#id5)
extraConfig = ''
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
add_header Referrer-Policy "strict-origin-when-cross-origin";
gzip on;
gzip_disable "msie6";
gzip_proxied any;
gzip_comp_level 5;
gzip_types
application/javascript
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
gzip_vary on;
'';
locations = {
"/" = {
extraConfig = proxyConfig;
proxyPass = "http://funkwhale-api/";
};
"/front/" = {
alias = "${pkgs.funkwhale}/front/";
extraConfig = ''
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
add_header Referrer-Policy "strict-origin-when-cross-origin";
expires 30d;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
'';
};
"= /front/embed.html" = {
alias = "${pkgs.funkwhale}/front/embed.html";
extraConfig = ''
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header X-Frame-Options "ALLOW";
expires 30d;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
'';
};
"/federation/" = {
extraConfig = proxyConfig;
proxyPass = "http://funkwhale-api/federation/";
};
"/rest/" = {
extraConfig = proxyConfig;
proxyPass = "http://funkwhale-api/api/subsonic/rest/";
};
"/.well-known/" = {
extraConfig = proxyConfig;
proxyPass = "http://funkwhale-api/.well-known/";
};
"/media/".alias = "${cfg.api.mediaRoot}/";
"/_protected/media/" = {
extraConfig = ''
internal;
'';
alias = "${cfg.api.mediaRoot}/";
};
"/_protected/music/" = {
extraConfig = ''
internal;
'';
alias = "${cfg.musicDirectoryPath}/";
};
"/staticfiles/".alias = "${cfg.api.staticRoot}/";
};
};
};
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
"d ${cfg.api.mediaRoot} 0755 ${cfg.user} ${cfg.group} - -"
"d ${cfg.api.staticRoot} 0755 ${cfg.user} ${cfg.group} - -"
"d ${cfg.musicDirectoryPath} 0755 ${cfg.user} ${cfg.group} - -"
];
systemd.targets.funkwhale = {
description = "Funkwhale";
wants = ["funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service"];
};
systemd.services =
let serviceConfig = {
User = "${cfg.user}";
WorkingDirectory = "${pkgs.funkwhale}";
EnvironmentFile = "${funkwhaleEnvFile}";
};
in {
funkwhale-psql-init = mkIf cfg.database.createLocally {
description = "Funkwhale database preparation";
after = [ "redis.service" "postgresql.service" ];
wantedBy = [ "funkwhale-init.service" ];
before = [ "funkwhale-init.service" ];
serviceConfig = {
User = "postgres";
ExecStart = '' ${config.services.postgresql.package}/bin/psql -d ${cfg.database.name} -c 'CREATE EXTENSION IF NOT EXISTS "unaccent";CREATE EXTENSION IF NOT EXISTS "citext";' '';
};
};
funkwhale-init = {
description = "Funkwhale initialization";
wantedBy = [ "funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service" ];
before = [ "funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service" ];
environment = funkwhaleEnv;
serviceConfig = {
User = "${cfg.user}";
Group = "${cfg.group}";
};
script = ''
${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py migrate
${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py collectstatic --no-input
if ! test -e ${cfg.dataDir}/createSuperUser.sh; then
echo "#!/bin/sh
${funkwhaleEnvScriptData} ${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py createsuperuser" > ${cfg.dataDir}/createSuperUser.sh
chmod u+x ${cfg.dataDir}/createSuperUser.sh
chown -R ${cfg.user}.${cfg.group} ${cfg.dataDir}
fi
if ! test -e ${cfg.dataDir}/config; then
mkdir -p ${cfg.dataDir}/config
ln -s ${funkwhaleEnvFile} ${cfg.dataDir}/config/.env
ln -s ${funkwhaleEnvFile} ${cfg.dataDir}/.env
fi
'';
};
funkwhale-server = {
description = "Funkwhale application server";
partOf = [ "funkwhale.target" ];
serviceConfig = serviceConfig // {
ExecStart = "${pythonEnv}/bin/gunicorn config.asgi:application -w ${toString cfg.webWorkers} -k uvicorn.workers.UvicornWorker -b ${cfg.apiIp}:${toString cfg.apiPort}";
};
environment = funkwhaleEnv;
wantedBy = [ "multi-user.target" ];
};
funkwhale-worker = {
description = "Funkwhale celery worker";
partOf = [ "funkwhale.target" ];
serviceConfig = serviceConfig // {
RuntimeDirectory = "funkwhaleworker";
ExecStart = "${pythonEnv}/bin/celery -A funkwhale_api.taskapp worker -l INFO";
};
environment = funkwhaleEnv;
wantedBy = [ "multi-user.target" ];
};
funkwhale-beat = {
description = "Funkwhale celery beat process";
partOf = [ "funkwhale.target" ];
serviceConfig = serviceConfig // {
RuntimeDirectory = "funkwhalebeat";
ExecStart = '' ${pythonEnv}/bin/celery -A funkwhale_api.taskapp beat -l INFO --schedule="/run/funkwhalebeat/celerybeat-schedule.db" --pidfile="/run/funkwhalebeat/celerybeat.pid" '';
};
environment = funkwhaleEnv;
wantedBy = [ "multi-user.target" ];
};
};
};
meta = {
maintainers = with lib.maintainers; [ mmai ];
};
}

View file

@ -19,6 +19,7 @@ let
wasi = import wasiSrc { inherit wasiSrc; pkgs = pkgs // newpkgs; };
peertube = callPackage ./peertube { mylibs = import "${immaeNix}/lib" { inherit pkgs; }; };
alps = callPackage ./alps {};
funkwhale = callPackage ./funkwhale {};
};
in newpkgs

View file

@ -0,0 +1,47 @@
{ stdenv, fetchurl, unzip }:
# Look for the correct urls for build_front and build_api artifacts on the tags page of the project : https://dev.funkwhale.audio/funkwhale/funkwhale/pipelines?scope=tags
# Attention : do not use the url "https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/${release}/download?job=" : it is not guaranteed to be stable
let
release = "1.0.1";
srcs = {
api = fetchurl {
url = https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/56793/artifacts/download;
name = "api.zip";
sha256 = "0p21r8kbn7sr33chp7404fi9pm4yz6qhfz4z7gxf3vamg9fbsbsc";
};
frontend = fetchurl {
url = https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/56790/artifacts/download;
name = "frontend.zip";
sha256 = "0hz4d59sva6zi5q53wj3f6yaw5didcl9z148s6rsy2m6gyr8566d";
};
};
in stdenv.mkDerivation {
name = "funkwhale";
version = "${release}";
src = srcs.api;
nativeBuildInputs = [ unzip ];
postPatch = ''
substituteInPlace requirements/base.txt \
--replace "django-cleanup==3.2.0" django-cleanup
'';
installPhase = ''
mkdir $out
cp -R ./* $out
unzip ${srcs.frontend} -d $out
mv $out/front/ $out/front_tmp
mv $out/front_tmp/dist $out/front
rmdir $out/front_tmp
'';
meta = with stdenv.lib; {
description = "A modern, convivial and free music server";
homepage = https://funkwhale.audio/;
license = licenses.agpl3;
platforms = platforms.linux;
maintainers = with maintainers; [ mmai ];
};
}