Compare commits

..

51 commits

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 1133 additions and 534 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ result*
output output
todo 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 ```bash
pixelfed-manage user:create --name=test --username=test --email=test@test.com --password=testtest --confirm_email=1 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 ## 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 let
inherit (builtins) toString; inherit (builtins) toString;
inherit (lib) mkOption mkEnableOption; inherit (lib) mkOption mkEnableOption mkForce;
inherit (lib.types) types; inherit (lib.types) types;
in { in
{
imports = [ imports = [
./garage.nix ./garage.nix
./mastodon.nix ./mastodon.nix
@ -31,9 +32,27 @@ in {
pixelfed.enable = mkEnableOption "default Fediversity Pixelfed configuration"; pixelfed.enable = mkEnableOption "default Fediversity Pixelfed configuration";
peertube.enable = mkEnableOption "default Fediversity PeerTube 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 { internal = mkOption {
description = "options that are only meant to be used internally; change at your own risk"; description = "options that are only meant to be used internally; change at your own risk";
default = {}; default = { };
type = types.submodule { type = types.submodule {
options = { options = {
garage = { garage = {
@ -64,17 +83,17 @@ in {
type = types.str; type = types.str;
default = "web.garage.${config.fediversity.domain}"; default = "web.garage.${config.fediversity.domain}";
}; };
port = mkOption { internalPort = mkOption {
type = types.int; type = types.int;
default = 3902; default = 3902;
}; };
rootDomainAndPort = mkOption { domainForBucket = mkOption {
type = types.str;
default = "${config.fediversity.internal.garage.web.rootDomain}:${toString config.fediversity.internal.garage.web.port}";
};
urlFor = mkOption {
type = types.functionTo types.str; 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 { mastodon.domain = mkOption {
type = types.str; type = types.str;
default = "mastdodon.${config.fediversity.domain}"; default = "mastodon.${config.fediversity.domain}";
}; };
peertube.domain = mkOption { peertube.domain = mkOption {
type = types.str; 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 in
# TODO: expand to a multi-machine setup # TODO: expand to a multi-machine setup
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
inherit (builtins) toString; inherit (builtins) toString;
inherit (lib) types mkOption mkEnableOption optionalString concatStringsSep; inherit (lib)
types
mkOption
mkEnableOption
optionalString
concatStringsSep
;
inherit (lib.strings) escapeShellArg; inherit (lib.strings) escapeShellArg;
inherit (lib.attrsets) filterAttrs mapAttrs';
cfg = config.services.garage; cfg = config.services.garage;
fedicfg = config.fediversity.internal.garage;
concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset); concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset);
ensureBucketScriptFn = bucket: { website, aliases, corsRules }: ensureBucketScriptFn =
bucket:
{
website,
aliases,
corsRules,
}:
let let
bucketArg = escapeShellArg bucket; bucketArg = escapeShellArg bucket;
corsRulesJSON = escapeShellArg (builtins.toJSON { corsRulesJSON = escapeShellArg (
CORSRules = [{ builtins.toJSON {
AllowedHeaders = corsRules.allowedHeaders; CORSRules = [
AllowedMethods = corsRules.allowedMethods; {
AllowedOrigins = corsRules.allowedOrigins; AllowedHeaders = corsRules.allowedHeaders;
}]; AllowedMethods = corsRules.allowedMethods;
}); AllowedOrigins = corsRules.allowedOrigins;
in '' }
];
}
);
in
''
# garage bucket info tells us if the bucket already exists # garage bucket info tells us if the bucket already exists
garage bucket info ${bucketArg} || garage bucket create ${bucketArg} garage bucket info ${bucketArg} || garage bucket create ${bucketArg}
@ -35,26 +59,43 @@ let
garage bucket website --allow ${bucketArg} garage bucket website --allow ${bucketArg}
''} ''}
${concatStringsSep "\n" (map (alias: '' ${concatStringsSep "\n" (
garage bucket alias ${bucketArg} ${escapeShellArg alias} map (alias: ''
'') aliases)} garage bucket alias ${bucketArg} ${escapeShellArg alias}
'') aliases
)}
${optionalString corsRules.enable '' ${optionalString corsRules.enable ''
garage bucket allow --read --write --owner ${bucketArg} --key tmp garage bucket allow --read --write --owner ${bucketArg} --key tmp
# TODO: endpoin-url should not be hard-coded # 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 garage bucket deny --read --write --owner ${bucketArg} --key tmp
''} ''}
''; '';
ensureBucketsScript = concatMapAttrs ensureBucketScriptFn cfg.ensureBuckets; ensureBucketsScript = concatMapAttrs ensureBucketScriptFn cfg.ensureBuckets;
ensureAccessScriptFn = key: bucket: { read, write, owner }: '' ensureAccessScriptFn =
garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ key: bucket:
${escapeShellArg bucket} --key ${escapeShellArg key} {
''; read,
ensureKeyScriptFn = key: {id, secret, ensureAccess}: '' write,
garage key import --yes -n ${escapeShellArg key} ${escapeShellArg id} ${escapeShellArg secret} owner,
${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} }:
''; ''
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; ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg.ensureKeys;
in in
@ -63,94 +104,88 @@ in
options = { options = {
services.garage = { services.garage = {
ensureBuckets = mkOption { ensureBuckets = mkOption {
type = types.attrsOf (types.submodule { type = types.attrsOf (
options = { types.submodule {
website = mkOption { options = {
type = types.bool; website = mkOption {
default = false; 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 { # I think setting corsRules should allow another website to show images from your bucket
type = types.listOf types.str; corsRules = {
default = []; 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; type = types.listOf types.str;
default = []; default = [ ];
}; };
}; };
aliases = mkOption { }
type = types.listOf types.str; );
default = []; default = { };
};
};
});
default = {};
}; };
ensureKeys = mkOption { ensureKeys = mkOption {
type = types.attrsOf (types.submodule { type = types.attrsOf (
# TODO: these should be managed as secrets, not in the nix store types.submodule {
options = { # TODO: these should be managed as secrets, not in the nix store
id = mkOption { options = {
type = types.str; 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; );
}; default = { };
# 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 { config = lib.mkIf config.fediversity.enable {
virtualisation.diskSize = 2048; environment.systemPackages = [
virtualisation.forwardPorts = [ pkgs.minio-client
{ pkgs.awscli
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 = [ networking.firewall.allowedTCPPorts = [
config.fediversity.internal.garage.rpc.port fedicfg.rpc.port
config.fediversity.internal.garage.web.port
]; ];
services.garage = { services.garage = {
enable = true; enable = true;
@ -160,30 +195,59 @@ in
# TODO: use a secret file # TODO: use a secret file
rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625"; rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625";
# TODO: why does this have to be set? is there not a sensible default? # 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_bind_addr = "[::]:${toString fedicfg.rpc.port}";
rpc_public_addr = "[::1]:${toString config.fediversity.internal.garage.rpc.port}"; rpc_public_addr = "[::1]:${toString fedicfg.rpc.port}";
s3_api.api_bind_addr = "[::]:${toString config.fediversity.internal.garage.api.port}"; s3_api.api_bind_addr = "[::]:${toString fedicfg.api.port}";
s3_web.bind_addr = "[::]:${toString config.fediversity.internal.garage.web.port}"; s3_web.bind_addr = "[::]:${toString fedicfg.web.internalPort}";
s3_web.root_domain = ".${config.fediversity.internal.garage.web.rootDomain}"; s3_web.root_domain = ".${fedicfg.web.rootDomain}";
index = "index.html"; index = "index.html";
s3_api.s3_region = "garage"; 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 = { systemd.services.ensure-garage = {
after = [ "garage.service" ]; after = [ "garage.service" ];
wantedBy = [ "garage.service" ]; wantedBy = [ "garage.service" ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
}; };
path = [ cfg.package pkgs.perl pkgs.awscli ]; path = [
cfg.package
pkgs.perl
pkgs.awscli
];
script = '' script = ''
set -xeuo pipefail set -xeuo pipefail
# Give Garage time to start up by waiting until somethings speaks HTTP # Give Garage time to start up by waiting until somethings speaks HTTP
# behind Garage's API URL. # 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 # XXX: this is very sensitive to being a single instance
# (doing the bare minimum to get garage up and running) # (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 # 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: 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_ACCESS_KEY_ID=${snakeoil_key.id};
export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}; export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret};

View file

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

View file

@ -5,10 +5,17 @@ let
}; };
in in
{ config, lib, pkgs, ... }: {
config,
lib,
...
}:
lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
networking.firewall.allowedTCPPorts = [ 80 9000 ]; networking.firewall.allowedTCPPorts = [
80
443
];
services.garage = { services.garage = {
ensureBuckets = { ensureBuckets = {
@ -22,7 +29,7 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
allowedOrigins = [ "*" ]; 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 = { peertube-playlists = {
website = true; website = true;
corsRules = { 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. # TODO: in most of nixpkgs, these are true by default. upstream that unless there's a good reason not to.
redis.createLocally = true; redis.createLocally = true;
database.createLocally = true; database.createLocally = true;
configureNginx = true;
secrets.secretsFile = config.fediversity.temp.peertubeSecretsFile;
settings = { settings = {
object_storage = { object_storage = {
@ -67,24 +75,24 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) {
endpoint = config.fediversity.internal.garage.api.url; endpoint = config.fediversity.internal.garage.api.url;
region = "garage"; region = "garage";
# not supported by garage # not supported by garage
# SEE: https://garagehq.deuxfleurs.fr/documentation/connect/apps/#peertube # SEE: https://garagehq.deuxfleurs.fr/documentation/connect/apps/#peertube
proxy.proxyify_private_files = false; proxy.proxyify_private_files = false;
web_videos = rec { web_videos = rec {
bucket_name = "peertube-videos"; bucket_name = "peertube-videos";
prefix = ""; prefix = "";
base_url = config.fediversity.internal.garage.web.urlFor bucket_name; base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
}; };
videos = rec { videos = rec {
bucket_name = "peertube-videos"; bucket_name = "peertube-videos";
prefix = ""; prefix = "";
base_url = config.fediversity.internal.garage.web.urlFor bucket_name; base_url = config.fediversity.internal.garage.web.urlForBucket bucket_name;
}; };
streaming_playlists = rec { streaming_playlists = rec {
bucket_name = "peertube-playlists"; bucket_name = "peertube-playlists";
prefix = ""; 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_ACCESS_KEY_ID=${snakeoil_key.id}
AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret} 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 in
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) { lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
services.garage = { services.garage = {
@ -38,16 +43,37 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
services.pixelfed = { services.pixelfed = {
enable = true; enable = true;
domain = config.fediversity.internal.pixelfed.domain; 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 = { 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"; # DANGEROUSLY_SET_FILESYSTEM_DRIVER = "s3";
FILESYSTEM_CLOUD = "s3"; FILESYSTEM_CLOUD = "s3";
PF_ENABLE_CLOUD = true; PF_ENABLE_CLOUD = true;
AWS_ACCESS_KEY_ID = snakeoil_key.id; AWS_ACCESS_KEY_ID = snakeoil_key.id;
AWS_SECRET_ACCESS_KEY = snakeoil_key.secret; AWS_SECRET_ACCESS_KEY = snakeoil_key.secret;
AWS_DEFAULT_REGION = "garage"; 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_BUCKET = "pixelfed";
AWS_ENDPOINT = config.fediversity.internal.garage.api.url; AWS_ENDPOINT = config.fediversity.internal.garage.api.url;
AWS_USE_PATH_STYLE_ENDPOINT = false; AWS_USE_PATH_STYLE_ENDPOINT = false;
@ -59,4 +85,8 @@ lib.mkIf (config.fediversity.enable && config.fediversity.pixelfed.enable) {
after = [ "ensure-garage.service" ]; after = [ "ensure-garage.service" ];
}; };
networking.firewall.allowedTCPPorts = [
80
443
];
} }

View file

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

206
flake.nix
View file

@ -1,93 +1,143 @@
{ {
description = "Testing mastodon configurations";
inputs = { inputs = {
nixpkgs.url = "github:radvendii/nixpkgs/nixos_rebuild_tests"; 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 = inputs@{ self, nixpkgs }: outputs =
let {
system = "x86_64-linux"; self,
pkgs = nixpkgs.legacyPackages.${system}; nixpkgs,
in { 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;
packages.${system} = { ## VM-specific modules
pixelfed = pkgs.pixelfed.overrideAttrs (old: { interactive-vm = import ./vm/interactive-vm.nix;
patches = (old.patches or [ ]) ++ [ ./fediversity/pixelfed-group-permissions.patch ]; 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 = { disk-layout = import ./disk-layout.nix;
## Fediversity modules
fediversity = { pkgs, ... }: {
imports = [ ./fediversity ];
services.pixelfed.package = self.packages.${pkgs.stdenv.hostPlatform.system}.pixelfed;
}; };
## VM-specific modules nixosConfigurations = {
interactive-vm = { mastodon = nixpkgs.lib.nixosSystem {
imports = [ inherit system;
./vm/interactive-vm.nix modules = with self.nixosModules; [
self.nixosModules.fediversity 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
];
};
}; };
mastodon-vm = {
imports = [ ## Fully-feature ISO installer
./vm/mastodon-vm.nix mkInstaller = import ./installer.nix;
self.nixosModules.fediversity 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;
};
};
}; };
peertube-vm = {
imports = [ devShells.${system}.default = pkgs.mkShell {
./vm/peertube-vm.nix inputs = with pkgs; [
self.nixosModules.fediversity nil
];
};
pixelfed-vm = {
imports = [
./vm/pixelfed-vm.nix
self.nixosModules.fediversity
]; ];
shellHook = self.checks.${system}.pre-commit.shellHook;
}; };
}; };
nixosConfigurations = {
mastodon = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm mastodon-vm ];
};
peertube = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm peertube-vm ];
};
pixelfed = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [ fediversity interactive-vm pixelfed-vm ];
};
all = nixpkgs.lib.nixosSystem {
inherit system;
modules = with self.nixosModules; [
fediversity
interactive-vm
peertube-vm
pixelfed-vm
mastodon-vm
];
};
};
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 }: { pkgs, self }:
let let
lib = pkgs.lib; 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() seleniumScript =
options.add_argument("--headless") pkgs.writers.writePython3Bin "selenium-script"
# devtools don't show up in headless screenshots {
# options.add_argument("-devtools") libraries = with pkgs.python3Packages; [ selenium ];
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501 }
''
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) print(1)
driver.get("http://mastodon.localhost:55001/public/local")
# wait until the statuses load options = Options()
WebDriverWait(driver, 90).until( options.add_argument("--headless")
lambda x: x.find_element(By.CLASS_NAME, "status")) # 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 in
pkgs.nixosTest { pkgs.nixosTest {
name = "test-mastodon-garage"; name = "test-mastodon-garage";
nodes = { nodes = {
server = { config, ... }: { server =
virtualisation.memorySize = lib.mkVMOverride 4096; { config, ... }:
imports = with self.nixosModules; [ mastodon-vm ]; {
# TODO: pair down virtualisation.memorySize = lib.mkVMOverride 4096;
environment.systemPackages = with pkgs; [ imports = with self.nixosModules; [
python3 bleedingFediverse
firefox-unwrapped fediversity
geckodriver garage-vm
toot mastodon-vm
xh ];
seleniumScript # TODO: pair down
helix environment.systemPackages = with pkgs; [
imagemagick python3
]; firefox-unwrapped
environment.variables = { geckodriver
POST_MEDIA = ./green.png; toot
AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.mastodon.id; xh
AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.mastodon.secret; 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, ... }: '' testScript =
import re { nodes, ... }:
import time ''
import re
import time
server.start() server.start()
with subtest("Mastodon starts"): with subtest("Mastodon starts"):
server.wait_for_unit("mastodon-web.service") server.wait_for_unit("mastodon-web.service")
# make sure mastodon is fully up and running before we interact with it # make sure mastodon is fully up and running before we interact with it
# TODO: is there a way to test for this? # TODO: is there a way to test for this?
time.sleep(180) time.sleep(180)
with subtest("Account creation"): with subtest("Account creation"):
account_creation_output = server.succeed("mastodon-tootctl accounts create test --email test@test.com --confirmed --approve") 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) password_match = re.match('.*New password: ([^\n]*).*', account_creation_output, re.S)
if password_match is None: if password_match is None:
raise Exception(f"account creation did not generate a password.\n{account_creation_output}") raise Exception(f"account creation did not generate a password.\n{account_creation_output}")
password = password_match.group(1) password = password_match.group(1)
with subtest("TTY Login"): with subtest("TTY Login"):
server.wait_until_tty_matches("1", "login: ") server.wait_until_tty_matches("1", "login: ")
server.send_chars("root\n"); server.send_chars("root\n");
with subtest("Log in with toot"): 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 # 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.send_chars("toot login_cli --instance http://mastodon.localhost:55001 --email test@test.com\n")
server.wait_until_tty_matches("1", "Password: ") server.wait_until_tty_matches("1", "Password: ")
server.send_chars(password + "\n") server.send_chars(password + "\n")
server.wait_until_tty_matches("1", "Successfully logged in.") server.wait_until_tty_matches("1", "Successfully logged in.")
with subtest("post text"): with subtest("post text"):
server.succeed("echo 'hello mastodon' | toot post") server.succeed("echo 'hello mastodon' | toot post")
with subtest("post image"): with subtest("post image"):
server.succeed("toot post --media $POST_MEDIA") server.succeed("toot post --media $POST_MEDIA")
with subtest("access garage"): 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 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") server.succeed("mc ls garage/mastodon")
with subtest("access image in garage"): with subtest("access image in garage"):
image = server.succeed("mc find garage --regex original") image = server.succeed("mc find garage --regex original")
image = image.rstrip() image = image.rstrip()
if image == "": if image == "":
raise Exception("image posted to mastodon did not get stored in garage") raise Exception("image posted to mastodon did not get stored in garage")
server.succeed(f"mc cat {image} >/garage-image.webp") server.succeed(f"mc cat {image} >/garage-image.webp")
garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.webp") garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.webp")
image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA")
if garage_image_hash != image_hash: if garage_image_hash != image_hash:
raise Exception("image stored in garage did not match image uploaded") raise Exception("image stored in garage did not match image uploaded")
with subtest("Content security policy allows garage images"): with subtest("Content security policy allows garage images"):
headers = server.succeed("xh -h http://mastodon.localhost:55001/public/local") headers = server.succeed("xh -h http://mastodon.localhost:55001/public/local")
csp_match = None csp_match = None
# I can't figure out re.MULTILINE # I can't figure out re.MULTILINE
for header in headers.split("\n"): for header in headers.split("\n"):
csp_match = re.match('^Content-Security-Policy: (.*)$', header) csp_match = re.match('^Content-Security-Policy: (.*)$', header)
if csp_match is not None: if csp_match is not None:
break break
if csp_match is None: if csp_match is None:
raise Exception("mastodon did not send a content security policy header") raise Exception("mastodon did not send a content security policy header")
csp = csp_match.group(1) csp = csp_match.group(1)
# the img-src content security policy should include the garage server # 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. ## 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:3902.*", csp) garage_csp = re.match(".*; img-src[^;]*web\.garage\.localhost.*", csp)
if garage_csp is None: 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.") 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. # this could in theory give a false positive if mastodon changes it's colorscheme to include pure green.
with subtest("image displays"): with subtest("image displays"):
server.succeed("selenium-script") server.succeed("selenium-script")
server.copy_from_vm("/mastodon-screenshot.png", "") server.copy_from_vm("/mastodon-screenshot.png", "")
displayed_colors = server.succeed("convert /mastodon-screenshot.png -define histogram:unique-colors=true -format %c histogram:info:") displayed_colors = server.succeed("convert /mastodon-screenshot.png -define histogram:unique-colors=true -format %c histogram:info:")
# check that the green image displayed somewhere # check that the green image displayed somewhere
green_check = re.match(".*#00FF00.*", displayed_colors, re.S) green_check = re.match(".*#00FF00.*", displayed_colors, re.S)
if green_check is None: if green_check is None:
raise Exception("cannot detect the uploaded image on mastodon page.") raise Exception("cannot detect the uploaded image on mastodon page.")
''; '';
} }

View file

@ -1,7 +1,9 @@
{ pkgs, self }: { pkgs, self }:
let let
lib = pkgs.lib; 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"; email = "test@test.com";
password = "testtest"; password = "testtest";
@ -50,159 +52,176 @@ let
driver.quit() driver.quit()
''; '';
seleniumScriptPostPicture = pkgs.writers.writePython3Bin "selenium-script-post-picture" seleniumScriptPostPicture =
{ pkgs.writers.writePython3Bin "selenium-script-post-picture"
libraries = with pkgs.python3Packages; [ selenium ]; {
} '' libraries = with pkgs.python3Packages; [ selenium ];
import os }
import time ''
${seleniumImports} import os
from selenium.webdriver.support.wait import WebDriverWait import time
${seleniumImports}
from selenium.webdriver.support.wait import WebDriverWait
${seleniumSetup} ${seleniumSetup}
${seleniumPixelfedLogin} ${seleniumPixelfedLogin}
time.sleep(3) 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. # Find the new post form, fill it in with our pictureand a caption.
print("Click on Create New Post...", file=sys.stderr) print("Click on Create New Post...", file=sys.stderr)
driver.find_element(By.LINK_TEXT, "Create New Post").click() driver.find_element(By.LINK_TEXT, "Create New Post").click()
print("Add file to input element...", file=sys.stderr) print("Add file to input element...", file=sys.stderr)
driver.find_element(By.XPATH, "//input[@type='file']").send_keys(media_path) driver.find_element(By.XPATH, "//input[@type='file']").send_keys(media_path)
print("Add a caption", file=sys.stderr) print("Add a caption", file=sys.stderr)
driver.find_element(By.CSS_SELECTOR, ".media-body textarea").send_keys( driver.find_element(By.CSS_SELECTOR, ".media-body textarea").send_keys(
"Fediversity test of image upload to pixelfed with garage storage." "Fediversity test of image upload to pixelfed with garage storage."
) )
time.sleep(3) time.sleep(3)
print("Click on Post button...", file=sys.stderr) print("Click on Post button...", file=sys.stderr)
driver.find_element(By.LINK_TEXT, "Post").click() driver.find_element(By.LINK_TEXT, "Post").click()
# Wait until the post loads, and in particular its picture, then take a # Wait until the post loads, and in particular its picture, then take a
# screenshot of the whole page. # screenshot of the whole page.
print("Wait for post and image to be loaded...", file=sys.stderr) print("Wait for post and image to be loaded...", file=sys.stderr)
img = driver.find_element( img = driver.find_element(
By.XPATH, By.XPATH,
"//div[@class='timeline-status-component-content']//img" "//div[@class='timeline-status-component-content']//img"
) )
WebDriverWait(driver, timeout=10).until( WebDriverWait(driver, timeout=10).until(
lambda d: d.execute_script("return arguments[0].complete", img) lambda d: d.execute_script("return arguments[0].complete", img)
) )
time.sleep(3) time.sleep(3)
${seleniumTakeScreenshot "\"/home/selenium/screenshot.png\""} ${seleniumTakeScreenshot "\"/home/selenium/screenshot.png\""}
${seleniumQuit}''; ${seleniumQuit}'';
seleniumScriptGetSrc = pkgs.writers.writePython3Bin "selenium-script-get-src" seleniumScriptGetSrc =
{ pkgs.writers.writePython3Bin "selenium-script-get-src"
libraries = with pkgs.python3Packages; [ selenium ]; {
} '' libraries = with pkgs.python3Packages; [ selenium ];
${seleniumImports} }
${seleniumSetup} ''
${seleniumPixelfedLogin} ${seleniumImports}
${seleniumSetup}
${seleniumPixelfedLogin}
img = driver.find_element( img = driver.find_element(
By.XPATH, By.XPATH,
"//div[@class='timeline-status-component-content']//img" "//div[@class='timeline-status-component-content']//img"
) )
# REVIEW: Need to wait for it to be loaded? # REVIEW: Need to wait for it to be loaded?
print(img.get_attribute('src')) print(img.get_attribute('src'))
${seleniumQuit}''; ${seleniumQuit}'';
in in
pkgs.nixosTest { pkgs.nixosTest {
name = "test-pixelfed-garage"; name = "test-pixelfed-garage";
nodes = { nodes = {
server = { config, ... }: { server =
{ config, ... }:
{
services = { services = {
xserver = { xserver = {
enable = true; enable = true;
displayManager.lightdm.enable = true; displayManager.lightdm.enable = true;
desktopManager.lxqt.enable = true; desktopManager.lxqt.enable = true;
};
displayManager.autoLogin = {
enable = true;
user = "selenium";
};
};
virtualisation.resolution = {
x = 1680;
y = 1050;
}; };
displayManager.autoLogin = { virtualisation = {
enable = true; memorySize = lib.mkVMOverride 8192;
user = "selenium"; 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, ... }: '' testScript =
import re { nodes, ... }:
''
import re
server.start() server.start()
with subtest("Pixelfed starts"): with subtest("Pixelfed starts"):
server.wait_for_unit("phpfpm-pixelfed.service") server.wait_for_unit("phpfpm-pixelfed.service")
with subtest("Account creation"): with subtest("Account creation"):
server.succeed("pixelfed-manage user:create --name=test --username=test --email=${email} --password=${password} --confirm_email=1") 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 # 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). # 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 # 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. # there, then post a green image and check that the green pixel IS there.
with subtest("Image displays"): with subtest("Image displays"):
server.succeed("su - selenium -c 'selenium-script-post-picture ${email} ${password}'") server.succeed("su - selenium -c 'selenium-script-post-picture ${email} ${password}'")
server.copy_from_vm("/home/selenium/screenshot.png", "") 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:") 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 # check that the green image displayed somewhere
image_check = re.match(".*#FF0500.*", displayed_colors, re.S) image_check = re.match(".*#FF0500.*", displayed_colors, re.S)
if image_check is None: if image_check is None:
raise Exception("cannot detect the uploaded image on pixelfed page.") raise Exception("cannot detect the uploaded image on pixelfed page.")
with subtest("access garage"): 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 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") server.succeed("mc ls garage/pixelfed")
with subtest("access image in garage"): with subtest("access image in garage"):
image = server.succeed("mc find garage --regex '\\.png' --ignore '*_thumb.png'") image = server.succeed("mc find garage --regex '\\.png' --ignore '*_thumb.png'")
image = image.rstrip() image = image.rstrip()
if image == "": if image == "":
raise Exception("image posted to Pixelfed did not get stored in garage") raise Exception("image posted to Pixelfed did not get stored in garage")
server.succeed(f"mc cat {image} >/garage-image.png") server.succeed(f"mc cat {image} >/garage-image.png")
garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.png") garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.png")
image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA") image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA")
if garage_image_hash != image_hash: if garage_image_hash != image_hash:
raise Exception("image stored in garage did not match image uploaded") raise Exception("image stored in garage did not match image uploaded")
with subtest("Check that image comes from garage"): with subtest("Check that image comes from garage"):
src = server.succeed("su - selenium -c 'selenium-script-get-src ${email} ${password}'") src = server.succeed("su - selenium -c 'selenium-script-get-src ${email} ${password}'")
if not src.startswith("${nodes.server.fediversity.internal.garage.web.urlFor "pixelfed"}"): if not src.startswith("${nodes.server.fediversity.internal.garage.web.urlForBucket "pixelfed"}"):
raise Exception("image does not come from garage") raise Exception("image does not come from garage")
''; '';
} }

View file

@ -1,32 +1,42 @@
pkgs: test: pkgs: test:
let let
inherit (pkgs.lib) mapAttrsToList concatStringsSep genAttrs mkIf; inherit (pkgs.lib)
mapAttrsToList
concatStringsSep
genAttrs
mkIf
;
inherit (builtins) attrNames; inherit (builtins) attrNames;
interactiveConfig = ({ config, ... }: { interactiveConfig = (
# so we can run `nix shell nixpkgs#foo` on the machines { config, ... }:
nix.extraOptions = '' {
extra-experimental-features = nix-command flakes # 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 # so we can ssh in and rebuild them
services.openssh = { services.openssh = {
enable = true; enable = true;
settings = { settings = {
PermitRootLogin = "yes"; PermitRootLogin = "yes";
PermitEmptyPasswords = "yes"; PermitEmptyPasswords = "yes";
UsePAM = false; UsePAM = false;
};
}; };
};
virtualisation = mkIf (config.networking.hostName == "jumphost") { virtualisation = mkIf (config.networking.hostName == "jumphost") {
forwardPorts = [{ forwardPorts = [
from = "host"; {
host.port = 2222; from = "host";
guest.port = 22; host.port = 2222;
}]; guest.port = 22;
}; }
}); ];
};
}
);
sshConfig = pkgs.writeText "ssh-config" '' sshConfig = pkgs.writeText "ssh-config" ''
Host * Host *
@ -50,10 +60,11 @@ let
# create an association array from machine names to the path to their # create an association array from machine names to the path to their
# configuration in the nix store # configuration in the nix store
declare -A configPaths=(${ declare -A configPaths=(${
concatStringsSep " " concatStringsSep " " (
(mapAttrsToList mapAttrsToList (
(n: v: ''["${n}"]="${v.system.build.toplevel}"'') n: v: ''["${n}"]="${v.system.build.toplevel}"''
rebuildableTest.driverInteractive.nodes) ) rebuildableTest.driverInteractive.nodes
)
}) })
rebuild_one() { rebuild_one() {
@ -113,37 +124,40 @@ let
# we're at it) # we're at it)
rebuildableTest = rebuildableTest =
let let
preOverride = pkgs.nixosTest (test // { preOverride = pkgs.nixosTest (
interactive = (test.interactive or { }) // { test
# no need to // with test.interactive.nodes here, since we are iterating // {
# over all of them, and adding back in the config via `imports` interactive = (test.interactive or { }) // {
nodes = genAttrs # no need to // with test.interactive.nodes here, since we are iterating
( # over all of them, and adding back in the config via `imports`
attrNames test.nodes or { } ++ nodes =
attrNames test.interactive.nodes or { } ++ genAttrs (attrNames test.nodes or { } ++ attrNames test.interactive.nodes or { } ++ [ "jumphost" ])
[ "jumphost" ] (n: {
) imports = [
(n: { (test.interactive.${n} or { })
imports = [ interactiveConfig
(test.interactive.${n} or { }) ];
interactiveConfig });
]; };
}); # override with test.passthru in case someone wants to overwrite us.
}; passthru = {
# override with test.passthru in case someone wants to overwrite us. inherit rebuildScript sshConfig;
passthru = { inherit rebuildScript sshConfig; } // (test.passthru or { }); } // (test.passthru or { });
}); }
);
in in
preOverride // { preOverride
// {
driverInteractive = preOverride.driverInteractive.overrideAttrs (old: { driverInteractive = preOverride.driverInteractive.overrideAttrs (old: {
# this comes from runCommand, not mkDerivation, so this is the only # this comes from runCommand, not mkDerivation, so this is the only
# hook we have to override # hook we have to override
buildCommand = old.buildCommand + '' buildCommand =
ln -s ${sshConfig} $out/ssh-config old.buildCommand
ln -s ${rebuildScript}/bin/rebuild $out/bin/rebuild + ''
''; ln -s ${sshConfig} $out/ssh-config
ln -s ${rebuildScript}/bin/rebuild $out/bin/rebuild
'';
}); });
}; };
in in
rebuildableTest 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 # customize nixos-rebuild build-vm to be a bit more convenient
{ pkgs, ... }: { { pkgs, ... }:
{
# let us log in # let us log in
users.mutableUsers = false; users.mutableUsers = false;
users.users.root.hashedPassword = ""; users.users.root.hashedPassword = "";
@ -34,7 +35,10 @@
# no graphics. see nixos-shell # no graphics. see nixos-shell
virtualisation = { virtualisation = {
graphics = false; graphics = false;
qemu.consoles = [ "tty0" "hvc0" ]; qemu.consoles = [
"tty0"
"hvc0"
];
qemu.options = [ qemu.options = [
"-serial null" "-serial null"
"-device virtio-serial" "-device virtio-serial"
@ -44,12 +48,19 @@
]; ];
}; };
# we can't forward port 80 or 443, so let's run nginx on a different port # 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.defaultSSLListenPort = 8443;
services.nginx.defaultHTTPListenPort = 8080; services.nginx.defaultHTTPListenPort = 8080;
virtualisation.forwardPorts = [ virtualisation.forwardPorts = [
{
from = "host";
host.port = 22222;
guest.port = 22;
}
{ {
from = "host"; from = "host";
host.port = 8080; host.port = 8080;

View file

@ -1,8 +1,12 @@
{ modulesPath, lib, config, ... }: { {
modulesPath,
lib,
config,
...
}:
{
imports = [ imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
(modulesPath + "/virtualisation/qemu-vm.nix")
];
config = lib.mkMerge [ config = lib.mkMerge [
{ {
@ -10,19 +14,17 @@
enable = true; enable = true;
domain = "localhost"; domain = "localhost";
mastodon.enable = true; mastodon.enable = true;
temp.cores = config.virtualisation.cores;
}; };
services.mastodon = { services.mastodon = {
extraConfig = { extraConfig = {
EMAIL_DOMAIN_ALLOWLIST = "example.com"; 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 = { defaults = {
# invalid server; the systemd service will fail, and we won't get # invalid server; the systemd service will fail, and we won't get
# properly signed certificates. but let's not spam the letsencrypt # properly signed certificates. but let's not spam the letsencrypt

View file

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

View file

@ -1,8 +1,16 @@
{ pkgs, modulesPath, ... }: { {
lib,
modulesPath,
...
}:
imports = [ let
(modulesPath + "/virtualisation/qemu-vm.nix") inherit (lib) mkVMOverride;
];
in
{
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
fediversity = { fediversity = {
enable = true; enable = true;
@ -10,22 +18,16 @@
pixelfed.enable = true; pixelfed.enable = true;
}; };
networking.firewall.allowedTCPPorts = [ 80 ];
services.pixelfed = { services.pixelfed = {
# TODO: secrets management!
secretFile = pkgs.writeText "secrets.env" ''
APP_KEY=adKK9EcY8Hcj3PLU7rzG9rJ6KKTOtYfA
'';
settings = { settings = {
OPEN_REGISTRATION = true;
FORCE_HTTPS_URLS = false; 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 = { nginx = {
# locations."/public/".proxyPass = "${config.fediversity.internal.garage.web.urlFor "pixelfed"}/public/"; forceSSL = mkVMOverride false;
enableACME = mkVMOverride false;
}; };
}; };
virtualisation.memorySize = 2048; virtualisation.memorySize = 2048;
virtualisation.forwardPorts = [ virtualisation.forwardPorts = [
{ {