forked from fediversity/fediversity
		
	Format everything, RFC-style
This commit is contained in:
		
							parent
							
								
									9e234e7b2d
								
							
						
					
					
						commit
						81176a1a9a
					
				
					 16 changed files with 684 additions and 535 deletions
				
			
		|  | @ -10,4 +10,4 @@ writeShellApplication { | |||
|     "$result"/bin/switch-to-configuration switch | ||||
|     EOF | ||||
|   ''; | ||||
| }  | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,8 @@ let | |||
|   inherit (lib) mkOption mkEnableOption mkForce; | ||||
|   inherit (lib.types) types; | ||||
| 
 | ||||
| in { | ||||
| in | ||||
| { | ||||
|   imports = [ | ||||
|     ./garage.nix | ||||
|     ./mastodon.nix | ||||
|  | @ -33,7 +34,7 @@ in { | |||
| 
 | ||||
|       temp = mkOption { | ||||
|         description = "options that are only used while developing; should be removed eventually"; | ||||
|         default = {}; | ||||
|         default = { }; | ||||
|         type = types.submodule { | ||||
|           options = { | ||||
|             cores = mkOption { | ||||
|  | @ -51,7 +52,7 @@ in { | |||
| 
 | ||||
|       internal = mkOption { | ||||
|         description = "options that are only meant to be used internally; change at your own risk"; | ||||
|         default = {}; | ||||
|         default = { }; | ||||
|         type = types.submodule { | ||||
|           options = { | ||||
|             garage = { | ||||
|  |  | |||
|  | @ -8,27 +8,49 @@ let | |||
| in | ||||
| 
 | ||||
| # TODO: expand to a multi-machine setup | ||||
| { config, lib, pkgs, ... }: | ||||
| { | ||||
|   config, | ||||
|   lib, | ||||
|   pkgs, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| let | ||||
|   inherit (builtins) toString; | ||||
|   inherit (lib) types mkOption mkEnableOption optionalString concatStringsSep; | ||||
|   inherit (lib) | ||||
|     types | ||||
|     mkOption | ||||
|     mkEnableOption | ||||
|     optionalString | ||||
|     concatStringsSep | ||||
|     ; | ||||
|   inherit (lib.strings) escapeShellArg; | ||||
|   inherit (lib.attrsets) filterAttrs mapAttrs'; | ||||
|   cfg = config.services.garage; | ||||
|   fedicfg = config.fediversity.internal.garage; | ||||
|   concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset); | ||||
|   ensureBucketScriptFn = bucket: { website, aliases, corsRules }: | ||||
|   ensureBucketScriptFn = | ||||
|     bucket: | ||||
|     { | ||||
|       website, | ||||
|       aliases, | ||||
|       corsRules, | ||||
|     }: | ||||
|     let | ||||
|       bucketArg = escapeShellArg bucket; | ||||
|       corsRulesJSON = escapeShellArg (builtins.toJSON { | ||||
|         CORSRules = [{ | ||||
|           AllowedHeaders = corsRules.allowedHeaders; | ||||
|           AllowedMethods = corsRules.allowedMethods; | ||||
|           AllowedOrigins = corsRules.allowedOrigins; | ||||
|         }]; | ||||
|       }); | ||||
|     in '' | ||||
|       corsRulesJSON = escapeShellArg ( | ||||
|         builtins.toJSON { | ||||
|           CORSRules = [ | ||||
|             { | ||||
|               AllowedHeaders = corsRules.allowedHeaders; | ||||
|               AllowedMethods = corsRules.allowedMethods; | ||||
|               AllowedOrigins = corsRules.allowedOrigins; | ||||
|             } | ||||
|           ]; | ||||
|         } | ||||
|       ); | ||||
|     in | ||||
|     '' | ||||
|       # garage bucket info tells us if the bucket already exists | ||||
|       garage bucket info ${bucketArg} || garage bucket create ${bucketArg} | ||||
| 
 | ||||
|  | @ -37,9 +59,11 @@ let | |||
|         garage bucket website --allow ${bucketArg} | ||||
|       ''} | ||||
| 
 | ||||
|       ${concatStringsSep "\n" (map (alias: '' | ||||
|         garage bucket alias ${bucketArg} ${escapeShellArg alias} | ||||
|       '') aliases)} | ||||
|       ${concatStringsSep "\n" ( | ||||
|         map (alias: '' | ||||
|           garage bucket alias ${bucketArg} ${escapeShellArg alias} | ||||
|         '') aliases | ||||
|       )} | ||||
| 
 | ||||
|       ${optionalString corsRules.enable '' | ||||
|         garage bucket allow --read --write --owner ${bucketArg} --key tmp | ||||
|  | @ -49,15 +73,29 @@ let | |||
|       ''} | ||||
|     ''; | ||||
|   ensureBucketsScript = concatMapAttrs ensureBucketScriptFn cfg.ensureBuckets; | ||||
|   ensureAccessScriptFn = key: bucket: { read, write, owner }: '' | ||||
|     garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ | ||||
|       ${escapeShellArg bucket} --key ${escapeShellArg key} | ||||
|   ''; | ||||
|   ensureKeyScriptFn = key: {id, secret, ensureAccess}: '' | ||||
|     ## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :` | ||||
|     garage key import --yes -n ${escapeShellArg key} ${escapeShellArg id} ${escapeShellArg secret} || : | ||||
|     ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} | ||||
|   ''; | ||||
|   ensureAccessScriptFn = | ||||
|     key: bucket: | ||||
|     { | ||||
|       read, | ||||
|       write, | ||||
|       owner, | ||||
|     }: | ||||
|     '' | ||||
|       garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ | ||||
|         ${escapeShellArg bucket} --key ${escapeShellArg key} | ||||
|     ''; | ||||
|   ensureKeyScriptFn = | ||||
|     key: | ||||
|     { | ||||
|       id, | ||||
|       secret, | ||||
|       ensureAccess, | ||||
|     }: | ||||
|     '' | ||||
|       ## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :` | ||||
|       garage key import --yes -n ${escapeShellArg key} ${escapeShellArg id} ${escapeShellArg secret} || : | ||||
|       ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} | ||||
|     ''; | ||||
|   ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg.ensureKeys; | ||||
| in | ||||
| 
 | ||||
|  | @ -66,76 +104,85 @@ in | |||
|   options = { | ||||
|     services.garage = { | ||||
|       ensureBuckets = mkOption { | ||||
|         type = types.attrsOf (types.submodule { | ||||
|           options = { | ||||
|             website = mkOption { | ||||
|               type = types.bool; | ||||
|               default = false; | ||||
|             }; | ||||
|             # I think setting corsRules should allow another website to show images from your bucket | ||||
|             corsRules = { | ||||
|               enable = mkEnableOption "CORS Rules"; | ||||
|               allowedHeaders = mkOption { | ||||
|                 type = types.listOf types.str; | ||||
|                 default = []; | ||||
|         type = types.attrsOf ( | ||||
|           types.submodule { | ||||
|             options = { | ||||
|               website = mkOption { | ||||
|                 type = types.bool; | ||||
|                 default = false; | ||||
|               }; | ||||
|               allowedMethods = mkOption { | ||||
|                 type = types.listOf types.str; | ||||
|                 default = []; | ||||
|               # I think setting corsRules should allow another website to show images from your bucket | ||||
|               corsRules = { | ||||
|                 enable = mkEnableOption "CORS Rules"; | ||||
|                 allowedHeaders = mkOption { | ||||
|                   type = types.listOf types.str; | ||||
|                   default = [ ]; | ||||
|                 }; | ||||
|                 allowedMethods = mkOption { | ||||
|                   type = types.listOf types.str; | ||||
|                   default = [ ]; | ||||
|                 }; | ||||
|                 allowedOrigins = mkOption { | ||||
|                   type = types.listOf types.str; | ||||
|                   default = [ ]; | ||||
|                 }; | ||||
|               }; | ||||
|               allowedOrigins = mkOption { | ||||
|               aliases = mkOption { | ||||
|                 type = types.listOf types.str; | ||||
|                 default = []; | ||||
|                 default = [ ]; | ||||
|               }; | ||||
|             }; | ||||
|             aliases = mkOption { | ||||
|               type = types.listOf types.str; | ||||
|               default = []; | ||||
|             }; | ||||
|           }; | ||||
|         }); | ||||
|         default = {}; | ||||
|           } | ||||
|         ); | ||||
|         default = { }; | ||||
|       }; | ||||
|       ensureKeys = mkOption { | ||||
|         type = types.attrsOf (types.submodule { | ||||
|           # TODO: these should be managed as secrets, not in the nix store | ||||
|           options = { | ||||
|             id = mkOption { | ||||
|               type = types.str; | ||||
|         type = types.attrsOf ( | ||||
|           types.submodule { | ||||
|             # TODO: these should be managed as secrets, not in the nix store | ||||
|             options = { | ||||
|               id = mkOption { | ||||
|                 type = types.str; | ||||
|               }; | ||||
|               secret = mkOption { | ||||
|                 type = types.str; | ||||
|               }; | ||||
|               # TODO: assert at least one of these is true | ||||
|               # NOTE: this currently needs to be done at the top level module | ||||
|               ensureAccess = mkOption { | ||||
|                 type = types.attrsOf ( | ||||
|                   types.submodule { | ||||
|                     options = { | ||||
|                       read = mkOption { | ||||
|                         type = types.bool; | ||||
|                         default = false; | ||||
|                       }; | ||||
|                       write = mkOption { | ||||
|                         type = types.bool; | ||||
|                         default = false; | ||||
|                       }; | ||||
|                       owner = mkOption { | ||||
|                         type = types.bool; | ||||
|                         default = false; | ||||
|                       }; | ||||
|                     }; | ||||
|                   } | ||||
|                 ); | ||||
|                 default = [ ]; | ||||
|               }; | ||||
|             }; | ||||
|             secret = mkOption { | ||||
|               type = types.str; | ||||
|             }; | ||||
|             # TODO: assert at least one of these is true | ||||
|             # NOTE: this currently needs to be done at the top level module | ||||
|             ensureAccess = mkOption { | ||||
|               type = types.attrsOf (types.submodule { | ||||
|                 options = { | ||||
|                   read = mkOption { | ||||
|                     type = types.bool; | ||||
|                     default = false; | ||||
|                   }; | ||||
|                   write = mkOption { | ||||
|                     type = types.bool; | ||||
|                     default = false; | ||||
|                   }; | ||||
|                   owner = mkOption { | ||||
|                     type = types.bool; | ||||
|                     default = false; | ||||
|                   }; | ||||
|                 }; | ||||
|               }); | ||||
|               default = []; | ||||
|             }; | ||||
|           }; | ||||
|         }); | ||||
|         default = {}; | ||||
|           } | ||||
|         ); | ||||
|         default = { }; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   config = lib.mkIf config.fediversity.enable { | ||||
|     environment.systemPackages = [ pkgs.minio-client pkgs.awscli ]; | ||||
|     environment.systemPackages = [ | ||||
|       pkgs.minio-client | ||||
|       pkgs.awscli | ||||
|     ]; | ||||
| 
 | ||||
|     networking.firewall.allowedTCPPorts = [ | ||||
|       fedicfg.rpc.port | ||||
|  | @ -178,9 +225,11 @@ in | |||
|             ''; | ||||
|           }; | ||||
|         }; | ||||
|       in mapAttrs' | ||||
|         (bucket: _: {name = fedicfg.web.domainForBucket bucket; inherit value;}) | ||||
|         (filterAttrs (_: {website, ...}: website) cfg.ensureBuckets); | ||||
|       in | ||||
|       mapAttrs' (bucket: _: { | ||||
|         name = fedicfg.web.domainForBucket bucket; | ||||
|         inherit value; | ||||
|       }) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets); | ||||
| 
 | ||||
|     systemd.services.ensure-garage = { | ||||
|       after = [ "garage.service" ]; | ||||
|  | @ -188,7 +237,11 @@ in | |||
|       serviceConfig = { | ||||
|         Type = "oneshot"; | ||||
|       }; | ||||
|       path = [ cfg.package pkgs.perl pkgs.awscli ]; | ||||
|       path = [ | ||||
|         cfg.package | ||||
|         pkgs.perl | ||||
|         pkgs.awscli | ||||
|       ]; | ||||
|       script = '' | ||||
|         set -xeuo pipefail | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,10 +5,15 @@ let | |||
|   }; | ||||
| in | ||||
| 
 | ||||
| { config, lib, pkgs, ... }: | ||||
| { | ||||
|   config, | ||||
|   lib, | ||||
|   pkgs, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) { | ||||
|  #### garage setup | ||||
|   #### garage setup | ||||
|   services.garage = { | ||||
|     ensureBuckets = { | ||||
|       mastodon = { | ||||
|  | @ -58,7 +63,10 @@ lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) { | |||
|   #### mastodon setup | ||||
| 
 | ||||
|   # open up access to the mastodon web interface. 80 is necessary if only for ACME | ||||
|   networking.firewall.allowedTCPPorts = [ 80 443 ]; | ||||
|   networking.firewall.allowedTCPPorts = [ | ||||
|     80 | ||||
|     443 | ||||
|   ]; | ||||
| 
 | ||||
|   services.mastodon = { | ||||
|     enable = true; | ||||
|  |  | |||
|  | @ -5,10 +5,18 @@ let | |||
|   }; | ||||
| in | ||||
| 
 | ||||
| { config, lib, pkgs, ... }: | ||||
| { | ||||
|   config, | ||||
|   lib, | ||||
|   pkgs, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { | ||||
|   networking.firewall.allowedTCPPorts = [ 80 443 ]; | ||||
|   networking.firewall.allowedTCPPorts = [ | ||||
|     80 | ||||
|     443 | ||||
|   ]; | ||||
| 
 | ||||
|   services.garage = { | ||||
|     ensureBuckets = { | ||||
|  | @ -22,7 +30,7 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { | |||
|           allowedOrigins = [ "*" ]; | ||||
|         }; | ||||
|       }; | ||||
|         # TODO: these are too broad, after getting everything works narrow it down to the domain we actually want | ||||
|       # TODO: these are too broad, after getting everything works narrow it down to the domain we actually want | ||||
|       peertube-playlists = { | ||||
|         website = true; | ||||
|         corsRules = { | ||||
|  |  | |||
|  | @ -5,7 +5,12 @@ let | |||
|   }; | ||||
| in | ||||
| 
 | ||||
| { config, lib, pkgs, ... }: | ||||
| { | ||||
|   config, | ||||
|   lib, | ||||
|   pkgs, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) { | ||||
|   services.garage = { | ||||
|  | @ -80,5 +85,8 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) { | |||
|     after = [ "ensure-garage.service" ]; | ||||
|   }; | ||||
| 
 | ||||
|   networking.firewall.allowedTCPPorts = [ 80 443 ]; | ||||
|   networking.firewall.allowedTCPPorts = [ | ||||
|     80 | ||||
|     443 | ||||
|   ]; | ||||
| } | ||||
|  |  | |||
|  | @ -9,113 +9,123 @@ | |||
|     disko.url = "github:nix-community/disko"; | ||||
|   }; | ||||
| 
 | ||||
|   outputs = { self, nixpkgs, nixpkgs-latest, pixelfed, disko }: | ||||
|   let | ||||
|     system = "x86_64-linux"; | ||||
|     lib = nixpkgs.lib; | ||||
|     pkgs = nixpkgs.legacyPackages.${system}; | ||||
|     pkgsLatest = nixpkgs-latest.legacyPackages.${system}; | ||||
|     bleedingFediverseOverlay = (self: super: { | ||||
|       pixelfed = pkgsLatest.pixelfed.overrideAttrs (old: { | ||||
|         src = pixelfed; | ||||
|         patches = (old.patches or [ ]) ++ [ ./fediversity/pixelfed-group-permissions.patch ]; | ||||
|       }); | ||||
|       ## TODO: give mastodon, peertube the same treatment | ||||
|     }); | ||||
|   in { | ||||
|     nixosModules = { | ||||
|       ## Bleeding-edge fediverse packages | ||||
|       bleedingFediverse = { | ||||
|         nixpkgs.overlays = [ bleedingFediverseOverlay ]; | ||||
|       }; | ||||
|       ## Fediversity modules | ||||
|       fediversity = import ./fediversity; | ||||
|   outputs = | ||||
|     { | ||||
|       self, | ||||
|       nixpkgs, | ||||
|       nixpkgs-latest, | ||||
|       pixelfed, | ||||
|       disko, | ||||
|     }: | ||||
|     let | ||||
|       system = "x86_64-linux"; | ||||
|       lib = nixpkgs.lib; | ||||
|       pkgs = nixpkgs.legacyPackages.${system}; | ||||
|       pkgsLatest = nixpkgs-latest.legacyPackages.${system}; | ||||
|       bleedingFediverseOverlay = ( | ||||
|         self: super: { | ||||
|           pixelfed = pkgsLatest.pixelfed.overrideAttrs (old: { | ||||
|             src = pixelfed; | ||||
|             patches = (old.patches or [ ]) ++ [ ./fediversity/pixelfed-group-permissions.patch ]; | ||||
|           }); | ||||
|           ## TODO: give mastodon, peertube the same treatment | ||||
|         } | ||||
|       ); | ||||
|     in | ||||
|     { | ||||
|       nixosModules = { | ||||
|         ## Bleeding-edge fediverse packages | ||||
|         bleedingFediverse = { | ||||
|           nixpkgs.overlays = [ bleedingFediverseOverlay ]; | ||||
|         }; | ||||
|         ## Fediversity modules | ||||
|         fediversity = import ./fediversity; | ||||
| 
 | ||||
|       ## VM-specific modules | ||||
|       interactive-vm = import ./vm/interactive-vm.nix; | ||||
|       garage-vm = import ./vm/garage-vm.nix; | ||||
|       mastodon-vm = import ./vm/mastodon-vm.nix; | ||||
|       peertube-vm = import ./vm/peertube-vm.nix; | ||||
|       pixelfed-vm = import ./vm/pixelfed-vm.nix; | ||||
|         ## VM-specific modules | ||||
|         interactive-vm = import ./vm/interactive-vm.nix; | ||||
|         garage-vm = import ./vm/garage-vm.nix; | ||||
|         mastodon-vm = import ./vm/mastodon-vm.nix; | ||||
|         peertube-vm = import ./vm/peertube-vm.nix; | ||||
|         pixelfed-vm = import ./vm/pixelfed-vm.nix; | ||||
| 
 | ||||
|       disk-layout = import ./disk-layout.nix; | ||||
|     }; | ||||
| 
 | ||||
|     nixosConfigurations = { | ||||
|       mastodon = nixpkgs.lib.nixosSystem { | ||||
|         inherit system; | ||||
|         modules = with self.nixosModules; [ | ||||
|           disko.nixosModules.default | ||||
|           disk-layout | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           interactive-vm | ||||
|           garage-vm | ||||
|           mastodon-vm | ||||
|         ]; | ||||
|         disk-layout = import ./disk-layout.nix; | ||||
|       }; | ||||
| 
 | ||||
|       peertube = nixpkgs.lib.nixosSystem { | ||||
|         inherit system; | ||||
|         modules = with self.nixosModules; [ | ||||
|           disko.nixosModules.default | ||||
|           disk-layout | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           interactive-vm | ||||
|           garage-vm | ||||
|           peertube-vm | ||||
|         ]; | ||||
|       nixosConfigurations = { | ||||
|         mastodon = nixpkgs.lib.nixosSystem { | ||||
|           inherit system; | ||||
|           modules = with self.nixosModules; [ | ||||
|             disko.nixosModules.default | ||||
|             disk-layout | ||||
|             bleedingFediverse | ||||
|             fediversity | ||||
|             interactive-vm | ||||
|             garage-vm | ||||
|             mastodon-vm | ||||
|           ]; | ||||
|         }; | ||||
| 
 | ||||
|         peertube = nixpkgs.lib.nixosSystem { | ||||
|           inherit system; | ||||
|           modules = with self.nixosModules; [ | ||||
|             disko.nixosModules.default | ||||
|             disk-layout | ||||
|             bleedingFediverse | ||||
|             fediversity | ||||
|             interactive-vm | ||||
|             garage-vm | ||||
|             peertube-vm | ||||
|           ]; | ||||
|         }; | ||||
| 
 | ||||
|         pixelfed = nixpkgs.lib.nixosSystem { | ||||
|           inherit system; | ||||
|           modules = with self.nixosModules; [ | ||||
|             disko.nixosModules.default | ||||
|             disk-layout | ||||
|             bleedingFediverse | ||||
|             fediversity | ||||
|             interactive-vm | ||||
|             garage-vm | ||||
|             pixelfed-vm | ||||
|           ]; | ||||
|         }; | ||||
| 
 | ||||
|         all = nixpkgs.lib.nixosSystem { | ||||
|           inherit system; | ||||
|           modules = with self.nixosModules; [ | ||||
|             disko.nixosModules.default | ||||
|             disk-layout | ||||
|             bleedingFediverse | ||||
|             fediversity | ||||
|             interactive-vm | ||||
|             garage-vm | ||||
|             peertube-vm | ||||
|             pixelfed-vm | ||||
|             mastodon-vm | ||||
|           ]; | ||||
|         }; | ||||
|       }; | ||||
| 
 | ||||
|       pixelfed = nixpkgs.lib.nixosSystem { | ||||
|         inherit system; | ||||
|         modules = with self.nixosModules; [ | ||||
|           disko.nixosModules.default | ||||
|           disk-layout | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           interactive-vm | ||||
|           garage-vm | ||||
|           pixelfed-vm | ||||
|         ]; | ||||
|       ## Fully-feature ISO installer | ||||
|       mkInstaller = import ./installer.nix; | ||||
|       installers = lib.mapAttrs (_: config: self.mkInstaller nixpkgs config) self.nixosConfigurations; | ||||
| 
 | ||||
|       deploy = | ||||
|         let | ||||
|           deployCommand = (pkgs.callPackage ./deploy.nix { }); | ||||
|         in | ||||
|         lib.mapAttrs (name: config: deployCommand name config) self.nixosConfigurations; | ||||
| 
 | ||||
|       checks.${system} = { | ||||
|         mastodon-garage = import ./tests/mastodon-garage.nix { inherit pkgs self; }; | ||||
|         pixelfed-garage = import ./tests/pixelfed-garage.nix { inherit pkgs self; }; | ||||
|       }; | ||||
| 
 | ||||
|       all = nixpkgs.lib.nixosSystem { | ||||
|         inherit system; | ||||
|         modules = with self.nixosModules; [ | ||||
|           disko.nixosModules.default | ||||
|           disk-layout | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           interactive-vm | ||||
|           garage-vm | ||||
|           peertube-vm | ||||
|           pixelfed-vm | ||||
|           mastodon-vm | ||||
|       devShells.${system}.default = pkgs.mkShell { | ||||
|         inputs = with pkgs; [ | ||||
|           nil | ||||
|         ]; | ||||
|       }; | ||||
|     }; | ||||
| 
 | ||||
|     ## Fully-feature ISO installer | ||||
|     mkInstaller = import ./installer.nix; | ||||
|     installers = lib.mapAttrs (_: config: self.mkInstaller nixpkgs config) self.nixosConfigurations; | ||||
| 
 | ||||
|     deploy = | ||||
|       let | ||||
|         deployCommand = (pkgs.callPackage ./deploy.nix { }); | ||||
|       in | ||||
|       lib.mapAttrs (name: config: deployCommand name config) self.nixosConfigurations; | ||||
| 
 | ||||
|     checks.${system} = { | ||||
|       mastodon-garage = import ./tests/mastodon-garage.nix { inherit pkgs self; }; | ||||
|       pixelfed-garage = import ./tests/pixelfed-garage.nix { inherit pkgs self; }; | ||||
|     }; | ||||
| 
 | ||||
|     devShells.${system}.default = pkgs.mkShell { | ||||
|       inputs = with pkgs; [ | ||||
|         nil | ||||
|       ]; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -4,15 +4,22 @@ | |||
|   WARNING: Running this installer will format the target disk! | ||||
| */ | ||||
| 
 | ||||
| { nixpkgs, | ||||
|   hostKeys ? {} | ||||
| { | ||||
|   nixpkgs, | ||||
|   hostKeys ? { }, | ||||
| }: | ||||
| machine: | ||||
| 
 | ||||
| let | ||||
|   inherit (builtins) concatStringsSep attrValues mapAttrs; | ||||
| 
 | ||||
|   installer = { config, pkgs, lib, ... }: | ||||
|   installer = | ||||
|     { | ||||
|       config, | ||||
|       pkgs, | ||||
|       lib, | ||||
|       ... | ||||
|     }: | ||||
|     let | ||||
|       bootstrap = pkgs.writeShellApplication { | ||||
|         name = "bootstrap"; | ||||
|  | @ -20,39 +27,35 @@ let | |||
|         text = '' | ||||
|           ${machine.config.system.build.diskoScript} | ||||
|           nixos-install --no-root-password --no-channel-copy --system ${machine.config.system.build.toplevel} | ||||
|           ${ | ||||
|             concatStringsSep "\n" ( | ||||
|               attrValues ( | ||||
|               mapAttrs | ||||
|                 (kind: keys: '' | ||||
|                    cp ${keys.private} /mnt/etc/ssh/ssh_host_${kind}_key | ||||
|                    chmod 600 /mnt/etc/ssh/ssh_host_${kind}_key | ||||
|                    cp ${keys.public} /mnt/etc/ssh/ssh_host_${kind}_key.pub | ||||
|                    chmod 644 /mnt/etc/ssh/ssh_host_${kind}_key.pub | ||||
|                  '') | ||||
|                 hostKeys | ||||
|               ) | ||||
|           ${concatStringsSep "\n" ( | ||||
|             attrValues ( | ||||
|               mapAttrs (kind: keys: '' | ||||
|                 cp ${keys.private} /mnt/etc/ssh/ssh_host_${kind}_key | ||||
|                 chmod 600 /mnt/etc/ssh/ssh_host_${kind}_key | ||||
|                 cp ${keys.public} /mnt/etc/ssh/ssh_host_${kind}_key.pub | ||||
|                 chmod 644 /mnt/etc/ssh/ssh_host_${kind}_key.pub | ||||
|               '') hostKeys | ||||
|             ) | ||||
|           } | ||||
|           )} | ||||
|           poweroff | ||||
|         ''; | ||||
|       }; | ||||
|     in | ||||
|       { | ||||
|         imports = [ | ||||
|           "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" | ||||
|         ]; | ||||
|         nixpkgs.hostPlatform = "x86_64-linux"; | ||||
|         services.getty.autologinUser = lib.mkForce "root"; | ||||
|         programs.bash.loginShellInit = nixpkgs.lib.getExe bootstrap; | ||||
|     { | ||||
|       imports = [ | ||||
|         "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" | ||||
|       ]; | ||||
|       nixpkgs.hostPlatform = "x86_64-linux"; | ||||
|       services.getty.autologinUser = lib.mkForce "root"; | ||||
|       programs.bash.loginShellInit = nixpkgs.lib.getExe bootstrap; | ||||
| 
 | ||||
|         isoImage = { | ||||
|           compressImage = false; | ||||
|           squashfsCompression = "lz4"; | ||||
|           isoName = lib.mkForce "installer.iso"; | ||||
|           ## ^^ FIXME: Use a more interesting name or keep the default name and | ||||
|           ## use `isoImage.isoName` in the tests. | ||||
|         }; | ||||
|       isoImage = { | ||||
|         compressImage = false; | ||||
|         squashfsCompression = "lz4"; | ||||
|         isoName = lib.mkForce "installer.iso"; | ||||
|         ## ^^ FIXME: Use a more interesting name or keep the default name and | ||||
|         ## use `isoImage.isoName` in the tests. | ||||
|       }; | ||||
|     }; | ||||
| in | ||||
| (nixpkgs.lib.nixosSystem { modules = [installer]; }).config.system.build.isoImage | ||||
| (nixpkgs.lib.nixosSystem { modules = [ installer ]; }).config.system.build.isoImage | ||||
|  |  | |||
|  | @ -2,143 +2,149 @@ | |||
| let | ||||
|   lib = pkgs.lib; | ||||
|   rebuildableTest = import ./rebuildableTest.nix pkgs; | ||||
|   seleniumScript = pkgs.writers.writePython3Bin "selenium-script" | ||||
|     { | ||||
|       libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|     } '' | ||||
|     from selenium import webdriver | ||||
|     from selenium.webdriver.common.by import By | ||||
|     from selenium.webdriver.firefox.options import Options | ||||
|     from selenium.webdriver.support.ui import WebDriverWait | ||||
|   seleniumScript = | ||||
|     pkgs.writers.writePython3Bin "selenium-script" | ||||
|       { | ||||
|         libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|       } | ||||
|       '' | ||||
|         from selenium import webdriver | ||||
|         from selenium.webdriver.common.by import By | ||||
|         from selenium.webdriver.firefox.options import Options | ||||
|         from selenium.webdriver.support.ui import WebDriverWait | ||||
| 
 | ||||
|     print(1) | ||||
|         print(1) | ||||
| 
 | ||||
|     options = Options() | ||||
|     options.add_argument("--headless") | ||||
|     # devtools don't show up in headless screenshots | ||||
|     # options.add_argument("-devtools") | ||||
|     service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}")  # noqa: E501 | ||||
|         options = Options() | ||||
|         options.add_argument("--headless") | ||||
|         # devtools don't show up in headless screenshots | ||||
|         # options.add_argument("-devtools") | ||||
|         service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}")  # noqa: E501 | ||||
| 
 | ||||
|     driver = webdriver.Firefox(options=options, service=service) | ||||
|     driver.get("http://mastodon.localhost:55001/public/local") | ||||
|         driver = webdriver.Firefox(options=options, service=service) | ||||
|         driver.get("http://mastodon.localhost:55001/public/local") | ||||
| 
 | ||||
|     # wait until the statuses load | ||||
|     WebDriverWait(driver, 90).until( | ||||
|         lambda x: x.find_element(By.CLASS_NAME, "status")) | ||||
|         # wait until the statuses load | ||||
|         WebDriverWait(driver, 90).until( | ||||
|             lambda x: x.find_element(By.CLASS_NAME, "status")) | ||||
| 
 | ||||
|     driver.save_screenshot("/mastodon-screenshot.png") | ||||
|         driver.save_screenshot("/mastodon-screenshot.png") | ||||
| 
 | ||||
|     driver.close() | ||||
|   ''; | ||||
|         driver.close() | ||||
|       ''; | ||||
| in | ||||
| pkgs.nixosTest { | ||||
|   name = "test-mastodon-garage"; | ||||
| 
 | ||||
|   nodes = { | ||||
|     server = { config, ... }: { | ||||
|       virtualisation.memorySize = lib.mkVMOverride 4096; | ||||
|       imports = with self.nixosModules; [ | ||||
|         bleedingFediverse | ||||
|         fediversity | ||||
|         garage-vm | ||||
|         mastodon-vm | ||||
|       ]; | ||||
|       # TODO: pair down | ||||
|       environment.systemPackages = with pkgs; [ | ||||
|         python3 | ||||
|         firefox-unwrapped | ||||
|         geckodriver | ||||
|         toot | ||||
|         xh | ||||
|         seleniumScript | ||||
|         helix | ||||
|         imagemagick | ||||
|       ]; | ||||
|       environment.variables = { | ||||
|         POST_MEDIA = ./green.png; | ||||
|         AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.mastodon.id; | ||||
|         AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.mastodon.secret; | ||||
|     server = | ||||
|       { config, ... }: | ||||
|       { | ||||
|         virtualisation.memorySize = lib.mkVMOverride 4096; | ||||
|         imports = with self.nixosModules; [ | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           garage-vm | ||||
|           mastodon-vm | ||||
|         ]; | ||||
|         # TODO: pair down | ||||
|         environment.systemPackages = with pkgs; [ | ||||
|           python3 | ||||
|           firefox-unwrapped | ||||
|           geckodriver | ||||
|           toot | ||||
|           xh | ||||
|           seleniumScript | ||||
|           helix | ||||
|           imagemagick | ||||
|         ]; | ||||
|         environment.variables = { | ||||
|           POST_MEDIA = ./green.png; | ||||
|           AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.mastodon.id; | ||||
|           AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.mastodon.secret; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   testScript = { nodes, ... }: '' | ||||
|     import re | ||||
|     import time | ||||
|   testScript = | ||||
|     { nodes, ... }: | ||||
|     '' | ||||
|       import re | ||||
|       import time | ||||
| 
 | ||||
|     server.start() | ||||
|       server.start() | ||||
| 
 | ||||
|     with subtest("Mastodon starts"): | ||||
|       server.wait_for_unit("mastodon-web.service") | ||||
|       with subtest("Mastodon starts"): | ||||
|         server.wait_for_unit("mastodon-web.service") | ||||
| 
 | ||||
|     # make sure mastodon is fully up and running before we interact with it | ||||
|     # TODO: is there a way to test for this? | ||||
|     time.sleep(180) | ||||
|       # make sure mastodon is fully up and running before we interact with it | ||||
|       # TODO: is there a way to test for this? | ||||
|       time.sleep(180) | ||||
| 
 | ||||
|     with subtest("Account creation"): | ||||
|       account_creation_output = server.succeed("mastodon-tootctl accounts create test --email test@test.com --confirmed --approve") | ||||
|       password_match = re.match('.*New password: ([^\n]*).*', account_creation_output, re.S) | ||||
|       if password_match is None: | ||||
|         raise Exception(f"account creation did not generate a password.\n{account_creation_output}") | ||||
|       password = password_match.group(1) | ||||
|       with subtest("Account creation"): | ||||
|         account_creation_output = server.succeed("mastodon-tootctl accounts create test --email test@test.com --confirmed --approve") | ||||
|         password_match = re.match('.*New password: ([^\n]*).*', account_creation_output, re.S) | ||||
|         if password_match is None: | ||||
|           raise Exception(f"account creation did not generate a password.\n{account_creation_output}") | ||||
|         password = password_match.group(1) | ||||
| 
 | ||||
|     with subtest("TTY Login"): | ||||
|       server.wait_until_tty_matches("1", "login: ") | ||||
|       server.send_chars("root\n"); | ||||
|       with subtest("TTY Login"): | ||||
|         server.wait_until_tty_matches("1", "login: ") | ||||
|         server.send_chars("root\n"); | ||||
| 
 | ||||
|     with subtest("Log in with toot"): | ||||
|       # toot doesn't provide a way to just specify our login details as arguments, so we have to pretend we're typing them in at the prompt | ||||
|       server.send_chars("toot login_cli --instance http://mastodon.localhost:55001 --email test@test.com\n") | ||||
|       server.wait_until_tty_matches("1", "Password: ") | ||||
|       server.send_chars(password + "\n") | ||||
|       server.wait_until_tty_matches("1", "Successfully logged in.") | ||||
|       with subtest("Log in with toot"): | ||||
|         # toot doesn't provide a way to just specify our login details as arguments, so we have to pretend we're typing them in at the prompt | ||||
|         server.send_chars("toot login_cli --instance http://mastodon.localhost:55001 --email test@test.com\n") | ||||
|         server.wait_until_tty_matches("1", "Password: ") | ||||
|         server.send_chars(password + "\n") | ||||
|         server.wait_until_tty_matches("1", "Successfully logged in.") | ||||
| 
 | ||||
|     with subtest("post text"): | ||||
|       server.succeed("echo 'hello mastodon' | toot post") | ||||
|       with subtest("post text"): | ||||
|         server.succeed("echo 'hello mastodon' | toot post") | ||||
| 
 | ||||
|     with subtest("post image"): | ||||
|       server.succeed("toot post --media $POST_MEDIA") | ||||
|       with subtest("post image"): | ||||
|         server.succeed("toot post --media $POST_MEDIA") | ||||
| 
 | ||||
|     with subtest("access garage"): | ||||
|       server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY") | ||||
|       server.succeed("mc ls garage/mastodon") | ||||
|       with subtest("access garage"): | ||||
|         server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY") | ||||
|         server.succeed("mc ls garage/mastodon") | ||||
| 
 | ||||
|     with subtest("access image in garage"): | ||||
|       image = server.succeed("mc find garage --regex original") | ||||
|       image = image.rstrip() | ||||
|       if image == "": | ||||
|         raise Exception("image posted to mastodon did not get stored in garage") | ||||
|       server.succeed(f"mc cat {image} >/garage-image.webp") | ||||
|       garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.webp") | ||||
|       image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") | ||||
|       if garage_image_hash != image_hash: | ||||
|         raise Exception("image stored in garage did not match image uploaded") | ||||
|       with subtest("access image in garage"): | ||||
|         image = server.succeed("mc find garage --regex original") | ||||
|         image = image.rstrip() | ||||
|         if image == "": | ||||
|           raise Exception("image posted to mastodon did not get stored in garage") | ||||
|         server.succeed(f"mc cat {image} >/garage-image.webp") | ||||
|         garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.webp") | ||||
|         image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") | ||||
|         if garage_image_hash != image_hash: | ||||
|           raise Exception("image stored in garage did not match image uploaded") | ||||
| 
 | ||||
|     with subtest("Content security policy allows garage images"): | ||||
|       headers = server.succeed("xh -h http://mastodon.localhost:55001/public/local") | ||||
|       csp_match = None | ||||
|       # I can't figure out re.MULTILINE | ||||
|       for header in headers.split("\n"): | ||||
|         csp_match =  re.match('^Content-Security-Policy: (.*)$', header) | ||||
|         if csp_match is not None: | ||||
|           break | ||||
|       if csp_match is None: | ||||
|         raise Exception("mastodon did not send a content security policy header") | ||||
|       csp = csp_match.group(1) | ||||
|       # the img-src content security policy should include the garage server | ||||
|       ## TODO: use `nodes.server.fediversity.internal.garage.api.url` same as above, but beware of escaping the regex. Be careful with port 80 though. | ||||
|       garage_csp = re.match(".*; img-src[^;]*web\.garage\.localhost.*", csp) | ||||
|       if garage_csp is None: | ||||
|         raise Exception("Mastodon's content security policy does not include garage server. image will not be displayed properly on mastodon.") | ||||
|       with subtest("Content security policy allows garage images"): | ||||
|         headers = server.succeed("xh -h http://mastodon.localhost:55001/public/local") | ||||
|         csp_match = None | ||||
|         # I can't figure out re.MULTILINE | ||||
|         for header in headers.split("\n"): | ||||
|           csp_match =  re.match('^Content-Security-Policy: (.*)$', header) | ||||
|           if csp_match is not None: | ||||
|             break | ||||
|         if csp_match is None: | ||||
|           raise Exception("mastodon did not send a content security policy header") | ||||
|         csp = csp_match.group(1) | ||||
|         # the img-src content security policy should include the garage server | ||||
|         ## TODO: use `nodes.server.fediversity.internal.garage.api.url` same as above, but beware of escaping the regex. Be careful with port 80 though. | ||||
|         garage_csp = re.match(".*; img-src[^;]*web\.garage\.localhost.*", csp) | ||||
|         if garage_csp is None: | ||||
|           raise Exception("Mastodon's content security policy does not include garage server. image will not be displayed properly on mastodon.") | ||||
| 
 | ||||
|     # this could in theory give a false positive if mastodon changes it's colorscheme to include pure green. | ||||
|     with subtest("image displays"): | ||||
|       server.succeed("selenium-script") | ||||
|       server.copy_from_vm("/mastodon-screenshot.png", "") | ||||
|       displayed_colors = server.succeed("convert /mastodon-screenshot.png -define histogram:unique-colors=true -format %c histogram:info:") | ||||
|       # check that the green image displayed somewhere | ||||
|       green_check = re.match(".*#00FF00.*", displayed_colors, re.S) | ||||
|       if green_check is None: | ||||
|         raise Exception("cannot detect the uploaded image on mastodon page.") | ||||
|   ''; | ||||
|       # this could in theory give a false positive if mastodon changes it's colorscheme to include pure green. | ||||
|       with subtest("image displays"): | ||||
|         server.succeed("selenium-script") | ||||
|         server.copy_from_vm("/mastodon-screenshot.png", "") | ||||
|         displayed_colors = server.succeed("convert /mastodon-screenshot.png -define histogram:unique-colors=true -format %c histogram:info:") | ||||
|         # check that the green image displayed somewhere | ||||
|         green_check = re.match(".*#00FF00.*", displayed_colors, re.S) | ||||
|         if green_check is None: | ||||
|           raise Exception("cannot detect the uploaded image on mastodon page.") | ||||
|     ''; | ||||
| } | ||||
|  |  | |||
|  | @ -50,166 +50,176 @@ let | |||
|     driver.quit() | ||||
|   ''; | ||||
| 
 | ||||
|   seleniumScriptPostPicture = pkgs.writers.writePython3Bin "selenium-script-post-picture" | ||||
|     { | ||||
|       libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|     } '' | ||||
|     import os | ||||
|     import time | ||||
|     ${seleniumImports} | ||||
|     from selenium.webdriver.support.wait import WebDriverWait | ||||
|   seleniumScriptPostPicture = | ||||
|     pkgs.writers.writePython3Bin "selenium-script-post-picture" | ||||
|       { | ||||
|         libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|       } | ||||
|       '' | ||||
|         import os | ||||
|         import time | ||||
|         ${seleniumImports} | ||||
|         from selenium.webdriver.support.wait import WebDriverWait | ||||
| 
 | ||||
|     ${seleniumSetup} | ||||
|     ${seleniumPixelfedLogin} | ||||
|     time.sleep(3) | ||||
|         ${seleniumSetup} | ||||
|         ${seleniumPixelfedLogin} | ||||
|         time.sleep(3) | ||||
| 
 | ||||
|     media_path = os.environ['POST_MEDIA'] | ||||
|         media_path = os.environ['POST_MEDIA'] | ||||
| 
 | ||||
|     # Find the new post form, fill it in with our pictureand a caption. | ||||
|     print("Click on “Create New Post”...", file=sys.stderr) | ||||
|     driver.find_element(By.LINK_TEXT, "Create New Post").click() | ||||
|     print("Add file to input element...", file=sys.stderr) | ||||
|     driver.find_element(By.XPATH, "//input[@type='file']").send_keys(media_path) | ||||
|     print("Add a caption", file=sys.stderr) | ||||
|     driver.find_element(By.CSS_SELECTOR, ".media-body textarea").send_keys( | ||||
|         "Fediversity test of image upload to pixelfed with garage storage." | ||||
|     ) | ||||
|     time.sleep(3) | ||||
|     print("Click on “Post” button...", file=sys.stderr) | ||||
|     driver.find_element(By.LINK_TEXT, "Post").click() | ||||
|         # Find the new post form, fill it in with our pictureand a caption. | ||||
|         print("Click on “Create New Post”...", file=sys.stderr) | ||||
|         driver.find_element(By.LINK_TEXT, "Create New Post").click() | ||||
|         print("Add file to input element...", file=sys.stderr) | ||||
|         driver.find_element(By.XPATH, "//input[@type='file']").send_keys(media_path) | ||||
|         print("Add a caption", file=sys.stderr) | ||||
|         driver.find_element(By.CSS_SELECTOR, ".media-body textarea").send_keys( | ||||
|             "Fediversity test of image upload to pixelfed with garage storage." | ||||
|         ) | ||||
|         time.sleep(3) | ||||
|         print("Click on “Post” button...", file=sys.stderr) | ||||
|         driver.find_element(By.LINK_TEXT, "Post").click() | ||||
| 
 | ||||
|     # Wait until the post loads, and in particular its picture, then take a | ||||
|     # screenshot of the whole page. | ||||
|     print("Wait for post and image to be loaded...", file=sys.stderr) | ||||
|     img = driver.find_element( | ||||
|         By.XPATH, | ||||
|         "//div[@class='timeline-status-component-content']//img" | ||||
|     ) | ||||
|     WebDriverWait(driver, timeout=10).until( | ||||
|         lambda d: d.execute_script("return arguments[0].complete", img) | ||||
|     ) | ||||
|     time.sleep(3) | ||||
|         # Wait until the post loads, and in particular its picture, then take a | ||||
|         # screenshot of the whole page. | ||||
|         print("Wait for post and image to be loaded...", file=sys.stderr) | ||||
|         img = driver.find_element( | ||||
|             By.XPATH, | ||||
|             "//div[@class='timeline-status-component-content']//img" | ||||
|         ) | ||||
|         WebDriverWait(driver, timeout=10).until( | ||||
|             lambda d: d.execute_script("return arguments[0].complete", img) | ||||
|         ) | ||||
|         time.sleep(3) | ||||
| 
 | ||||
|     ${seleniumTakeScreenshot "\"/home/selenium/screenshot.png\""} | ||||
|     ${seleniumQuit}''; | ||||
|         ${seleniumTakeScreenshot "\"/home/selenium/screenshot.png\""} | ||||
|         ${seleniumQuit}''; | ||||
| 
 | ||||
|   seleniumScriptGetSrc = pkgs.writers.writePython3Bin "selenium-script-get-src" | ||||
|     { | ||||
|       libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|     } '' | ||||
|     ${seleniumImports} | ||||
|     ${seleniumSetup} | ||||
|     ${seleniumPixelfedLogin} | ||||
|   seleniumScriptGetSrc = | ||||
|     pkgs.writers.writePython3Bin "selenium-script-get-src" | ||||
|       { | ||||
|         libraries = with pkgs.python3Packages; [ selenium ]; | ||||
|       } | ||||
|       '' | ||||
|         ${seleniumImports} | ||||
|         ${seleniumSetup} | ||||
|         ${seleniumPixelfedLogin} | ||||
| 
 | ||||
|     img = driver.find_element( | ||||
|         By.XPATH, | ||||
|         "//div[@class='timeline-status-component-content']//img" | ||||
|     ) | ||||
|     # REVIEW: Need to wait for it to be loaded? | ||||
|     print(img.get_attribute('src')) | ||||
|         img = driver.find_element( | ||||
|             By.XPATH, | ||||
|             "//div[@class='timeline-status-component-content']//img" | ||||
|         ) | ||||
|         # REVIEW: Need to wait for it to be loaded? | ||||
|         print(img.get_attribute('src')) | ||||
| 
 | ||||
|     ${seleniumQuit}''; | ||||
|         ${seleniumQuit}''; | ||||
| 
 | ||||
| in | ||||
| pkgs.nixosTest { | ||||
|   name = "test-pixelfed-garage"; | ||||
| 
 | ||||
|   nodes = { | ||||
|     server = { config, ... }: { | ||||
|     server = | ||||
|       { config, ... }: | ||||
|       { | ||||
| 
 | ||||
|       services = { | ||||
|         xserver = { | ||||
|           enable = true; | ||||
|           displayManager.lightdm.enable = true; | ||||
|           desktopManager.lxqt.enable = true; | ||||
|         services = { | ||||
|           xserver = { | ||||
|             enable = true; | ||||
|             displayManager.lightdm.enable = true; | ||||
|             desktopManager.lxqt.enable = true; | ||||
|           }; | ||||
| 
 | ||||
|           displayManager.autoLogin = { | ||||
|             enable = true; | ||||
|             user = "selenium"; | ||||
|           }; | ||||
|         }; | ||||
|         virtualisation.resolution = { | ||||
|           x = 1680; | ||||
|           y = 1050; | ||||
|         }; | ||||
| 
 | ||||
|         displayManager.autoLogin = { | ||||
|           enable = true; | ||||
|           user = "selenium"; | ||||
|         virtualisation = { | ||||
|           memorySize = lib.mkVMOverride 8192; | ||||
|           cores = 8; | ||||
|         }; | ||||
|         imports = with self.nixosModules; [ | ||||
|           bleedingFediverse | ||||
|           fediversity | ||||
|           garage-vm | ||||
|           pixelfed-vm | ||||
|         ]; | ||||
|         # TODO: pair down | ||||
|         environment.systemPackages = with pkgs; [ | ||||
|           python3 | ||||
|           chromium | ||||
|           chromedriver | ||||
|           xh | ||||
|           seleniumScriptPostPicture | ||||
|           seleniumScriptGetSrc | ||||
|           helix | ||||
|           imagemagick | ||||
|         ]; | ||||
|         environment.variables = { | ||||
|           POST_MEDIA = ./fediversity.png; | ||||
|           AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.pixelfed.id; | ||||
|           AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.pixelfed.secret; | ||||
|           ## without this we get frivolous errors in the logs | ||||
|           MC_REGION = "garage"; | ||||
|         }; | ||||
|         # chrome does not like being run as root | ||||
|         users.users.selenium = { | ||||
|           isNormalUser = true; | ||||
|         }; | ||||
|       }; | ||||
|       virtualisation.resolution = { x = 1680; y = 1050; }; | ||||
| 
 | ||||
| 
 | ||||
|       virtualisation = { | ||||
|         memorySize = lib.mkVMOverride 8192; | ||||
|         cores = 8; | ||||
|       }; | ||||
|       imports = with self.nixosModules; [ | ||||
|         bleedingFediverse | ||||
|         fediversity | ||||
|         garage-vm | ||||
|         pixelfed-vm | ||||
|       ]; | ||||
|       # TODO: pair down | ||||
|       environment.systemPackages = with pkgs; [ | ||||
|         python3 | ||||
|         chromium | ||||
|         chromedriver | ||||
|         xh | ||||
|         seleniumScriptPostPicture | ||||
|         seleniumScriptGetSrc | ||||
|         helix | ||||
|         imagemagick | ||||
|       ]; | ||||
|       environment.variables = { | ||||
|         POST_MEDIA = ./fediversity.png; | ||||
|         AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.pixelfed.id; | ||||
|         AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.pixelfed.secret; | ||||
|         ## without this we get frivolous errors in the logs | ||||
|         MC_REGION = "garage"; | ||||
|       }; | ||||
|       # chrome does not like being run as root | ||||
|       users.users.selenium = { | ||||
|         isNormalUser = true; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   testScript = { nodes, ... }: '' | ||||
|     import re | ||||
|   testScript = | ||||
|     { nodes, ... }: | ||||
|     '' | ||||
|       import re | ||||
| 
 | ||||
|     server.start() | ||||
|       server.start() | ||||
| 
 | ||||
|     with subtest("Pixelfed starts"): | ||||
|       server.wait_for_unit("phpfpm-pixelfed.service") | ||||
|       with subtest("Pixelfed starts"): | ||||
|         server.wait_for_unit("phpfpm-pixelfed.service") | ||||
| 
 | ||||
|     with subtest("Account creation"): | ||||
|       server.succeed("pixelfed-manage user:create --name=test --username=test --email=${email} --password=${password} --confirm_email=1") | ||||
|       with subtest("Account creation"): | ||||
|         server.succeed("pixelfed-manage user:create --name=test --username=test --email=${email} --password=${password} --confirm_email=1") | ||||
| 
 | ||||
|     # NOTE: This could in theory give a false positive if pixelfed changes it's | ||||
|     # colorscheme to include pure green. (see same problem in pixelfed-garage.nix). | ||||
|     # TODO: For instance: post a red image and check that the green pixel IS NOT | ||||
|     # there, then post a green image and check that the green pixel IS there. | ||||
|       # NOTE: This could in theory give a false positive if pixelfed changes it's | ||||
|       # colorscheme to include pure green. (see same problem in pixelfed-garage.nix). | ||||
|       # TODO: For instance: post a red image and check that the green pixel IS NOT | ||||
|       # there, then post a green image and check that the green pixel IS there. | ||||
| 
 | ||||
|     with subtest("Image displays"): | ||||
|       server.succeed("su - selenium -c 'selenium-script-post-picture ${email} ${password}'") | ||||
|       server.copy_from_vm("/home/selenium/screenshot.png", "") | ||||
|       displayed_colors = server.succeed("magick /home/selenium/screenshot.png -define histogram:unique-colors=true -format %c histogram:info:") | ||||
|       # check that the green image displayed somewhere | ||||
|       image_check = re.match(".*#FF0500.*", displayed_colors, re.S) | ||||
|       if image_check is None: | ||||
|         raise Exception("cannot detect the uploaded image on pixelfed page.") | ||||
|       with subtest("Image displays"): | ||||
|         server.succeed("su - selenium -c 'selenium-script-post-picture ${email} ${password}'") | ||||
|         server.copy_from_vm("/home/selenium/screenshot.png", "") | ||||
|         displayed_colors = server.succeed("magick /home/selenium/screenshot.png -define histogram:unique-colors=true -format %c histogram:info:") | ||||
|         # check that the green image displayed somewhere | ||||
|         image_check = re.match(".*#FF0500.*", displayed_colors, re.S) | ||||
|         if image_check is None: | ||||
|           raise Exception("cannot detect the uploaded image on pixelfed page.") | ||||
| 
 | ||||
|     with subtest("access garage"): | ||||
|       server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY") | ||||
|       server.succeed("mc ls garage/pixelfed") | ||||
|       with subtest("access garage"): | ||||
|         server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY") | ||||
|         server.succeed("mc ls garage/pixelfed") | ||||
| 
 | ||||
|     with subtest("access image in garage"): | ||||
|       image = server.succeed("mc find garage --regex '\\.png' --ignore '*_thumb.png'") | ||||
|       image = image.rstrip() | ||||
|       if image == "": | ||||
|         raise Exception("image posted to Pixelfed did not get stored in garage") | ||||
|       server.succeed(f"mc cat {image} >/garage-image.png") | ||||
|       garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.png") | ||||
|       image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") | ||||
|       if garage_image_hash != image_hash: | ||||
|         raise Exception("image stored in garage did not match image uploaded") | ||||
|       with subtest("access image in garage"): | ||||
|         image = server.succeed("mc find garage --regex '\\.png' --ignore '*_thumb.png'") | ||||
|         image = image.rstrip() | ||||
|         if image == "": | ||||
|           raise Exception("image posted to Pixelfed did not get stored in garage") | ||||
|         server.succeed(f"mc cat {image} >/garage-image.png") | ||||
|         garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.png") | ||||
|         image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") | ||||
|         if garage_image_hash != image_hash: | ||||
|           raise Exception("image stored in garage did not match image uploaded") | ||||
| 
 | ||||
|     with subtest("Check that image comes from garage"): | ||||
|       src = server.succeed("su - selenium -c 'selenium-script-get-src ${email} ${password}'") | ||||
|       if not src.startswith("${nodes.server.fediversity.internal.garage.web.urlForBucket "pixelfed"}"): | ||||
|         raise Exception("image does not come from garage") | ||||
|   ''; | ||||
|       with subtest("Check that image comes from garage"): | ||||
|         src = server.succeed("su - selenium -c 'selenium-script-get-src ${email} ${password}'") | ||||
|         if not src.startswith("${nodes.server.fediversity.internal.garage.web.urlForBucket "pixelfed"}"): | ||||
|           raise Exception("image does not come from garage") | ||||
|     ''; | ||||
| } | ||||
|  |  | |||
|  | @ -1,32 +1,42 @@ | |||
| pkgs: test: | ||||
| let | ||||
|   inherit (pkgs.lib) mapAttrsToList concatStringsSep genAttrs mkIf; | ||||
|   inherit (pkgs.lib) | ||||
|     mapAttrsToList | ||||
|     concatStringsSep | ||||
|     genAttrs | ||||
|     mkIf | ||||
|     ; | ||||
|   inherit (builtins) attrNames; | ||||
| 
 | ||||
|   interactiveConfig = ({ config, ... }: { | ||||
|     # so we can run `nix shell nixpkgs#foo` on the machines | ||||
|     nix.extraOptions = '' | ||||
|       extra-experimental-features = nix-command flakes | ||||
|     ''; | ||||
|   interactiveConfig = ( | ||||
|     { config, ... }: | ||||
|     { | ||||
|       # so we can run `nix shell nixpkgs#foo` on the machines | ||||
|       nix.extraOptions = '' | ||||
|         extra-experimental-features = nix-command flakes | ||||
|       ''; | ||||
| 
 | ||||
|     # so we can ssh in and rebuild them | ||||
|     services.openssh = { | ||||
|       enable = true; | ||||
|       settings = { | ||||
|         PermitRootLogin = "yes"; | ||||
|         PermitEmptyPasswords = "yes"; | ||||
|         UsePAM = false; | ||||
|       # so we can ssh in and rebuild them | ||||
|       services.openssh = { | ||||
|         enable = true; | ||||
|         settings = { | ||||
|           PermitRootLogin = "yes"; | ||||
|           PermitEmptyPasswords = "yes"; | ||||
|           UsePAM = false; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
| 
 | ||||
|     virtualisation = mkIf (config.networking.hostName == "jumphost") { | ||||
|       forwardPorts = [{ | ||||
|         from = "host"; | ||||
|         host.port = 2222; | ||||
|         guest.port = 22; | ||||
|       }]; | ||||
|     }; | ||||
|   }); | ||||
|       virtualisation = mkIf (config.networking.hostName == "jumphost") { | ||||
|         forwardPorts = [ | ||||
|           { | ||||
|             from = "host"; | ||||
|             host.port = 2222; | ||||
|             guest.port = 22; | ||||
|           } | ||||
|         ]; | ||||
|       }; | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   sshConfig = pkgs.writeText "ssh-config" '' | ||||
|     Host * | ||||
|  | @ -50,10 +60,11 @@ let | |||
|     # create an association array from machine names to the path to their | ||||
|     # configuration in the nix store | ||||
|     declare -A configPaths=(${ | ||||
|       concatStringsSep " " | ||||
|         (mapAttrsToList | ||||
|           (n: v: ''["${n}"]="${v.system.build.toplevel}"'') | ||||
|           rebuildableTest.driverInteractive.nodes) | ||||
|       concatStringsSep " " ( | ||||
|         mapAttrsToList ( | ||||
|           n: v: ''["${n}"]="${v.system.build.toplevel}"'' | ||||
|         ) rebuildableTest.driverInteractive.nodes | ||||
|       ) | ||||
|     }) | ||||
| 
 | ||||
|     rebuild_one() { | ||||
|  | @ -113,37 +124,40 @@ let | |||
|   # we're at it) | ||||
|   rebuildableTest = | ||||
|     let | ||||
|       preOverride = pkgs.nixosTest (test // { | ||||
|         interactive = (test.interactive or { }) // { | ||||
|           # no need to // with test.interactive.nodes here, since we are iterating | ||||
|           # over all of them, and adding back in the config via `imports` | ||||
|           nodes = genAttrs | ||||
|             ( | ||||
|               attrNames test.nodes or { } ++ | ||||
|                 attrNames test.interactive.nodes or { } ++ | ||||
|                 [ "jumphost" ] | ||||
|             ) | ||||
|             (n: { | ||||
|               imports = [ | ||||
|                 (test.interactive.${n} or { }) | ||||
|                 interactiveConfig | ||||
|               ]; | ||||
|             }); | ||||
|         }; | ||||
|         # override with test.passthru in case someone wants to overwrite us. | ||||
|         passthru = { inherit rebuildScript sshConfig; } // (test.passthru or { }); | ||||
|       }); | ||||
|       preOverride = pkgs.nixosTest ( | ||||
|         test | ||||
|         // { | ||||
|           interactive = (test.interactive or { }) // { | ||||
|             # no need to // with test.interactive.nodes here, since we are iterating | ||||
|             # over all of them, and adding back in the config via `imports` | ||||
|             nodes = | ||||
|               genAttrs (attrNames test.nodes or { } ++ attrNames test.interactive.nodes or { } ++ [ "jumphost" ]) | ||||
|                 (n: { | ||||
|                   imports = [ | ||||
|                     (test.interactive.${n} or { }) | ||||
|                     interactiveConfig | ||||
|                   ]; | ||||
|                 }); | ||||
|           }; | ||||
|           # override with test.passthru in case someone wants to overwrite us. | ||||
|           passthru = { | ||||
|             inherit rebuildScript sshConfig; | ||||
|           } // (test.passthru or { }); | ||||
|         } | ||||
|       ); | ||||
|     in | ||||
|     preOverride // { | ||||
|     preOverride | ||||
|     // { | ||||
|       driverInteractive = preOverride.driverInteractive.overrideAttrs (old: { | ||||
|         # this comes from runCommand, not mkDerivation, so this is the only | ||||
|         # hook we have to override | ||||
|         buildCommand = old.buildCommand + '' | ||||
|           ln -s ${sshConfig} $out/ssh-config | ||||
|           ln -s ${rebuildScript}/bin/rebuild $out/bin/rebuild | ||||
|         ''; | ||||
|         buildCommand = | ||||
|           old.buildCommand | ||||
|           + '' | ||||
|             ln -s ${sshConfig} $out/ssh-config | ||||
|             ln -s ${rebuildScript}/bin/rebuild $out/bin/rebuild | ||||
|           ''; | ||||
|       }); | ||||
|     }; | ||||
| in | ||||
| rebuildableTest | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,9 @@ | |||
| { lib, config, modulesPath, ... }: | ||||
| { | ||||
|   lib, | ||||
|   config, | ||||
|   modulesPath, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| let | ||||
|   inherit (lib) mkVMOverride mapAttrs' filterAttrs; | ||||
|  | @ -7,7 +12,8 @@ let | |||
| 
 | ||||
|   fedicfg = config.fediversity.internal.garage; | ||||
| 
 | ||||
| in { | ||||
| in | ||||
| { | ||||
|   imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ]; | ||||
| 
 | ||||
|   services.nginx.virtualHosts = | ||||
|  | @ -16,9 +22,11 @@ in { | |||
|         forceSSL = mkVMOverride false; | ||||
|         enableACME = mkVMOverride false; | ||||
|       }; | ||||
|     in mapAttrs' | ||||
|       (bucket: _: {name = fedicfg.web.domainForBucket bucket; inherit value;}) | ||||
|       (filterAttrs (_: {website, ...}: website) cfg.ensureBuckets); | ||||
|     in | ||||
|     mapAttrs' (bucket: _: { | ||||
|       name = fedicfg.web.domainForBucket bucket; | ||||
|       inherit value; | ||||
|     }) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets); | ||||
| 
 | ||||
|   virtualisation.diskSize = 2048; | ||||
|   virtualisation.forwardPorts = [ | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| # customize nixos-rebuild build-vm to be a bit more convenient | ||||
| { pkgs, ... }: { | ||||
| { pkgs, ... }: | ||||
| { | ||||
|   # let us log in | ||||
|   users.mutableUsers = false; | ||||
|   users.users.root.hashedPassword = ""; | ||||
|  | @ -34,7 +35,10 @@ | |||
|   # no graphics. see nixos-shell | ||||
|   virtualisation = { | ||||
|     graphics = false; | ||||
|     qemu.consoles = [ "tty0" "hvc0" ]; | ||||
|     qemu.consoles = [ | ||||
|       "tty0" | ||||
|       "hvc0" | ||||
|     ]; | ||||
|     qemu.options = [ | ||||
|       "-serial null" | ||||
|       "-device virtio-serial" | ||||
|  | @ -45,7 +49,10 @@ | |||
|   }; | ||||
| 
 | ||||
|   # we can't forward port 80 or 443, so let's run nginx on a different port | ||||
|   networking.firewall.allowedTCPPorts = [ 8443 8080 ]; | ||||
|   networking.firewall.allowedTCPPorts = [ | ||||
|     8443 | ||||
|     8080 | ||||
|   ]; | ||||
|   services.nginx.defaultSSLListenPort = 8443; | ||||
|   services.nginx.defaultHTTPListenPort = 8080; | ||||
|   virtualisation.forwardPorts = [ | ||||
|  |  | |||
|  | @ -1,4 +1,10 @@ | |||
| { modulesPath, lib, config, ... }: { | ||||
| { | ||||
|   modulesPath, | ||||
|   lib, | ||||
|   config, | ||||
|   ... | ||||
| }: | ||||
| { | ||||
| 
 | ||||
|   imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| { pkgs, modulesPath, ... }: { | ||||
| { pkgs, modulesPath, ... }: | ||||
| { | ||||
| 
 | ||||
|   imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,9 +1,15 @@ | |||
| { pkgs, lib, modulesPath, ... }: | ||||
| { | ||||
|   pkgs, | ||||
|   lib, | ||||
|   modulesPath, | ||||
|   ... | ||||
| }: | ||||
| 
 | ||||
| let | ||||
|   inherit (lib) mkVMOverride; | ||||
| 
 | ||||
| in { | ||||
| in | ||||
| { | ||||
|   imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ]; | ||||
| 
 | ||||
|   fediversity = { | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Valentin Gagarin
							Valentin Gagarin