Compare commits

...
This repository has been archived on 2024-11-13. You can view files and clone it, but cannot push or open issues or pull requests.

51 commits
main ... main

Author SHA1 Message Date
Nicolas Jeannerod b1a5e16432
Add pre-commit hooks for formatting and dead code 2024-11-11 17:39:20 +01:00
Nicolas Jeannerod 661f81b3f9
Cleanup dead code 2024-11-11 17:28:35 +01:00
Nicolas Jeannerod 7007da1775
Format everything, RFC-style 2024-11-11 17:25:42 +01:00
Nicolas Jeannerod 49473c43c8
Proxy Peertube behind Nginx 2024-11-11 17:10:58 +01:00
Nicolas Jeannerod 4f8ba4bf3c
Require secrets file also when on metal 2024-11-11 17:10:44 +01:00
Nicolas Jeannerod 8e03b4b34e
Fix typo 2024-11-11 16:36:33 +01:00
Nicolas Jeannerod c1dcdfe493
Open port 80, necessary for ACME 2024-11-11 16:34:24 +01:00
Nicolas Jeannerod f53a27baee
Number of cores also when on metal 2024-11-11 16:16:27 +01:00
Nicolas Jeannerod 2d522f51f5
Support installing host keys in the installer 2024-11-08 17:35:25 +01:00
Nicolas Jeannerod f04b71047c
Slight rework of the installer 2024-11-07 18:36:43 +01:00
Nicolas Jeannerod cd194f818d
Turn off the machine once if install is successful 2024-11-07 12:02:09 +01:00
Nicolas Jeannerod 007c168081 Fix Mastodon/Garage test 2024-10-30 19:44:07 +00:00
Nicolas Jeannerod fb342b02fb Also forward SSH port 2024-10-30 18:38:39 +00:00
Nicolas Jeannerod 96acf1f10d Use recommended proxy settings for Garage 2024-10-30 18:37:45 +00:00
Nicolas Jeannerod e299978508 Avoid clashes of security.acme.defaults options 2024-10-30 18:37:06 +00:00
Nicolas Jeannerod 0b5e3ca40e
Bump Taeer's nixpkgs 2024-10-29 17:13:51 +01:00
Nicolas Jeannerod 1de8f5bc17 Some fixes for Pixelfed on metal (#27) 2024-10-29 17:09:19 +01:00
Taeer Bar-Yam b36166ccc0 fix test to not use ACME/SSL (again) 2024-10-01 17:08:09 -04:00
Nicolas Jeannerod 4c8d380e9e
Proxy all buckets that have website = true 2024-10-01 18:18:47 +02:00
Nicolas Jeannerod 247a4258b2
No certificate for Garage web root domain 2024-10-01 18:04:53 +02:00
Nicolas Jeannerod be756ab8d3
Faster compression and note on isoName 2024-10-01 13:29:06 +02:00
Nicolas Jeannerod dd9b481b78
Expose mkInstaller 2024-10-01 13:14:56 +02:00
Nicolas Jeannerod 3cfc4370f7 Add missing module in tests 2024-10-01 09:40:38 +00:00
Nicolas Jeannerod e9b5de893d Create automatic installation ISOs (#26)
Co-authored-by: Taeer Bar-Yam <taeer.bar-yam@moduscreate.com>
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
Reviewed-on: Fediversity/simple-nixos-fediverse#26
Co-authored-by: Nicolas “Niols” Jeannerod <nicolas.jeannerod@moduscreate.com>
Co-committed-by: Nicolas “Niols” Jeannerod <nicolas.jeannerod@moduscreate.com>
2024-10-01 10:02:01 +02:00
Nicolas Jeannerod 7b36774b80
We are way past that! 2024-09-27 11:48:49 +02:00
Taeer Bar-Yam 4da997b3af fix frivolous errors in garage test 2024-09-26 01:41:06 -04:00
Taeer Bar-Yam fa53ecac53 fix the overlay 2024-09-25 11:25:21 -04:00
Taeer Bar-Yam d910dfe788 take bleeding edge pixelfed 2024-09-25 00:40:53 -04:00
Nicolas Jeannerod b461a44707
Not localhost 2024-09-24 16:59:37 +02:00
Nicolas Jeannerod fc18582a1b
Make Garage API domain be localhost 2024-09-24 16:42:53 +02:00
Nicolas Jeannerod e6b58b656b
Remove SSL in Garage VM 2024-09-24 14:56:33 +02:00
Nicolas Jeannerod bf303ff1d1
Remove SSL in VM 2024-09-24 14:52:13 +02:00
Nicolas Jeannerod a600829d56
s/port/internalPort 2024-09-24 14:42:18 +02:00
Nicolas Jeannerod 042cb2d517
Move Garage VM stuff out of main file 2024-09-24 14:40:35 +02:00
Nicolas Jeannerod 050042d255
domainForBucket 2024-09-24 14:23:29 +02:00
Nicolas Jeannerod 6b45256839
s/urlFor/urlForBucket 2024-09-24 14:17:56 +02:00
Taeer Bar-Yam 51a294a659 acme fixup 2 2024-09-23 12:39:55 -04:00
Taeer Bar-Yam 2116ac6b27 acme fixup 2024-09-23 12:39:15 -04:00
Taeer Bar-Yam 3e4b486921 httpS 2024-09-23 12:22:40 -04:00
Taeer Bar-Yam db39623eeb ADD http:// to proxypass 2024-09-23 12:18:22 -04:00
Taeer Bar-Yam ffb941687a remove http:// from nginx server name 2024-09-23 12:14:40 -04:00
Taeer Bar-Yam 2657e2130f mv {,internal}port 2024-09-23 12:11:04 -04:00
Taeer Bar-Yam ca8310dce3 had two 'cfg's. changed one to 'fedicfg' 2024-09-23 12:09:16 -04:00
Taeer Bar-Yam e093632222 ; 2024-09-23 11:58:49 -04:00
Taeer Bar-Yam 2501c480fb proxy garage web to port 80 2024-09-23 11:55:54 -04:00
Nicolas Jeannerod 011f166fd3
Exceptionally use non-staging LetsEncrypt servers 2024-09-20 18:55:00 +02:00
Nicolas Jeannerod 3bb9569eb4
ACME 2024-09-20 18:51:21 +02:00
Nicolas Jeannerod 6323e0adc8
Also open HTTPS port 2024-09-20 18:44:47 +02:00
Nicolas Jeannerod 55a6377b12
Ignore errors of garage key import 2024-09-20 18:39:32 +02:00
Nicolas Jeannerod 9be8232083
[HACK] comment out virtualisation 2024-09-20 18:25:21 +02:00
Nicolas Jeannerod c9665b927f
Move stuff from pixelfed-vm to pixelfed 2024-09-20 17:56:40 +02:00
20 changed files with 1131 additions and 509 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ result*
output
todo
/.pre-commit-config.yaml

View file

@ -46,6 +46,26 @@ NOTE: it sometimes takes a while for the services to start up, and in the meanti
```bash
pixelfed-manage user:create --name=test --username=test --email=test@test.com --password=testtest --confirm_email=1
```
# Building an installer image
Build an installer image for the desired configuration, e.g. for `peertube`:
```bash
nix build .#installers.peertube
```
Upload the image in `./result` to Proxmox when creating a VM.
Booting the image will format the disk and install NixOS with the desired configuration.
# Deploying an updated machine configuration
> TODO: There is currently no way to specify an actual target machine by name.
Assuming you have SSH configuration with access to the remote `root` user stored for a machine called e.g. `peertube`, deploy the configuration by the same name:
```bash
nix run .#deploy.peertube
```
## debugging notes

13
deploy.nix Normal file
View file

@ -0,0 +1,13 @@
{ writeShellApplication }:
name: _config:
writeShellApplication {
name = "deploy";
text = ''
result="$(nix build --print-out-paths ${./.}#nixosConfigurations#${name} --eval-store auto --store ssh-ng://${name})"
# shellcheck disable=SC2087
ssh ${name} << EOF
nix-env -p /nix/var/nix/profiles/system --set "$result"
"$result"/bin/switch-to-configuration switch
EOF
'';
}

36
disk-layout.nix Normal file
View file

@ -0,0 +1,36 @@
{ ... }:
{
disko.devices.disk.main = {
device = "/dev/sda";
type = "disk";
content = {
type = "gpt";
partitions = {
MBR = {
priority = 0;
size = "1M";
type = "EF02";
};
ESP = {
priority = 1;
size = "500M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
priority = 2;
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}

View file

@ -2,10 +2,11 @@
let
inherit (builtins) toString;
inherit (lib) mkOption mkEnableOption;
inherit (lib) mkOption mkEnableOption mkForce;
inherit (lib.types) types;
in {
in
{
imports = [
./garage.nix
./mastodon.nix
@ -31,9 +32,27 @@ in {
pixelfed.enable = mkEnableOption "default Fediversity Pixelfed configuration";
peertube.enable = mkEnableOption "default Fediversity PeerTube configuration";
temp = mkOption {
description = "options that are only used while developing; should be removed eventually";
default = { };
type = types.submodule {
options = {
cores = mkOption {
description = "number of cores; should be obtained from NixOps4";
type = types.int;
};
peertubeSecretsFile = mkOption {
description = "should it be provided by NixOps4? or maybe we should just ask for a main secret from which to derive all the others?";
type = types.path;
};
};
};
};
internal = mkOption {
description = "options that are only meant to be used internally; change at your own risk";
default = {};
default = { };
type = types.submodule {
options = {
garage = {
@ -64,17 +83,17 @@ in {
type = types.str;
default = "web.garage.${config.fediversity.domain}";
};
port = mkOption {
internalPort = mkOption {
type = types.int;
default = 3902;
};
rootDomainAndPort = mkOption {
type = types.str;
default = "${config.fediversity.internal.garage.web.rootDomain}:${toString config.fediversity.internal.garage.web.port}";
};
urlFor = mkOption {
domainForBucket = mkOption {
type = types.functionTo types.str;
default = bucket: "http://${bucket}.${config.fediversity.internal.garage.web.rootDomainAndPort}";
default = bucket: "${bucket}.${config.fediversity.internal.garage.web.rootDomain}";
};
urlForBucket = mkOption {
type = types.functionTo types.str;
default = bucket: "http://${config.fediversity.internal.garage.web.domainForBucket bucket}";
};
};
};
@ -89,7 +108,7 @@ in {
};
mastodon.domain = mkOption {
type = types.str;
default = "mastdodon.${config.fediversity.domain}";
default = "mastodon.${config.fediversity.domain}";
};
peertube.domain = mkOption {
type = types.str;
@ -100,4 +119,19 @@ in {
};
};
};
config = {
## FIXME: This should clearly go somewhere else; and we should have a
## `staging` vs. `production` setting somewhere.
security.acme = {
acceptTerms = true;
defaults.email = "nicolas.jeannerod+fediversity@moduscreate.com";
# defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
};
## NOTE: For a one-machine deployment, this removes the need to provide an
## `s3.garage.<domain>` domain. However, this will quickly stop working once
## we go to multi-machines deployment.
fediversity.internal.garage.api.domain = mkForce "s3.garage.localhost";
};
}

View file

@ -8,25 +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}
@ -35,26 +59,43 @@ 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
# TODO: endpoin-url should not be hard-coded
aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${config.fediversity.internal.garage.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${fedicfg.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
garage bucket deny --read --write --owner ${bucketArg} --key tmp
''}
'';
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}: ''
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
@ -63,94 +104,88 @@ 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 {
virtualisation.diskSize = 2048;
virtualisation.forwardPorts = [
{
from = "host";
host.port = config.fediversity.internal.garage.rpc.port;
guest.port = config.fediversity.internal.garage.rpc.port;
}
{
from = "host";
host.port = config.fediversity.internal.garage.web.port;
guest.port = config.fediversity.internal.garage.web.port;
}
environment.systemPackages = [
pkgs.minio-client
pkgs.awscli
];
environment.systemPackages = [ pkgs.minio-client pkgs.awscli ];
networking.firewall.allowedTCPPorts = [
config.fediversity.internal.garage.rpc.port
config.fediversity.internal.garage.web.port
fedicfg.rpc.port
];
services.garage = {
enable = true;
@ -160,30 +195,59 @@ in
# TODO: use a secret file
rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625";
# TODO: why does this have to be set? is there not a sensible default?
rpc_bind_addr = "[::]:${toString config.fediversity.internal.garage.rpc.port}";
rpc_public_addr = "[::1]:${toString config.fediversity.internal.garage.rpc.port}";
s3_api.api_bind_addr = "[::]:${toString config.fediversity.internal.garage.api.port}";
s3_web.bind_addr = "[::]:${toString config.fediversity.internal.garage.web.port}";
s3_web.root_domain = ".${config.fediversity.internal.garage.web.rootDomain}";
rpc_bind_addr = "[::]:${toString fedicfg.rpc.port}";
rpc_public_addr = "[::1]:${toString fedicfg.rpc.port}";
s3_api.api_bind_addr = "[::]:${toString fedicfg.api.port}";
s3_web.bind_addr = "[::]:${toString fedicfg.web.internalPort}";
s3_web.root_domain = ".${fedicfg.web.rootDomain}";
index = "index.html";
s3_api.s3_region = "garage";
s3_api.root_domain = ".${config.fediversity.internal.garage.api.domain}";
s3_api.root_domain = ".${fedicfg.api.domain}";
};
};
## Create a proxy from <bucket>.web.garage.<domain> to localhost:3902 for
## each bucket that has `website = true`.
services.nginx.virtualHosts =
let
value = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:3902";
extraConfig = ''
## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Disable buffering to a temporary file.
proxy_max_temp_file_size 0;
'';
};
};
in
mapAttrs' (bucket: _: {
name = fedicfg.web.domainForBucket bucket;
inherit value;
}) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets);
systemd.services.ensure-garage = {
after = [ "garage.service" ];
wantedBy = [ "garage.service" ];
serviceConfig = {
Type = "oneshot";
};
path = [ cfg.package pkgs.perl pkgs.awscli ];
path = [
cfg.package
pkgs.perl
pkgs.awscli
];
script = ''
set -xeuo pipefail
# Give Garage time to start up by waiting until somethings speaks HTTP
# behind Garage's API URL.
until ${pkgs.curl}/bin/curl -sio /dev/null ${config.fediversity.internal.garage.api.url}; do sleep 1; done
until ${pkgs.curl}/bin/curl -sio /dev/null ${fedicfg.api.url}; do sleep 1; done
# XXX: this is very sensitive to being a single instance
# (doing the bare minimum to get garage up and running)
@ -197,7 +261,8 @@ in
# XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys
# TODO: generate this key here rather than using a well-known key
garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret}
# TODO: if the key already exists, we get an error; hacked with this `|| :` which needs to be removed
garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret} || :
export AWS_ACCESS_KEY_ID=${snakeoil_key.id};
export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret};

View file

@ -5,10 +5,14 @@ let
};
in
{ config, lib, pkgs, ... }:
{
config,
lib,
...
}:
lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) {
#### garage setup
#### garage setup
services.garage = {
ensureBuckets = {
mastodon = {
@ -46,7 +50,7 @@ lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) {
AWS_ACCESS_KEY_ID = snakeoil_key.id;
AWS_SECRET_ACCESS_KEY = snakeoil_key.secret;
S3_PROTOCOL = "http";
S3_HOSTNAME = config.fediversity.internal.garage.web.rootDomainAndPort;
S3_HOSTNAME = config.fediversity.internal.garage.web.rootDomain;
# by default it tries to use "<S3_HOSTNAME>/<S3_BUCKET>"
S3_ALIAS_HOST = "${S3_BUCKET}.${S3_HOSTNAME}";
# SEE: the last section in https://docs.joinmastodon.org/admin/optional/object-storage/
@ -57,8 +61,11 @@ lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) {
#### mastodon setup
# open up access to the mastodon web interface
networking.firewall.allowedTCPPorts = [ 443 ];
# open up access to the mastodon web interface. 80 is necessary if only for ACME
networking.firewall.allowedTCPPorts = [
80
443
];
services.mastodon = {
enable = true;
@ -66,6 +73,10 @@ lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) {
localDomain = config.fediversity.internal.mastodon.domain;
configureNginx = true;
# from the documentation: recommended is the amount of your CPU cores minus
# one. but it also must be a positive integer
streamingProcesses = lib.max 1 (config.fediversity.temp.cores - 1);
# TODO: configure a mailserver so this works
smtp = {
fromAddress = "noreply@${config.fediversity.internal.mastodon.domain}";

View file

@ -5,10 +5,17 @@ let
};
in
{ config, lib, pkgs, ... }:
{
config,
lib,
...
}:
lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
networking.firewall.allowedTCPPorts = [ 80 9000 ];
networking.firewall.allowedTCPPorts = [
80
443
];
services.garage = {
ensureBuckets = {
@ -22,7 +29,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 = {
@ -59,7 +66,8 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
# TODO: in most of nixpkgs, these are true by default. upstream that unless there's a good reason not to.
redis.createLocally = true;
database.createLocally = true;
configureNginx = true;
secrets.secretsFile = config.fediversity.temp.peertubeSecretsFile;
settings = {
object_storage = {
@ -67,24 +75,24 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
endpoint = config.fediversity.internal.garage.api.url;
region = "garage";
# not supported by garage
# not supported by garage
# SEE: https://garagehq.deuxfleurs.fr/documentation/connect/apps/#peertube
proxy.proxyify_private_files = false;
web_videos = rec {
bucket_name = "peertube-videos";
prefix = "";
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
};
videos = rec {
bucket_name = "peertube-videos";
prefix = "";
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
};
streaming_playlists = rec {
bucket_name = "peertube-playlists";
prefix = "";
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
};
};
};
@ -94,4 +102,12 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
AWS_ACCESS_KEY_ID=${snakeoil_key.id}
AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}
'';
## Proxying through Nginx
services.peertube.configureNginx = true;
services.nginx.virtualHosts.${config.services.peertube.localDomain} = {
forceSSL = true;
enableACME = true;
};
}

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 = {
@ -38,16 +43,37 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
services.pixelfed = {
enable = true;
domain = config.fediversity.internal.pixelfed.domain;
# TODO: secrets management!!!
secretFile = pkgs.writeText "secrets.env" ''
APP_KEY=adKK9EcY8Hcj3PLU7rzG9rJ6KKTOtYfA
'';
## Taeer feels like this way of configuring Nginx is odd; there should
## instead be a `services.pixefed.nginx.enable` option and the actual Nginx
## configuration should be in `services.nginx`. See eg. `pretix`.
##
## TODO: If that indeed makes sense, upstream.
nginx = {
forceSSL = true;
enableACME = true;
# locations."/public/".proxyPass = "${config.fediversity.internal.garage.web.urlForBucket "pixelfed"}/public/";
};
};
services.pixelfed.settings = {
## NOTE: This depends on the targets, eg. universities might want control
## over who has an account. We probably want a universal
## `fediversity.openRegistration` option.
OPEN_REGISTRATION = true;
# DANGEROUSLY_SET_FILESYSTEM_DRIVER = "s3";
FILESYSTEM_CLOUD = "s3";
PF_ENABLE_CLOUD = true;
AWS_ACCESS_KEY_ID = snakeoil_key.id;
AWS_SECRET_ACCESS_KEY = snakeoil_key.secret;
AWS_DEFAULT_REGION = "garage";
AWS_URL = config.fediversity.internal.garage.web.urlFor "pixelfed";
AWS_URL = config.fediversity.internal.garage.web.urlForBucket "pixelfed";
AWS_BUCKET = "pixelfed";
AWS_ENDPOINT = config.fediversity.internal.garage.api.url;
AWS_USE_PATH_STYLE_ENDPOINT = false;
@ -59,7 +85,8 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
after = [ "ensure-garage.service" ];
};
services.pixelfed.package = pkgs.pixelfed.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ ./pixelfed-group-permissions.patch ];
});
networking.firewall.allowedTCPPorts = [
80
443
];
}

View file

@ -1,12 +1,151 @@
{
"nodes": {
"disko": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1727347829,
"narHash": "sha256-y7cW6TjJKy+tu7efxeWI6lyg4VVx/9whx+OmrhmRShU=",
"owner": "nix-community",
"repo": "disko",
"rev": "1879e48907c14a70302ff5d0539c3b9b6f97feaa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1730814269,
"narHash": "sha256-fWPHyhYE6xvMI1eGY3pwBTq85wcy1YXqdzTZF+06nOg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "d70155fdc00df4628446352fc58adc640cd705c2",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1723726852,
"narHash": "sha256-lRzlx4fPRtzA+dgz9Rh4WK5yAW3TsAXx335DQqxY2XY=",
"lastModified": 1725194671,
"narHash": "sha256-tLGCFEFTB5TaOKkpfw3iYT9dnk4awTP/q4w+ROpMfuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-latest": {
"locked": {
"lastModified": 1727220152,
"narHash": "sha256-6ezRTVBZT25lQkvaPrfJSxYLwqcbNWm6feD/vG1FO0o=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "24959f933187217890b206788a85bfa73ba75949",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
"narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1730768919,
"narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1730137230,
"narHash": "sha256-0kW6v0alzWIc/Dc/DoVZ7A9qNScv77bj/zYTKI67HZM=",
"owner": "radvendii",
"repo": "nixpkgs",
"rev": "9286249a1673cf5b14a4793e22dd44b70cb69a0d",
"rev": "df815998652a1d00ce7c059a1e5ef7d7c0548c90",
"type": "github"
},
"original": {
@ -16,9 +155,30 @@
"type": "github"
}
},
"pixelfed": {
"flake": false,
"locked": {
"lastModified": 1719823820,
"narHash": "sha256-CKjqnxp7p2z/13zfp4HQ1OAmaoUtqBKS6HFm6TV8Jwg=",
"owner": "pixelfed",
"repo": "pixelfed",
"rev": "4c245cf429330d01fcb8ebeb9aa8c84a9574a645",
"type": "github"
},
"original": {
"owner": "pixelfed",
"ref": "v0.12.3",
"repo": "pixelfed",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"disko": "disko",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_3",
"nixpkgs-latest": "nixpkgs-latest",
"pixelfed": "pixelfed"
}
}
},

173
flake.nix
View file

@ -1,64 +1,143 @@
{
description = "Testing mastodon configurations";
inputs = {
nixpkgs.url = "github:radvendii/nixpkgs/nixos_rebuild_tests";
nixpkgs-latest.url = "github:nixos/nixpkgs";
git-hooks.url = "github:cachix/git-hooks.nix";
pixelfed = {
url = "github:pixelfed/pixelfed?ref=v0.12.3";
flake = false;
};
disko.url = "github:nix-community/disko";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
outputs =
{
self,
nixpkgs,
nixpkgs-latest,
git-hooks,
pixelfed,
disko,
}:
let
system = "x86_64-linux";
lib = nixpkgs.lib;
pkgs = nixpkgs.legacyPackages.${system};
pkgsLatest = nixpkgs-latest.legacyPackages.${system};
bleedingFediverseOverlay = (
_: _: {
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;
nixosModules = {
## 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;
mastodon-vm = import ./vm/mastodon-vm.nix;
peertube-vm = import ./vm/peertube-vm.nix;
pixelfed-vm = import ./vm/pixelfed-vm.nix;
};
nixosConfigurations = {
mastodon = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm mastodon-vm ];
disk-layout = import ./disk-layout.nix;
};
peertube = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-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; [ fediversity interactive-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; };
pre-commit = git-hooks.lib.${system}.run {
src = ./.;
hooks = {
nixfmt-rfc-style.enable = true;
deadnix.enable = true;
};
};
};
all = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [
fediversity
interactive-vm
peertube-vm
pixelfed-vm
mastodon-vm
devShells.${system}.default = pkgs.mkShell {
inputs = with pkgs; [
nil
];
shellHook = self.checks.${system}.pre-commit.shellHook;
};
};
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
];
};
};
}

61
installer.nix Normal file
View file

@ -0,0 +1,61 @@
/**
Convert a NixOS configuration to one for a minimal installer ISO
WARNING: Running this installer will format the target disk!
*/
{
nixpkgs,
hostKeys ? { },
}:
machine:
let
inherit (builtins) concatStringsSep attrValues mapAttrs;
installer =
{
config,
pkgs,
lib,
...
}:
let
bootstrap = pkgs.writeShellApplication {
name = "bootstrap";
runtimeInputs = with pkgs; [ nixos-install-tools ];
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
)
)}
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;
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

View file

@ -1,139 +1,153 @@
{ pkgs, self }:
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
print(1)
## FIXME: this binding was not used, but maybe we want a side-effect or something?
# rebuildableTest = import ./rebuildableTest.nix pkgs;
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
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
driver = webdriver.Firefox(options=options, service=service)
driver.get("http://mastodon.localhost:55001/public/local")
print(1)
# wait until the statuses load
WebDriverWait(driver, 90).until(
lambda x: x.find_element(By.CLASS_NAME, "status"))
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.save_screenshot("/mastodon-screenshot.png")
driver = webdriver.Firefox(options=options, service=service)
driver.get("http://mastodon.localhost:55001/public/local")
driver.close()
'';
# 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.close()
'';
in
pkgs.nixosTest {
name = "test-mastodon-garage";
nodes = {
server = { config, ... }: {
virtualisation.memorySize = lib.mkVMOverride 4096;
imports = with self.nixosModules; [ 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.
garage_csp = re.match(".*; img-src[^;]*web\.garage\.localhost:3902.*", 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

@ -1,7 +1,9 @@
{ pkgs, self }:
let
lib = pkgs.lib;
rebuildableTest = import ./rebuildableTest.nix pkgs;
## FIXME: this binding was not used but maybe we want a side effect or something?
# rebuildableTest = import ./rebuildableTest.nix pkgs;
email = "test@test.com";
password = "testtest";
@ -50,159 +52,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; [ 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;
};
# 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.urlFor "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

44
vm/garage-vm.nix Normal file
View file

@ -0,0 +1,44 @@
{
lib,
config,
modulesPath,
...
}:
let
inherit (lib) mkVMOverride mapAttrs' filterAttrs;
cfg = config.services.garage;
fedicfg = config.fediversity.internal.garage;
in
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
services.nginx.virtualHosts =
let
value = {
forceSSL = mkVMOverride false;
enableACME = mkVMOverride false;
};
in
mapAttrs' (bucket: _: {
name = fedicfg.web.domainForBucket bucket;
inherit value;
}) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets);
virtualisation.diskSize = 2048;
virtualisation.forwardPorts = [
{
from = "host";
host.port = fedicfg.rpc.port;
guest.port = fedicfg.rpc.port;
}
{
from = "host";
host.port = fedicfg.web.internalPort;
guest.port = fedicfg.web.internalPort;
}
];
}

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"
@ -44,12 +48,19 @@
];
};
# 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 = [
{
from = "host";
host.port = 22222;
guest.port = 22;
}
{
from = "host";
host.port = 8080;

View file

@ -1,9 +1,12 @@
{ modulesPath, lib, config, ... }: {
{
modulesPath,
lib,
config,
...
}:
{
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
config = lib.mkMerge [
{
@ -11,19 +14,17 @@
enable = true;
domain = "localhost";
mastodon.enable = true;
temp.cores = config.virtualisation.cores;
};
services.mastodon = {
extraConfig = {
EMAIL_DOMAIN_ALLOWLIST = "example.com";
};
# from the documentation: recommended is the amount of your CPU cores
# minus one. but it also must be a positive integer
streamingProcesses = lib.max 1 (config.virtualisation.cores - 1);
};
security.acme = {
security.acme = lib.mkVMOverride {
defaults = {
# invalid server; the systemd service will fail, and we won't get
# properly signed certificates. but let's not spam the letsencrypt

View file

@ -1,9 +1,8 @@
{ pkgs, modulesPath, ... }: {
{ modulesPath, ... }:
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
services.peertube = {
enableWebHttps = false;
@ -11,10 +10,6 @@
listen.hostname = "0.0.0.0";
instance.name = "PeerTube Test VM";
};
# TODO: use agenix
secrets.secretsFile = pkgs.writeText "secret" ''
574e093907d1157ac0f8e760a6deb1035402003af5763135bae9cbd6abe32b24
'';
};
virtualisation.forwardPorts = [

View file

@ -1,9 +1,16 @@
{ pkgs, modulesPath, ... }: {
{
lib,
modulesPath,
...
}:
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
let
inherit (lib) mkVMOverride;
in
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
fediversity = {
enable = true;
@ -11,22 +18,16 @@
pixelfed.enable = true;
};
networking.firewall.allowedTCPPorts = [ 80 ];
services.pixelfed = {
# TODO: secrets management!
secretFile = pkgs.writeText "secrets.env" ''
APP_KEY=adKK9EcY8Hcj3PLU7rzG9rJ6KKTOtYfA
'';
settings = {
OPEN_REGISTRATION = true;
FORCE_HTTPS_URLS = false;
};
# I feel like this should have an `enable` option and be configured via `services.nginx` rather than mirroring those options in services.pixelfed.nginx
# TODO: If that indeed makes sense, upstream it.
nginx = {
# locations."/public/".proxyPass = "${config.fediversity.internal.garage.web.urlFor "pixelfed"}/public/";
forceSSL = mkVMOverride false;
enableACME = mkVMOverride false;
};
};
virtualisation.memorySize = 2048;
virtualisation.forwardPorts = [
{