diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml index 5208671e4dab..3dc4a494588b 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml @@ -2122,6 +2122,13 @@ sudo mkdir /var/lib/redis-peertube sudo cp /var/lib/redis/dump.rdb /var/lib/redis-peertube/dump.rdb + + + Added the keter NixOS module. Keter reverse + proxies requests to your loaded application based on virtual + hostnames. + + If you are using Wayland you can choose to use the Ozone diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md index faf941f56996..f1f544035002 100644 --- a/nixos/doc/manual/release-notes/rl-2205.section.md +++ b/nixos/doc/manual/release-notes/rl-2205.section.md @@ -776,6 +776,7 @@ In addition to numerous new and upgraded packages, this release has the followin sudo mkdir /var/lib/redis-peertube sudo cp /var/lib/redis/dump.rdb /var/lib/redis-peertube/dump.rdb ``` +- Added the `keter` NixOS module. Keter reverse proxies requests to your loaded application based on virtual hostnames. - If you are using Wayland you can choose to use the Ozone Wayland support in Chrome and several Electron apps by setting the environment variable diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index d67602a26761..08834022cf46 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1118,6 +1118,7 @@ ./services/web-servers/pomerium.nix ./services/web-servers/unit/default.nix ./services/web-servers/tomcat.nix + ./services/web-servers/keter ./services/web-servers/traefik.nix ./services/web-servers/trafficserver/default.nix ./services/web-servers/ttyd.nix diff --git a/nixos/modules/services/web-servers/keter/bundle.nix b/nixos/modules/services/web-servers/keter/bundle.nix new file mode 100644 index 000000000000..32b08c3be206 --- /dev/null +++ b/nixos/modules/services/web-servers/keter/bundle.nix @@ -0,0 +1,40 @@ +/* This makes a keter bundle as described on the github page: + https://github.com/snoyberg/keter#bundling-your-app-for-keter +*/ +{ keterDomain +, keterExecutable +, gnutar +, writeTextFile +, lib +, stdenv +, ... +}: + +let + str.stanzas = [{ + # we just use nix as an absolute path so we're not bundling any binaries + type = "webapp"; + /* Note that we're not actually putting the executable in the bundle, + we already can use the nix store for copying, so we just + symlink to the app. */ + exec = keterExecutable; + host = keterDomain; + }]; + configFile = writeTextFile { + name = "keter.yml"; + text = (lib.generators.toYAML { } str); + }; + +in +stdenv.mkDerivation { + name = "keter-bundle"; + buildCommand = '' + mkdir -p config + cp ${configFile} config/keter.yaml + + echo 'create a gzipped tarball' + mkdir -p $out + tar -zcvf $out/bundle.tar.gz.keter ./. + ''; + buildInputs = [ gnutar ]; +} diff --git a/nixos/modules/services/web-servers/keter/default.nix b/nixos/modules/services/web-servers/keter/default.nix new file mode 100644 index 000000000000..83e221add37e --- /dev/null +++ b/nixos/modules/services/web-servers/keter/default.nix @@ -0,0 +1,162 @@ +{ config, pkgs, lib, ... }: +let + cfg = config.services.keter; +in +{ + meta = { + maintainers = with lib.maintainers; [ jappie ]; + }; + + options.services.keter = { + enable = lib.mkEnableOption ''keter, a web app deployment manager. +Note that this module only support loading of webapps: +Keep an old app running and swap the ports when the new one is booted. +''; + + keterRoot = lib.mkOption { + type = lib.types.str; + default = "/var/lib/keter"; + description = "Mutable state folder for keter"; + }; + + keterPackage = lib.mkOption { + type = lib.types.package; + default = pkgs.haskellPackages.keter; + defaultText = lib.literalExpression "pkgs.haskellPackages.keter"; + description = "The keter package to be used"; + }; + + globalKeterConfig = lib.mkOption { + type = lib.types.attrs; + default = { + ip-from-header = true; + listeners = [{ + host = "*4"; + port = 6981; + }]; + }; + # You want that ip-from-header in the nginx setup case + # so it's not set to 127.0.0.1. + # using a port above 1024 allows you to avoid needing CAP_NET_BIND_SERVICE + defaultText = lib.literalExpression '' + { + ip-from-header = true; + listeners = [{ + host = "*4"; + port = 6981; + }]; + } + ''; + description = "Global config for keter"; + }; + + bundle = { + appName = lib.mkOption { + type = lib.types.str; + default = "myapp"; + description = "The name keter assigns to this bundle"; + }; + + executable = lib.mkOption { + type = lib.types.path; + description = "The executable to be run"; + }; + + domain = lib.mkOption { + type = lib.types.str; + default = "example.com"; + description = "The domain keter will bind to"; + }; + + publicScript = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Allows loading of public environment variables, + these are emitted to the log so it shouldn't contain secrets. + ''; + example = "ADMIN_EMAIL=hi@example.com"; + }; + + secretScript = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Allows loading of private environment variables"; + example = "MY_AWS_KEY=$(cat /run/keys/AWS_ACCESS_KEY_ID)"; + }; + }; + + }; + + config = lib.mkIf cfg.enable ( + let + incoming = "${cfg.keterRoot}/incoming"; + + + globalKeterConfigFile = pkgs.writeTextFile { + name = "keter-config.yml"; + text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.keterRoot; })); + }; + + # If things are expected to change often, put it in the bundle! + bundle = pkgs.callPackage ./bundle.nix + (cfg.bundle // { keterExecutable = executable; keterDomain = cfg.bundle.domain; }); + + # This indirection is required to ensure the nix path + # gets copied over to the target machine in remote deployments. + # Furthermore, it's important that we use exec to + # run the binary otherwise we get process leakage due to this + # being executed on every change. + executable = pkgs.writeShellScript "bundle-wrapper" '' + set -e + ${cfg.bundle.secretScript} + set -xe + ${cfg.bundle.publicScript} + exec ${cfg.bundle.executable} + ''; + + in + { + systemd.services.keter = { + description = "keter app loader"; + script = '' + set -xe + mkdir -p ${incoming} + { tail -F ${cfg.keterRoot}/log/keter/current.log -n 0 & ${cfg.keterPackage}/bin/keter ${globalKeterConfigFile}; } + ''; + wantedBy = [ "multi-user.target" "nginx.service" ]; + + serviceConfig = { + Restart = "always"; + RestartSec = "10s"; + }; + + after = [ + "network.target" + "local-fs.target" + "postgresql.service" + ]; + }; + + # On deploy this will load our app, by moving it into the incoming dir + # If the bundle content changes, this will run again. + # Because the bundle content contains the nix path to the exectuable, + # we inherit nix based cache busting. + systemd.services.load-keter-bundle = { + description = "load keter bundle into incoming folder"; + after = [ "keter.service" ]; + wantedBy = [ "multi-user.target" ]; + # we can't override keter bundles because it'll stop the previous app + # https://github.com/snoyberg/keter#deploying + script = '' + set -xe + cp ${bundle}/bundle.tar.gz.keter ${incoming}/${cfg.bundle.appName}.keter + ''; + path = [ + executable + cfg.bundle.executable + ]; # this is a hack to get the executable copied over to the machine. + }; + } + ); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1064d62da930..e6fd508100dd 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -264,6 +264,7 @@ in { kerberos = handleTest ./kerberos/default.nix {}; kernel-generic = handleTest ./kernel-generic.nix {}; kernel-latest-ath-user-regd = handleTest ./kernel-latest-ath-user-regd.nix {}; + keter = handleTest ./keter.nix {}; kexec = handleTest ./kexec.nix {}; keycloak = discoverTests (import ./keycloak.nix); keymap = handleTest ./keymap.nix {}; diff --git a/nixos/tests/keter.nix b/nixos/tests/keter.nix new file mode 100644 index 000000000000..0bfb96e1c324 --- /dev/null +++ b/nixos/tests/keter.nix @@ -0,0 +1,42 @@ +import ./make-test-python.nix ({ pkgs, ... }: +let + port = 81; +in +{ + name = "keter"; + meta = with pkgs.lib.maintainers; { + maintainers = [ jappie ]; + }; + + + nodes.machine = { config, pkgs, ... }: { + services.keter = { + enable = true; + + globalKeterConfig = { + listeners = [{ + host = "*4"; + inherit port; + }]; + }; + bundle = { + appName = "test-bundle"; + domain = "localhost"; + executable = pkgs.writeShellScript "run" '' + ${pkgs.python3}/bin/python -m http.server $PORT + ''; + }; + }; + }; + + testScript = + '' + machine.wait_for_unit("keter.service") + + machine.wait_for_open_port(${toString port}) + machine.wait_for_console_text("Activating app test-bundle with hosts: localhost") + + + machine.succeed("curl --fail http://localhost:${toString port}/") + ''; +})