diff --git a/default.nix b/default.nix index 2cccff2..6466507 100644 --- a/default.nix +++ b/default.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/demo/default.nix b/demo/default.nix index 046aaa1..e9ca54b 100644 --- a/demo/default.nix +++ b/demo/default.nix @@ -14,7 +14,7 @@ users.users.bob = { isNormalUser = true; password = "beta"; - uid = 1001; + uid = 1001; packages = [ pkgs.intake ]; }; @@ -26,11 +26,13 @@ }; # Expose the vm's intake revproxy at host port 5234 - virtualisation.forwardPorts = [{ - from = "host"; - host.port = 5234; - guest.port = 8080; - }]; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 5234; + guest.port = 8080; + } + ]; # Mount the demo content for both users nixos-shell.mounts = { @@ -47,20 +49,20 @@ # Create an activation script that copies and chowns the demo content # chmod 777 because the users may not exist when the activation script runs - system.activationScripts = - let - userSetup = name: uid: '' - ${pkgs.coreutils}/bin/mkdir -p /home/${name}/.local/share/intake - ${pkgs.coreutils}/bin/cp -r /mnt/${name}/* /home/${name}/.local/share/intake/ - ${pkgs.coreutils}/bin/chown -R ${uid} /home/${name} - ${pkgs.findutils}/bin/find /home/${name} -type d -exec ${pkgs.coreutils}/bin/chmod 755 {} \; - ${pkgs.findutils}/bin/find /home/${name} -type f -exec ${pkgs.coreutils}/bin/chmod 644 {} \; - ''; - in - { - aliceSetup = userSetup "alice" "1000"; - bobSetup = userSetup "bob" "1001"; - }; + system.activationScripts = + let + userSetup = name: uid: '' + ${pkgs.coreutils}/bin/mkdir -p /home/${name}/.local/share/intake + ${pkgs.coreutils}/bin/cp -r /mnt/${name}/* /home/${name}/.local/share/intake/ + ${pkgs.coreutils}/bin/chown -R ${uid} /home/${name} + ${pkgs.findutils}/bin/find /home/${name} -type d -exec ${pkgs.coreutils}/bin/chmod 755 {} \; + ${pkgs.findutils}/bin/find /home/${name} -type f -exec ${pkgs.coreutils}/bin/chmod 644 {} \; + ''; + in + { + aliceSetup = userSetup "alice" "1000"; + bobSetup = userSetup "bob" "1001"; + }; # Put the demo sources on the global PATH environment.variables.PATH = "/mnt/sources"; diff --git a/flake.nix b/flake.nix index fc479a7..98ea838 100644 --- a/flake.nix +++ b/flake.nix @@ -13,63 +13,91 @@ nixos-shell.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, flake-compat, nixos-shell }: - let - inherit (nixpkgs.lib) makeOverridable nixosSystem; - system = "x86_64-linux"; - in { - formatter.${system} = nixpkgs.legacyPackages.${system}.nixfmt-rfc-style; + outputs = + { + self, + nixpkgs, + flake-compat, + nixos-shell, + }: + let + inherit (nixpkgs.lib) makeOverridable nixosSystem; + system = "x86_64-linux"; + in + { + formatter.${system} = nixpkgs.legacyPackages.${system}.nixfmt-rfc-style; - packages.${system} = let - pkgs = (import nixpkgs { + packages.${system} = + let + pkgs = ( + import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + } + ); + in + { + default = self.packages.${system}.intake; + inherit (pkgs) intake; + }; + + devShells.${system} = { + default = + let + pkgs = nixpkgs.legacyPackages.${system}; + pythonEnv = pkgs.python3.withPackages ( + pypkgs: with pypkgs; [ + flask + black + pytest + ] + ); + in + pkgs.mkShell { + packages = [ + pythonEnv + pkgs.nixos-shell + # We only take this dependency for htpasswd, which is a little unfortunate + pkgs.apacheHttpd + ]; + shellHook = '' + PS1="(develop) $PS1" + ''; + }; + }; + + overlays.default = final: prev: { + intake = final.python3Packages.buildPythonPackage { + name = "intake"; + src = builtins.path { + path = ./.; + name = "intake"; + }; + format = "pyproject"; + propagatedBuildInputs = with final.python3Packages; [ + flask + setuptools + ]; + }; + }; + + templates.source = { + path = builtins.path { + path = ./template; + name = "source"; + }; + description = "A basic intake source config"; + }; + + nixosModules.default = import ./module.nix self; + + nixosConfigurations."demo" = makeOverridable nixosSystem { inherit system; - overlays = [ self.overlays.default ]; - }); - in { - default = self.packages.${system}.intake; - inherit (pkgs) intake; - }; - - devShells.${system} = { - default = let - pkgs = nixpkgs.legacyPackages.${system}; - pythonEnv = pkgs.python3.withPackages (pypkgs: with pypkgs; [ flask black pytest ]); - in pkgs.mkShell { - packages = [ - pythonEnv - pkgs.nixos-shell - # We only take this dependency for htpasswd, which is a little unfortunate - pkgs.apacheHttpd + modules = [ + nixos-shell.nixosModules.nixos-shell + self.nixosModules.default + ./demo ]; - shellHook = '' - PS1="(develop) $PS1" - ''; }; }; - - overlays.default = final: prev: { - intake = final.python3Packages.buildPythonPackage { - name = "intake"; - src = builtins.path { path = ./.; name = "intake"; }; - format = "pyproject"; - propagatedBuildInputs = with final.python3Packages; [ flask setuptools ]; - }; - }; - - templates.source = { - path = builtins.path { path = ./template; name = "source"; }; - description = "A basic intake source config"; - }; - - nixosModules.default = import ./module.nix self; - - nixosConfigurations."demo" = makeOverridable nixosSystem { - inherit system; - modules = [ - nixos-shell.nixosModules.nixos-shell - self.nixosModules.default - ./demo - ]; - }; - }; } diff --git a/module.nix b/module.nix index 10c68f9..43e985e 100644 --- a/module.nix +++ b/module.nix @@ -1,153 +1,179 @@ -flake: { config, lib, pkgs, ... }: +flake: +{ + config, + lib, + pkgs, + ... +}: let - inherit (lib) filterAttrs foldl imap1 mapAttrsToList mkEnableOption mkIf mkMerge mkOption mkPackageOption types; + inherit (lib) + filterAttrs + foldl + imap1 + mapAttrsToList + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + types + ; intakeCfg = config.services.intake; -in { +in +{ options = { services.intake = { listen.addr = mkOption { type = types.str; default = "0.0.0.0"; - description = "The listen address for the entry point to intake services. This endpoint will redirect to a " - "local port based on the request's HTTP Basic Auth credentials."; + description = "The listen address for the entry point to intake services. This endpoint will redirect to a local port based on the request's HTTP Basic Auth credentials."; }; listen.port = mkOption { type = types.port; default = 80; - description = "The listen port for the entry point to intake services. This endpoint will redirect to a local " - "port based on the request's HTTP Basic Auth credentials."; + description = "The listen port for the entry point to intake services. This endpoint will redirect to a local port based on the request's HTTP Basic Auth credentials."; }; - package = mkPackageOption pkgs "intake" {}; + package = mkPackageOption pkgs "intake" { }; internalPortStart = mkOption { type = types.port; default = 24130; - description = "The first port to use for internal service endpoints. A number of ports will be continguously " - "allocated equal to the number of users with enabled intake services."; + description = "The first port to use for internal service endpoints. A number of ports will be continguously allocated equal to the number of users with enabled intake services."; }; extraPackages = mkOption { type = types.listOf types.package; - default = []; + default = [ ]; description = "Extra packages available to all enabled users and their intake services."; }; users = mkOption { description = "User intake service definitions."; - default = {}; - type = types.attrsOf (types.submodule { - options = { - enable = mkEnableOption "intake, a personal feed aggregator."; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + enable = mkEnableOption "intake, a personal feed aggregator."; - extraPackages = mkOption { - type = types.listOf types.package; - default = []; - description = "Extra packages available to this user and their intake service."; + extraPackages = mkOption { + type = types.listOf types.package; + default = [ ]; + description = "Extra packages available to this user and their intake service."; + }; }; - }; - }); + } + ); }; }; }; config = - let - # Define the intake package and a python environment to run it from - intake = intakeCfg.package; - pythonEnv = pkgs.python3.withPackages (pypkgs: [ intake ]); - - # Assign each user an internal port for their personal intake instance - enabledUsers = filterAttrs (userName: userCfg: userCfg.enable) intakeCfg.users; - enabledUserNames = mapAttrsToList (userName: userCfg: userName) enabledUsers; - userPortList = imap1 (i: userName: { ${userName} = i + intakeCfg.internalPortStart; }) enabledUserNames; - userPort = foldl (acc: val: acc // val) {} userPortList; - - # To avoid polluting PATH with httpd programs, define an htpasswd wrapper - htpasswdWrapper = pkgs.writeShellScriptBin "htpasswd" '' - ${pkgs.apacheHttpd}/bin/htpasswd $@ - ''; - - # File locations - intakeDir = "/etc/intake"; - intakePwd = "${intakeDir}/htpasswd"; - in { - # Apply the overlay so intake is included in pkgs. - nixpkgs.overlays = [ flake.overlays.default ]; - - # Define a user group for access to the htpasswd file. nginx needs to be able to read it. - users.groups.intake.members = mkIf (enabledUsers != {}) (enabledUserNames ++ [ "nginx" ]); - - # Define an activation script that ensures that the htpasswd file exists. - system.activationScripts.etc-intake = '' - if [ ! -e ${intakeDir} ]; then - ${pkgs.coreutils}/bin/mkdir -p ${intakeDir}; - fi - ${pkgs.coreutils}/bin/chown root:root ${intakeDir} - ${pkgs.coreutils}/bin/chmod 755 ${intakeDir} - if [ ! -e ${intakePwd} ]; then - ${pkgs.coreutils}/bin/touch ${intakePwd} - fi - ${pkgs.coreutils}/bin/chown root:intake ${intakePwd} - ${pkgs.coreutils}/bin/chmod 660 ${intakePwd} - ''; - - # Give every intake user the htpasswd wrapper, the shared packages, and the user-specific packages. - users.users = let - addPackagesToUser = userName: { - ${userName}.packages = - [ htpasswdWrapper intake ] - ++ intakeCfg.extraPackages - ++ intakeCfg.users.${userName}.extraPackages; - }; - in mkMerge (map addPackagesToUser enabledUserNames); + # Define the intake package and a python environment to run it from + intake = intakeCfg.package; + pythonEnv = pkgs.python3.withPackages (pypkgs: [ intake ]); - # Enable cron - services.cron.enable = true; + # Assign each user an internal port for their personal intake instance + enabledUsers = filterAttrs (userName: userCfg: userCfg.enable) intakeCfg.users; + enabledUserNames = mapAttrsToList (userName: userCfg: userName) enabledUsers; + userPortList = imap1 (i: userName: { + ${userName} = i + intakeCfg.internalPortStart; + }) enabledUserNames; + userPort = foldl (acc: val: acc // val) { } userPortList; - # Define a user service for each configured user - systemd.services = - let - runScript = userName: pkgs.writeShellScript "intake-run.sh" '' - # Add the setuid wrapper directory so `crontab` is accessible - export PATH="${config.security.wrapperDir}:$PATH" - ${pythonEnv}/bin/intake run -d /home/${userName}/.local/share/intake --port ${toString userPort.${userName}} + # To avoid polluting PATH with httpd programs, define an htpasswd wrapper + htpasswdWrapper = pkgs.writeShellScriptBin "htpasswd" '' + ${pkgs.apacheHttpd}/bin/htpasswd $@ ''; - # systemd service definition for a single user, given `services.intake.users.userName` = `userCfg` - userServiceConfig = userName: userCfg: { - "intake@${userName}" = { - description = "Intake service for user ${userName}"; - script = "${runScript userName}"; - path = intakeCfg.extraPackages ++ userCfg.extraPackages; - serviceConfig = { - User = userName; - Type = "simple"; - }; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - enable = userCfg.enable; - }; - }; - in mkMerge (mapAttrsToList userServiceConfig intakeCfg.users); - # Define an nginx reverse proxy to request auth - services.nginx = mkIf (enabledUsers != {}) { - enable = true; - virtualHosts."intake" = mkIf (enabledUsers != {}) { - listen = [ intakeCfg.listen ]; - locations."/" = { - proxyPass = "http://127.0.0.1:$target_port"; - basicAuthFile = intakePwd; + # File locations + intakeDir = "/etc/intake"; + intakePwd = "${intakeDir}/htpasswd"; + in + { + # Apply the overlay so intake is included in pkgs. + nixpkgs.overlays = [ flake.overlays.default ]; + + # Define a user group for access to the htpasswd file. nginx needs to be able to read it. + users.groups.intake.members = mkIf (enabledUsers != { }) (enabledUserNames ++ [ "nginx" ]); + + # Define an activation script that ensures that the htpasswd file exists. + system.activationScripts.etc-intake = '' + if [ ! -e ${intakeDir} ]; then + ${pkgs.coreutils}/bin/mkdir -p ${intakeDir}; + fi + ${pkgs.coreutils}/bin/chown root:root ${intakeDir} + ${pkgs.coreutils}/bin/chmod 755 ${intakeDir} + if [ ! -e ${intakePwd} ]; then + ${pkgs.coreutils}/bin/touch ${intakePwd} + fi + ${pkgs.coreutils}/bin/chown root:intake ${intakePwd} + ${pkgs.coreutils}/bin/chmod 660 ${intakePwd} + ''; + + # Give every intake user the htpasswd wrapper, the shared packages, and the user-specific packages. + users.users = + let + addPackagesToUser = userName: { + ${userName}.packages = [ + htpasswdWrapper + intake + ] ++ intakeCfg.extraPackages ++ intakeCfg.users.${userName}.extraPackages; + }; + in + mkMerge (map addPackagesToUser enabledUserNames); + + # Enable cron + services.cron.enable = true; + + # Define a user service for each configured user + systemd.services = + let + runScript = + userName: + pkgs.writeShellScript "intake-run.sh" '' + # Add the setuid wrapper directory so `crontab` is accessible + export PATH="${config.security.wrapperDir}:$PATH" + ${pythonEnv}/bin/intake run -d /home/${userName}/.local/share/intake --port ${toString userPort.${userName}} + ''; + # systemd service definition for a single user, given `services.intake.users.userName` = `userCfg` + userServiceConfig = userName: userCfg: { + "intake@${userName}" = { + description = "Intake service for user ${userName}"; + script = "${runScript userName}"; + path = intakeCfg.extraPackages ++ userCfg.extraPackages; + serviceConfig = { + User = userName; + Type = "simple"; + }; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + enable = userCfg.enable; + }; + }; + in + mkMerge (mapAttrsToList userServiceConfig intakeCfg.users); + + # Define an nginx reverse proxy to request auth + services.nginx = mkIf (enabledUsers != { }) { + enable = true; + virtualHosts."intake" = mkIf (enabledUsers != { }) { + listen = [ intakeCfg.listen ]; + locations."/" = { + proxyPass = "http://127.0.0.1:$target_port"; + basicAuthFile = intakePwd; + }; + extraConfig = foldl (acc: val: acc + val) "" ( + mapAttrsToList (userName: port: '' + if ($remote_user = "${userName}") { + set $target_port ${toString port}; + } + '') userPort + ); }; - extraConfig = foldl (acc: val: acc + val) "" (mapAttrsToList (userName: port: '' - if ($remote_user = "${userName}") { - set $target_port ${toString port}; - } - '') userPort); }; }; - }; } diff --git a/shell.nix b/shell.nix index 6234bb4..493783d 100644 --- a/shell.nix +++ b/shell.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).shellNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).shellNix