first stab at a nixos test

for now, had to get rid of vmVariant. we can figure out how to add it
back when we understand how we should actually distinguish between
real machines and VMs
This commit is contained in:
Taeer Bar-Yam 2024-06-25 06:39:04 -04:00
parent 4e719da9d9
commit 693e21b1a8
9 changed files with 364 additions and 120 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
nixos.qcow2 nixos.qcow2
result* result*
.direnv .direnv
.nixos-test-history

View file

@ -27,8 +27,11 @@ With the VM running, you can then access the apps on your local machine's web br
NOTE: it sometimes takes a while for the services to start up, and in the meantime you will get 502 Bad Gateway. NOTE: it sometimes takes a while for the services to start up, and in the meantime you will get 502 Bad Gateway.
- Mastodon: <http://mastodon.localhost:55001> - Mastodon: through the reverse proxy at <https://mastodon.localhost:8443> and directly at <http://mastodon.localhost:55001>
- You can also create accounts on the machine itself by running `mastodon-tootctl accounts create test --email test@test.com --confirmed --approve` - You can create accounts on the machine itself by running `mastodon-tootctl accounts create test --email test@test.com --confirmed --approve`
- Account-related activities (logging in/out; preferences) can only be done on the insecure direct page <http://mastodon.localhost:55001>
- After you've logged in, you can go back to the secure page and you will remain logged in
- some operations may remove the port number from the URL. You'll have to add that back in manually
- PeerTube: <http://peertube.localhost:9000> - PeerTube: <http://peertube.localhost:9000>
- The root account can be accessed with username "root". The password can be obtained by running the following command on the VM: - The root account can be accessed with username "root". The password can be obtained by running the following command on the VM:
@ -51,6 +54,7 @@ NOTE: it sometimes takes a while for the services to start up, and in the meanti
- mastodon-web.service - mastodon-web.service
- peertube.service - peertube.service
- the `garage` CLI command gives information about garage storage, but cannot be used to actually inspect the contents. use `mc` (minio) for that - the `garage` CLI command gives information about garage storage, but cannot be used to actually inspect the contents. use `mc` (minio) for that
- in the chromium devtools, you can go to the networking tab and change things like response headers in a way that persists through reloads. this is much faster iteration time if that's what you need to epxeriment with.
# questions # questions
@ -77,5 +81,7 @@ When mastodon is running in production mode, we have a few problems:
- you have to click "accept the security risk" - you have to click "accept the security risk"
- it takes a while for the webpage to come online. Until then you see "502 Bad Gateway" - it takes a while for the webpage to come online. Until then you see "502 Bad Gateway"
- email sent from the mastodon instance (e.g. for account confirmation) should be accessible at <https://mastodon.localhost:55001/letter_opener>, but it's not working. - email sent from the mastodon instance (e.g. for account confirmation) should be accessible at <https://mastodon.localhost:55001/letter_opener>, but it's not working.
- mastodon is trying to fetch `missing.png` without ssl (`http://`). This isn't allowed, and i'm not sure why it's doing it.
- mastodon is trying to fetch `custom.css` from https://mastodon.localhost (no port), which is not the configured `LOCAL_DOMAIN`, so it's unclear why.

View file

@ -44,5 +44,24 @@
"-device virtconsole,chardev=char0,nr=0" "-device virtconsole,chardev=char0,nr=0"
]; ];
}; };
# we can't forward port 80 or 443, so let's run nginx on a different port
networking.firewall.allowedTCPPorts = [ 8443 8080 ];
services.nginx.defaultSSLListenPort = 8443;
services.nginx.defaultHTTPListenPort = 8080;
virtualisation.forwardPorts = [
{
from = "host";
host.port = 8080;
guest.port = 8080;
}
{
from = "host";
host.port = 8443;
guest.port = 8443;
}
];
}; };
} }

View file

@ -11,6 +11,13 @@
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in { in {
nixosModules = {
mastodon = import ./mastodon.nix;
peertube = import ./peertube.nix;
pixelfed = import ./pixelfed.nix;
garage = import ./garage.nix;
};
nixosConfigurations = { nixosConfigurations = {
mastodon = nixpkgs.lib.nixosSystem { mastodon = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
@ -33,6 +40,10 @@
}; };
}; };
checks.${system} = {
mastodon-garage = import ./tests/mastodon-garage.nix { inherit pkgs self; };
};
devShells.${system}.default = pkgs.mkShell { devShells.${system}.default = pkgs.mkShell {
inputs = with pkgs; [ inputs = with pkgs; [
nil nil

View file

@ -124,7 +124,6 @@ in {
}; };
config = { config = {
virtualisation.vmVariant = {
virtualisation.diskSize = 2048; virtualisation.diskSize = 2048;
virtualisation.forwardPorts = [ virtualisation.forwardPorts = [
{ {
@ -138,7 +137,6 @@ in {
guest.port = 3902; guest.port = 3902;
} }
]; ];
};
environment.systemPackages = [ pkgs.minio-client pkgs.awscli ]; environment.systemPackages = [ pkgs.minio-client pkgs.awscli ];
@ -190,7 +188,7 @@ in {
${ensureBucketsScript} ${ensureBucketsScript}
${ensureKeysScript} ${ensureKeysScript}
# garage doesn't like deleting keys that once existed # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time
# garage key delete ${snakeoil_key.id} --yes # garage key delete ${snakeoil_key.id} --yes
''; '';
}; };

View file

@ -10,12 +10,12 @@ in
ensureBuckets = { ensureBuckets = {
mastodon = { mastodon = {
website = true; website = true;
# corsRules = { corsRules = {
# enable = true; enable = true;
# allowedHeaders = [ "*" ]; allowedHeaders = [ "*" ];
# allowedMethods = [ "GET" ]; allowedMethods = [ "GET" ];
# allowedOrigins = [ "*" ]; allowedOrigins = [ "*" ];
# }; };
}; };
}; };
ensureKeys = { ensureKeys = {
@ -47,7 +47,7 @@ in
# but we want "<S3_BUCKET>.<S3_HOSTNAME>" # but we want "<S3_BUCKET>.<S3_HOSTNAME>"
S3_ALIAS_HOST = "mastodon.web.garage.localhost:3902"; S3_ALIAS_HOST = "mastodon.web.garage.localhost:3902";
# XXX: I think we need to set up a proper CDN host # XXX: I think we need to set up a proper CDN host
CDN_HOST = "mastodon.web.garage.localhost:3902"; # CDN_HOST = "mastodon.web.garage.localhost:3902";
# SEE: the last section in https://docs.joinmastodon.org/admin/optional/object-storage/ # SEE: the last section in https://docs.joinmastodon.org/admin/optional/object-storage/
# TODO: can we set up ACLs with garage? # TODO: can we set up ACLs with garage?
S3_PERMISSION = ""; S3_PERMISSION = "";
@ -82,8 +82,6 @@ in
} }
# VM setup # VM setup
{ {
# these configurations only apply when producing a VM (e.g. nixos-rebuild build-vm)
virtualisation.vmVariant = { config, ... }: {
services.mastodon = { services.mastodon = {
# redirects to localhost, but allows it to have a proper domain name # redirects to localhost, but allows it to have a proper domain name
localDomain = "mastodon.localhost"; localDomain = "mastodon.localhost";
@ -119,13 +117,11 @@ in
guest.port = 443; guest.port = 443;
} }
]; ];
};
} }
# mastodon development environment # mastodon development environment
{ {
networking.firewall.allowedTCPPorts = [ 55001 ]; networking.firewall.allowedTCPPorts = [ 55001 ];
virtualisation.vmVariant = { config, ... }: {
services.mastodon = { services.mastodon = {
# needed so we can directly access mastodon at port 55001 # needed so we can directly access mastodon at port 55001
# otherwise, mastodon has to be accessed *from* port 443, which we can't do via port forwarding # otherwise, mastodon has to be accessed *from* port 443, which we can't do via port forwarding
@ -136,8 +132,15 @@ in
BIND = "0.0.0.0"; BIND = "0.0.0.0";
# for letter_opener (still doesn't work though) # for letter_opener (still doesn't work though)
REMOTE_DEV = "true"; REMOTE_DEV = "true";
LOCAL_DOMAIN = "mastodon.localhost:8443";
}; };
}; };
# services.nginx.virtualHosts."${config.services.mastodon.localDomain}" = {
# extraConfig = ''
# add_header Content-Security-Policy 'base-uri 'none'; default-src 'none'; frame-ancestors 'none'; font-src 'self' http://mastodon.localhost:8443; img-src * https: data: blob: http://mastodon.localhost:8443; style-src 'self' http://mastodon.localhost:8443 'nonce-QvwdQ3lNRMmEcQnhZ22MAg=='; media-src 'self' https: data: http://mastodon.localhost:8443; frame-src 'self' https:; manifest-src 'self' http://mastodon.localhost:8443; form-action 'self'; child-src 'self' blob: http://mastodon.localhost:8443; worker-src 'self' blob: http://mastodon.localhost:8443; connect-src 'self' data: blob: http://mastodon.localhost:8443 http://mastodon.web.garage.localhost:3902 ws://mastodon.localhost:4000 ws://localhost:3035 http://localhost:3035; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://mastodon.localhost:8443'
# '';
# };
# services.nginx.virtualHosts."${config.services.mastodon.localDomain}".locations."/sw.js" =
services.postgresql = { services.postgresql = {
enable = true; enable = true;
@ -180,7 +183,7 @@ in
rails db:setup rails db:setup
# SAFETY_ASSURED=1 rails db:schema:load # SAFETY_ASSURED=1 rails db:schema:load
rails db:seed rails db:seed
else # else
# echo "Migrating database (this might be a noop)" # echo "Migrating database (this might be a noop)"
# rails db:migrate # rails db:migrate
fi fi
@ -192,6 +195,5 @@ in
guest.port = 55001; guest.port = 55001;
} }
]; ];
};
} }
] ]

BIN
tests/fediversity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

58
tests/mastodon-garage.nix Normal file
View file

@ -0,0 +1,58 @@
{ pkgs, self }:
let
# python = pkgs.python310.withPackages (ps: with ps; [ requests aiokafka ]);
rebuildableTest = import ./rebuildableTest.nix pkgs;
in
rebuildableTest {
name = "test-mastodon-garage";
# skipLint = true;
# skipTypeCheck = true;
nodes = {
server = {
imports = [ self.nixosModules.garage self.nixosModules.mastodon ];
environment.systemPackages = with pkgs; [ toot ];
};
};
testScript = ''
import re
import time
server.start()
with subtest("Mastodon starts"):
server.wait_for_unit("mastodon-web.service")
# make sure mastodon is fully up and running before we interact with it
time.sleep(180)
with subtest("Account creation"):
account_creation_output = server.succeed("mastodon-tootctl accounts create test --email test@test.com --confirmed --approve")
password_re = re.compile('New password: (.*)')
password_match = password_re.match(account_creation_output)
assert password_match is not None
password = password_match.groups()[0]
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_ctl\n")
time.sleep(0.2)
# Enter instance URL
server.send_chars("http://mastodon.localhost:55001\n")
time.sleep(0.2)
# Email
server.send_chars("test@test.com\n")
time.sleep(0.2)
# Password
server.send_chars(password + "\n")
with subtest("Post an image"):
server.succeed("toot post --media ${./fediversity.png}")
# TODO: I don't think there's a good way to test for whether the image visually shows up.
# we can test for CORS headers using curl / xh
# or **maybe** somehow read the javascript console?
'';
}

149
tests/rebuildableTest.nix Normal file
View file

@ -0,0 +1,149 @@
pkgs: test:
let
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
'';
# so we can ssh in and rebuild them
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "yes";
PermitEmptyPasswords = "yes";
UsePAM = "no";
};
};
virtualisation = mkIf (config.networking.hostName == "jumphost") {
forwardPorts = [{
from = "host";
host.port = 2222;
guest.port = 22;
}];
};
});
sshConfig = pkgs.writeText "ssh-config" ''
Host *
User root
StrictHostKeyChecking no
BatchMode yes
ConnectTimeout 20
UserKnownHostsFile=/dev/null
LogLevel Error # no "added to known hosts"
Host jumphost
Port 2222
HostName localhost
Host * !jumphost
ProxyJump jumphost
'';
# one should first start up the interactive test driver, then start the
# machines, then update the config, and then redeploy with the `rebuildScript`
# associated with the new config.
rebuildScript = pkgs.writeShellScriptBin "rebuild" ''
# 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)
})
rebuild_one() {
machine="$1"
echo "pushing new config to $machine"
if [ -z ''${configPaths[$machine]+x} ]; then
echo 'No machine '"$machine"' in this test.'
exit 1
fi
if ! ssh -F ${sshConfig} $machine true; then
echo 'Couldn'"'"'t connect to '"$machine"'. Make sure you'"'"'ve started it with `'"$machine"'.start()` in the test interactive driver.'
exit 1
fi
# taken from nixos-rebuild (we only want to do the activate part)
cmd=(
"systemd-run"
"-E" "LOCALE_ARCHIVE"
"--collect"
"--no-ask-password"
"--pty"
"--quiet"
"--same-dir"
"--service-type=exec"
"--unit=nixos-rebuild-switch-to-configuration"
"--wait"
"''${configPaths[$machine]}/bin/switch-to-configuration"
"test"
)
if ! ssh -F ${sshConfig} $machine "''${cmd[@]}"; then
echo "warning: error(s) occurred while switching to the new configuration"
exit 1
fi
}
if ! ssh -F ${sshConfig} jumphost true; then
echo 'Couldn'"'"'t connect to jump host. Make sure you are running driverInteractive, and that you'"'"'ve run `jumphost.start()` and `jumphost.forward_port(2222,22)`'
exit 1
fi
if [ -n "$1" ]; then
rebuild_one "$1"
else
for machine in ${concatStringsSep " " (attrNames rebuildableTest.driverInteractive.nodes)}; do
rebuild_one $machine
done
fi
'';
# NOTE: This is awkward because NixOS does not expose the module interface
# that is used to build tests. When we upstream this, we can build it into the
# system more naturally (and expose more of the interface to end users while
# 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 { });
});
in
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
'';
});
};
in
rebuildableTest