From 86c56bb9a40425e4567c3648d427ad7e6be01a65 Mon Sep 17 00:00:00 2001 From: stuebinm Date: Fri, 5 Feb 2021 13:31:42 +0100 Subject: Functional module, extracted from fediventure repo (just to make it easier to hack, and remove fediventure-specific deployment logic) --- instance-options.nix | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ workadventure-nix.nix | 33 ++++++++++++ workadventure.nix | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 314 insertions(+) create mode 100644 instance-options.nix create mode 100644 workadventure-nix.nix create mode 100644 workadventure.nix diff --git a/instance-options.nix b/instance-options.nix new file mode 100644 index 0000000..6a1d2dc --- /dev/null +++ b/instance-options.nix @@ -0,0 +1,143 @@ +# Configuration options specific to a single workadventure instance. + +{ lib, config, ... }: + +with lib; +let workadventure = import ./workadventure-nix.nix { inherit lib; }; +in +{ + options = rec { + backend = { + httpPort = mkOption { + default = 8081; + type = types.ints.u16; + description = "The TCP port the backend will bind to for http"; + }; + + grpcPort = mkOption { + default = 50051; + type = types.ints.u16; + description = "The TCP port the backend will bind to for grpc"; + }; + + package = mkOption { + default = workadventure.back; + defaultText = "third_party.workadventure-nix.back"; + type = types.package; + description = "Backend package to use"; + }; + }; + + pusher = { + port = mkOption { + default = 8080; + type = types.ints.u16; + description = "The TCP port the pusher will bind to"; + }; + + package = mkOption { + default = workadventure.pusher; + defaultText = "third_party.workadventure-nix.pusher"; + type = types.package; + description = "Pusher package to use"; + }; + }; + + frontend = { + package = mkOption { + default = workadventure.front; + defaultText = "third_party.workadventure-nix.front"; + type = types.package; + description = "Front package to use"; + }; + + defaultMap = mkOption { + default = null; + defaultText = "not set"; + type = types.nullOr types.str; + description = "The url to the default map, which will be loaded if none is given in the url. Must be a reachable url relative to the public map url defined in `maps.url`."; + }; + + urls = { + api = mkOption { + default = "/pusher"; + type = types.str; + description = "The base url for the api, from the browser's point of view"; + }; + + uploader = mkOption { + default = "/uploader"; + type = types.str; + description = "The base url for the uploader, from the browser's point of view"; + }; + + admin = mkOption { + default = "/admin"; + type = types.str; + description = "The base url for the admin, from the browser's point of view"; + }; + + maps = mkOption { + default = "/maps"; + type = types.str; + description = "The base url for serving maps, from the browser's point of view"; + }; + }; + }; + + maps = { + path = mkOption { + default = workadventure.maps.outPath + "/workadventuremaps/"; + defaultText = "third_party.workadventure-nix.maps"; + type = types.path; + description = "Maps package to use"; + }; + }; + + nginx = { + default = mkOption { + default = false; + type = types.bool; + description = "Whether this instance will be the default one served by nginx"; + }; + + domain = mkOption { + default = null; + type = types.nullOr types.str; + description = "The domain name to serve workadenture services under. Mutually exclusive with domains.X"; + }; + + serveDefaultMaps = mkOption { + default = true; + type = types.bool; + description = "Whether to serve the maps provided by workadventure"; + }; + + domains = { + back = mkOption { + default = null; + type = types.nullOr types.str; + description = "The domain name to serve the backend under"; + }; + + pusher = mkOption { + default = null; + type = types.nullOr types.str; + description = "The domain name to serve the pusher under"; + }; + + maps = mkOption { + default = null; + type = types.nullOr types.str; + description = "The domain name to serve the maps under"; + }; + + front = mkOption { + default = null; + type = types.nullOr types.str; + description = "The domain name to serve the front under"; + }; + }; + }; + }; +} diff --git a/workadventure-nix.nix b/workadventure-nix.nix new file mode 100644 index 0000000..4c515cb --- /dev/null +++ b/workadventure-nix.nix @@ -0,0 +1,33 @@ +# WorkAdventure packaging effort by SuperSandro2000, not yet upstreamed into nixpkgs. + +{ lib, ... }: + + +let + pkgs = import {}; + + src = pkgs.fetchgit { + url = "https://gitlab.infra4future.de/stuebinm/workadventure-nix"; + rev = "71ed23142c5ab6db05263b6e5c52f8fab1d84425"; + sha256 = "0g20rzaxp5md26hc3dig4hhp296bd45n1zi3b67a8q0l290ydn2g"; + }; + + # Use a fixed-point operator to build a nixpkgs-like structure that contains all + # workadventure derivation. + wapkgs = lib.fix (self: let + callPackage = lib.callPackageWith (pkgs // self); + in { + workadventure-pusher = callPackage "${src}/pusher" {}; + workadventure-back = callPackage "${src}/back" {}; + workadventure-front = callPackage "${src}/front" {}; + workadventure-messages = callPackage "${src}/messages" {}; + workadventure-maps = callPackage "${src}/maps" {}; + }); + +# Build public attrset of all accessible components. +in rec { + pusher = wapkgs.workadventure-pusher; + back = wapkgs.workadventure-back; + front = wapkgs.workadventure-front; + maps = wapkgs.workadventure-maps; +} diff --git a/workadventure.nix b/workadventure.nix new file mode 100644 index 0000000..02f9803 --- /dev/null +++ b/workadventure.nix @@ -0,0 +1,138 @@ +# Workadventure NixOS module. Used to deploy fediventure-compatible instances. + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.workadventure; + + servicesBack = mapAttrs' (instanceName: instanceConfig: { + name = "wa-back-${instanceName}"; + value = { + description = "WorkAdventure backend ${instanceName}"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + # Hack to get node-grpc-precompiled to work on NixOS by adding getconf to + # $PATH. + # + # It uses node-pre-gyp which attempts to select the right native module + # via npmjs.com/package/detect-libc, which says 'yep, it's glibc' as long + # as `getconf GNU_LIBC_VERSION` returns something sensible. This happens + # during the build process (as stdenv.mkDerivation has enough of a glibc + # dev env to make it work) but doesn't happen on production deployments + # in which the environment is much more limited. This is regardless of + # actual glibc ABI presence wrt. to /nix/store vs. /usr/lib64 paths. + # + # This should be fixed in workadventure-nix. + path = [ + pkgs.getconf + ]; + environment = { + HTTP_PORT = toString instanceConfig.backend.httpPort; + GRPC_PORT = toString instanceConfig.backend.grpcPort; + }; + serviceConfig = { + User = "workadventure-backend"; + Group = "workadventure-backend"; + DynamicUser = true; # Note: this implies a lot of other security features. + ExecStart = "${instanceConfig.backend.package}/bin/workadventureback"; + Restart = "always"; + RestartSec = "10s"; + }; + }; + }) cfg.instances; + + servicesPusher = mapAttrs' (instanceName: instanceConfig: { + name = "wa-pusher-${instanceName}"; + value = { + description = "WorkAdventure pusher ${instanceName}"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + path = [ + pkgs.getconf + ]; + environment = { + PUSHER_HTTP_PORT = toString instanceConfig.pusher.port; + API_URL = "localhost:${toString instanceConfig.backend.grpcPort}"; + }; + serviceConfig = { + User = "workadventure-pusher"; + Group = "workadventure-pusher"; + DynamicUser = true; + ExecStart = "${instanceConfig.pusher.package}/bin/workadventurepusher"; + Restart = "always"; + RestartSec = "10s"; + }; + }; + }) cfg.instances; + + frontPackage = mapAttrs (instanceName: instanceConfig: + instanceConfig.frontend.package.override { + environment = { + API_URL = instanceConfig.frontend.urls.api; + UPLOADER_URL = instanceConfig.frontend.urls.uploader; + ADMIN_URL = instanceConfig.frontend.urls.admin; + MAPS_URL = instanceConfig.frontend.urls.maps; + } // (if instanceConfig.frontend.defaultMap == null then {} else { DEFAULT_MAP_URL = instanceConfig.frontend.defaultMap; }); + } + ) cfg.instances; + + virtualHosts = mapAttrs (instanceName: instanceConfig: + if instanceConfig.nginx.domain != null then { + default = instanceConfig.nginx.default; + serverName = instanceConfig.nginx.domain; + root = frontPackage.${instanceName} + "/dist"; + locations = { + "/_/" = { + tryFiles = "/index.html =404"; + }; + + "/pusher/" = { + proxyPass = "http://localhost:${toString instanceConfig.pusher.port}/"; + }; + + "/maps/" = mkIf instanceConfig.nginx.serveDefaultMaps { + alias = instanceConfig.maps.path; + }; + }; + } else + # TODO: Configuration with separate domains is unsupported for now. + # Not sure if there's any interest in that anyway. + builtins.throw "Configurations with separate domains are not supported yet" + ) cfg.instances; +in { + options = { + services.workadventure = rec { + instances = mkOption { + type = types.attrsOf (types.submodule (import ./instance-options.nix { + inherit config lib; + })); + default = {}; + description = "Declarative WorkAdventure instance config"; + }; + nginx = { + enable = mkOption { + default = true; + type = types.bool; + description = "Whether to enable nginx and configure it to serve the instances"; + }; + }; + }; + }; + + config = { + assertions = mapAttrsToList (name: instance: { + assertion = !cfg.nginx.enable + || (instance.nginx.domain != null && all (d: d == null) (attrValues instance.nginx.domains)) + || (instance.nginx.domain == null && all (d: d != null) (attrValues instance.nginx.domains)); + message = "In instance ${name}, you have to either define nginx.domain or all attributes of nginx.domains"; + }) cfg.instances; + systemd.services = servicesBack // servicesPusher; + services.nginx = mkIf cfg.nginx.enable { + inherit virtualHosts; + enable = mkDefault true; + }; + }; +} -- cgit v1.2.3