Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

20 changed files with 510 additions and 1132 deletions

1
.gitignore vendored
View file

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

View file

@ -46,26 +46,6 @@ 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

View file

@ -1,13 +0,0 @@
{ 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
'';
}

View file

@ -1,36 +0,0 @@
{ ... }:
{
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,11 +2,10 @@
let
inherit (builtins) toString;
inherit (lib) mkOption mkEnableOption mkForce;
inherit (lib) mkOption mkEnableOption;
inherit (lib.types) types;
in
{
in {
imports = [
./garage.nix
./mastodon.nix
@ -32,27 +31,9 @@ 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 = {
@ -83,17 +64,17 @@ in
type = types.str;
default = "web.garage.${config.fediversity.domain}";
};
internalPort = mkOption {
port = mkOption {
type = types.int;
default = 3902;
};
domainForBucket = mkOption {
type = types.functionTo types.str;
default = bucket: "${bucket}.${config.fediversity.internal.garage.web.rootDomain}";
rootDomainAndPort = mkOption {
type = types.str;
default = "${config.fediversity.internal.garage.web.rootDomain}:${toString config.fediversity.internal.garage.web.port}";
};
urlForBucket = mkOption {
urlFor = mkOption {
type = types.functionTo types.str;
default = bucket: "http://${config.fediversity.internal.garage.web.domainForBucket bucket}";
default = bucket: "http://${bucket}.${config.fediversity.internal.garage.web.rootDomainAndPort}";
};
};
};
@ -108,7 +89,7 @@ in
};
mastodon.domain = mkOption {
type = types.str;
default = "mastodon.${config.fediversity.domain}";
default = "mastdodon.${config.fediversity.domain}";
};
peertube.domain = mkOption {
type = types.str;
@ -119,19 +100,4 @@ 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,49 +8,25 @@ 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}
@ -59,43 +35,26 @@ 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 ${fedicfg.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
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}
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,
}:
''
## 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}: ''
garage key import --yes -n ${escapeShellArg key} ${escapeShellArg id} ${escapeShellArg secret}
${concatMapAttrs (ensureAccessScriptFn key) ensureAccess}
'';
ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg.ensureKeys;
in
@ -104,88 +63,94 @@ 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 = [ ];
};
allowedMethods = mkOption {
type = types.listOf types.str;
default = [ ];
};
allowedOrigins = mkOption {
type = types.listOf types.str;
default = [ ];
};
};
aliases = 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 = [ ];
default = [];
};
allowedMethods = mkOption {
type = types.listOf types.str;
default = [];
};
allowedOrigins = mkOption {
type = types.listOf types.str;
default = [];
};
};
}
);
default = { };
aliases = mkOption {
type = types.listOf types.str;
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;
};
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 = [ ];
};
type = types.attrsOf (types.submodule {
# TODO: these should be managed as secrets, not in the nix store
options = {
id = mkOption {
type = types.str;
};
}
);
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 = {};
};
};
};
config = lib.mkIf config.fediversity.enable {
environment.systemPackages = [
pkgs.minio-client
pkgs.awscli
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 ];
networking.firewall.allowedTCPPorts = [
fedicfg.rpc.port
config.fediversity.internal.garage.rpc.port
config.fediversity.internal.garage.web.port
];
services.garage = {
enable = true;
@ -195,59 +160,30 @@ 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 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}";
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}";
index = "index.html";
s3_api.s3_region = "garage";
s3_api.root_domain = ".${fedicfg.api.domain}";
s3_api.root_domain = ".${config.fediversity.internal.garage.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 ${fedicfg.api.url}; do sleep 1; done
until ${pkgs.curl}/bin/curl -sio /dev/null ${config.fediversity.internal.garage.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)
@ -261,8 +197,7 @@ 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
# 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} || :
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,14 +5,10 @@ let
};
in
{
config,
lib,
...
}:
{ config, lib, pkgs, ... }:
lib.mkIf (config.fediversity.enable && config.fediversity.mastodon.enable) {
#### garage setup
#### garage setup
services.garage = {
ensureBuckets = {
mastodon = {
@ -50,7 +46,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.rootDomain;
S3_HOSTNAME = config.fediversity.internal.garage.web.rootDomainAndPort;
# 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/
@ -61,11 +57,8 @@ 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
];
# open up access to the mastodon web interface
networking.firewall.allowedTCPPorts = [ 443 ];
services.mastodon = {
enable = true;
@ -73,10 +66,6 @@ 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,17 +5,10 @@ let
};
in
{
config,
lib,
...
}:
{ config, lib, pkgs, ... }:
lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
networking.firewall.allowedTCPPorts = [
80
443
];
networking.firewall.allowedTCPPorts = [ 80 9000 ];
services.garage = {
ensureBuckets = {
@ -29,7 +22,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 = {
@ -66,8 +59,7 @@ 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;
secrets.secretsFile = config.fediversity.temp.peertubeSecretsFile;
configureNginx = true;
settings = {
object_storage = {
@ -75,24 +67,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.urlForBucket bucket_name;
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
};
videos = rec {
bucket_name = "peertube-videos";
prefix = "";
base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
};
streaming_playlists = rec {
bucket_name = "peertube-playlists";
prefix = "";
base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
base_url = config.fediversity.internal.garage.web.urlFor bucket_name;
};
};
};
@ -102,12 +94,4 @@ 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,12 +5,7 @@ let
};
in
{
config,
lib,
pkgs,
...
}:
{ config, lib, pkgs, ... }:
lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
services.garage = {
@ -43,37 +38,16 @@ 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.urlForBucket "pixelfed";
AWS_URL = config.fediversity.internal.garage.web.urlFor "pixelfed";
AWS_BUCKET = "pixelfed";
AWS_ENDPOINT = config.fediversity.internal.garage.api.url;
AWS_USE_PATH_STYLE_ENDPOINT = false;
@ -85,8 +59,7 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
after = [ "ensure-garage.service" ];
};
networking.firewall.allowedTCPPorts = [
80
443
];
services.pixelfed.package = pkgs.pixelfed.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ ./pixelfed-group-permissions.patch ];
});
}

View file

@ -1,151 +1,12 @@
{
"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": 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=",
"lastModified": 1723726852,
"narHash": "sha256-lRzlx4fPRtzA+dgz9Rh4WK5yAW3TsAXx335DQqxY2XY=",
"owner": "radvendii",
"repo": "nixpkgs",
"rev": "df815998652a1d00ce7c059a1e5ef7d7c0548c90",
"rev": "9286249a1673cf5b14a4793e22dd44b70cb69a0d",
"type": "github"
},
"original": {
@ -155,30 +16,9 @@
"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": {
"disko": "disko",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_3",
"nixpkgs-latest": "nixpkgs-latest",
"pixelfed": "pixelfed"
"nixpkgs": "nixpkgs"
}
}
},

173
flake.nix
View file

@ -1,143 +1,64 @@
{
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,
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;
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
## 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;
nixosModules = {
## Fediversity modules
fediversity = import ./fediversity;
disk-layout = import ./disk-layout.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 ];
};
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
];
};
peertube = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm peertube-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;
};
};
pixelfed = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm pixelfed-vm ];
};
devShells.${system}.default = pkgs.mkShell {
inputs = with pkgs; [
nil
all = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [
fediversity
interactive-vm
peertube-vm
pixelfed-vm
mastodon-vm
];
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
];
};
};
}

View file

@ -1,61 +0,0 @@
/**
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,153 +1,139 @@
{ 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
## FIXME: this binding was not used, but maybe we want a side-effect or something?
# rebuildableTest = import ./rebuildableTest.nix pkgs;
print(1)
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
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
print(1)
driver = webdriver.Firefox(options=options, service=service)
driver.get("http://mastodon.localhost:55001/public/local")
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
# wait until the statuses load
WebDriverWait(driver, 90).until(
lambda x: x.find_element(By.CLASS_NAME, "status"))
driver = webdriver.Firefox(options=options, service=service)
driver.get("http://mastodon.localhost:55001/public/local")
driver.save_screenshot("/mastodon-screenshot.png")
# 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()
'';
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; [ 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.
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.")
# 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,9 +1,7 @@
{ pkgs, self }:
let
lib = pkgs.lib;
## FIXME: this binding was not used but maybe we want a side effect or something?
# rebuildableTest = import ./rebuildableTest.nix pkgs;
rebuildableTest = import ./rebuildableTest.nix pkgs;
email = "test@test.com";
password = "testtest";
@ -52,176 +50,159 @@ 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;
};
displayManager.autoLogin = {
enable = true;
user = "selenium";
};
};
virtualisation.resolution = {
x = 1680;
y = 1050;
services = {
xserver = {
enable = true;
displayManager.lightdm.enable = true;
desktopManager.lxqt.enable = true;
};
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;
displayManager.autoLogin = {
enable = true;
user = "selenium";
};
};
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.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.urlFor "pixelfed"}"):
raise Exception("image does not come from garage")
'';
}

View file

@ -1,42 +1,32 @@
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 *
@ -60,11 +50,10 @@ 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() {
@ -124,40 +113,37 @@ 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,44 +0,0 @@
{
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,6 +1,5 @@
# customize nixos-rebuild build-vm to be a bit more convenient
{ pkgs, ... }:
{
{ pkgs, ... }: {
# let us log in
users.mutableUsers = false;
users.users.root.hashedPassword = "";
@ -35,10 +34,7 @@
# no graphics. see nixos-shell
virtualisation = {
graphics = false;
qemu.consoles = [
"tty0"
"hvc0"
];
qemu.consoles = [ "tty0" "hvc0" ];
qemu.options = [
"-serial null"
"-device virtio-serial"
@ -48,19 +44,12 @@
];
};
# 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,12 +1,9 @@
{
modulesPath,
lib,
config,
...
}:
{
{ modulesPath, lib, config, ... }: {
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
config = lib.mkMerge [
{
@ -14,17 +11,19 @@
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 = lib.mkVMOverride {
security.acme = {
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,8 +1,9 @@
{ modulesPath, ... }:
{ pkgs, modulesPath, ... }: {
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
services.peertube = {
enableWebHttps = false;
@ -10,6 +11,10 @@
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,16 +1,9 @@
{
lib,
modulesPath,
...
}:
{ pkgs, modulesPath, ... }: {
let
inherit (lib) mkVMOverride;
in
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
imports = [
../fediversity
(modulesPath + "/virtualisation/qemu-vm.nix")
];
fediversity = {
enable = true;
@ -18,16 +11,22 @@ in
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 = {
forceSSL = mkVMOverride false;
enableACME = mkVMOverride false;
# locations."/public/".proxyPass = "${config.fediversity.internal.garage.web.urlFor "pixelfed"}/public/";
};
};
virtualisation.memorySize = 2048;
virtualisation.forwardPorts = [
{