Format everything, RFC-style

This commit is contained in:
Nicolas Jeannerod 2024-11-11 17:25:42 +01:00
parent 49473c43c8
commit 7007da1775
Signed by: Niols
GPG key ID: 35DB9EC8886E1CB8
16 changed files with 684 additions and 535 deletions

View file

@ -10,4 +10,4 @@ writeShellApplication {
"$result"/bin/switch-to-configuration switch
EOF
'';
}
}

View file

@ -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 = {

View file

@ -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

View file

@ -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;

View file

@ -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 = {

View file

@ -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
];
}

206
flake.nix
View file

@ -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
];
};
};
}

View file

@ -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

View file

@ -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.")
'';
}

View file

@ -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")
'';
}

View file

@ -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

View file

@ -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 = [

View file

@ -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 = [

View file

@ -1,4 +1,10 @@
{ modulesPath, lib, config, ... }: {
{
modulesPath,
lib,
config,
...
}:
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];

View file

@ -1,4 +1,5 @@
{ pkgs, modulesPath, ... }: {
{ pkgs, modulesPath, ... }:
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];

View file

@ -1,9 +1,15 @@
{ pkgs, lib, modulesPath, ... }:
{
pkgs,
lib,
modulesPath,
...
}:
let
inherit (lib) mkVMOverride;
in {
in
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
fediversity = {