From 693e21b1a8f395b3adfb9bf98ba014be33e75db0 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Tue, 25 Jun 2024 06:39:04 -0400 Subject: [PATCH] first stab at a nixos test for now, had to get rid of vmVariant. we can figure out how to add it back when we understand how we should actually distinguish between real machines and VMs --- .gitignore | 1 + README.md | 10 +- common.nix | 19 ++++ flake.nix | 11 ++ garage.nix | 30 +++--- mastodon.nix | 206 +++++++++++++++++++------------------- tests/fediversity.png | Bin 0 -> 5640 bytes tests/mastodon-garage.nix | 58 +++++++++++ tests/rebuildableTest.nix | 149 +++++++++++++++++++++++++++ 9 files changed, 364 insertions(+), 120 deletions(-) create mode 100644 tests/fediversity.png create mode 100644 tests/mastodon-garage.nix create mode 100644 tests/rebuildableTest.nix diff --git a/.gitignore b/.gitignore index b83e248..e83c5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ nixos.qcow2 result* .direnv +.nixos-test-history diff --git a/README.md b/README.md index 6a8f983..1a3e805 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ With the VM running, you can then access the apps on your local machine's web br NOTE: it sometimes takes a while for the services to start up, and in the meantime you will get 502 Bad Gateway. -- Mastodon: - - You can also create accounts on the machine itself by running `mastodon-tootctl accounts create test --email test@test.com --confirmed --approve` +- Mastodon: through the reverse proxy at and directly at + - You can create accounts on the machine itself by running `mastodon-tootctl accounts create test --email test@test.com --confirmed --approve` + - Account-related activities (logging in/out; preferences) can only be done on the insecure direct page + - After you've logged in, you can go back to the secure page and you will remain logged in + - some operations may remove the port number from the URL. You'll have to add that back in manually - PeerTube: - The root account can be accessed with username "root". The password can be obtained by running the following command on the VM: @@ -51,6 +54,7 @@ NOTE: it sometimes takes a while for the services to start up, and in the meanti - mastodon-web.service - peertube.service - the `garage` CLI command gives information about garage storage, but cannot be used to actually inspect the contents. use `mc` (minio) for that +- in the chromium devtools, you can go to the networking tab and change things like response headers in a way that persists through reloads. this is much faster iteration time if that's what you need to epxeriment with. # questions @@ -77,5 +81,7 @@ When mastodon is running in production mode, we have a few problems: - you have to click "accept the security risk" - it takes a while for the webpage to come online. Until then you see "502 Bad Gateway" - email sent from the mastodon instance (e.g. for account confirmation) should be accessible at , but it's not working. +- mastodon is trying to fetch `missing.png` without ssl (`http://`). This isn't allowed, and i'm not sure why it's doing it. +- mastodon is trying to fetch `custom.css` from https://mastodon.localhost (no port), which is not the configured `LOCAL_DOMAIN`, so it's unclear why. diff --git a/common.nix b/common.nix index 2f98d29..431e487 100644 --- a/common.nix +++ b/common.nix @@ -44,5 +44,24 @@ "-device virtconsole,chardev=char0,nr=0" ]; }; + + + # we can't forward port 80 or 443, so let's run nginx on a different port + networking.firewall.allowedTCPPorts = [ 8443 8080 ]; + services.nginx.defaultSSLListenPort = 8443; + services.nginx.defaultHTTPListenPort = 8080; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 8080; + guest.port = 8080; + } + { + from = "host"; + host.port = 8443; + guest.port = 8443; + } + ]; + }; } diff --git a/flake.nix b/flake.nix index 9a6e011..bdc1b4f 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,13 @@ pkgs = nixpkgs.legacyPackages.${system}; in { + nixosModules = { + mastodon = import ./mastodon.nix; + peertube = import ./peertube.nix; + pixelfed = import ./pixelfed.nix; + garage = import ./garage.nix; + }; + nixosConfigurations = { mastodon = nixpkgs.lib.nixosSystem { inherit system; @@ -33,6 +40,10 @@ }; }; + checks.${system} = { + mastodon-garage = import ./tests/mastodon-garage.nix { inherit pkgs self; }; + }; + devShells.${system}.default = pkgs.mkShell { inputs = with pkgs; [ nil diff --git a/garage.nix b/garage.nix index c6baba7..2c47ec9 100644 --- a/garage.nix +++ b/garage.nix @@ -124,21 +124,19 @@ in { }; config = { - virtualisation.vmVariant = { - virtualisation.diskSize = 2048; - virtualisation.forwardPorts = [ - { - from = "host"; - host.port = 3901; - guest.port = 3901; - } - { - from = "host"; - host.port = 3902; - guest.port = 3902; - } - ]; - }; + virtualisation.diskSize = 2048; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 3901; + guest.port = 3901; + } + { + from = "host"; + host.port = 3902; + guest.port = 3902; + } + ]; environment.systemPackages = [ pkgs.minio-client pkgs.awscli ]; @@ -190,7 +188,7 @@ in { ${ensureBucketsScript} ${ensureKeysScript} - # garage doesn't like deleting keys that once existed + # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time # garage key delete ${snakeoil_key.id} --yes ''; }; diff --git a/mastodon.nix b/mastodon.nix index 3ae312b..5527949 100644 --- a/mastodon.nix +++ b/mastodon.nix @@ -10,12 +10,12 @@ in ensureBuckets = { mastodon = { website = true; - # corsRules = { - # enable = true; - # allowedHeaders = [ "*" ]; - # allowedMethods = [ "GET" ]; - # allowedOrigins = [ "*" ]; - # }; + corsRules = { + enable = true; + allowedHeaders = [ "*" ]; + allowedMethods = [ "GET" ]; + allowedOrigins = [ "*" ]; + }; }; }; ensureKeys = { @@ -47,7 +47,7 @@ in # but we want "." S3_ALIAS_HOST = "mastodon.web.garage.localhost:3902"; # XXX: I think we need to set up a proper CDN host - CDN_HOST = "mastodon.web.garage.localhost:3902"; + # CDN_HOST = "mastodon.web.garage.localhost:3902"; # SEE: the last section in https://docs.joinmastodon.org/admin/optional/object-storage/ # TODO: can we set up ACLs with garage? S3_PERMISSION = ""; @@ -82,116 +82,118 @@ in } # VM setup { - # these configurations only apply when producing a VM (e.g. nixos-rebuild build-vm) - virtualisation.vmVariant = { config, ... }: { - services.mastodon = { - # redirects to localhost, but allows it to have a proper domain name - localDomain = "mastodon.localhost"; + services.mastodon = { + # redirects to localhost, but allows it to have a proper domain name + localDomain = "mastodon.localhost"; - smtp = { - fromAddress = "mastodon@mastodon.localhost"; - createLocally = false; - }; - - 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); + smtp = { + fromAddress = "mastodon@mastodon.localhost"; + createLocally = false; }; - 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 servers (and we don't own this domain anyways) - server = "https://127.0.0.1"; - email = "none"; - }; + extraConfig = { + EMAIL_DOMAIN_ALLOWLIST = "example.com"; }; - virtualisation.memorySize = 2048; - virtualisation.forwardPorts = [ - { - from = "host"; - host.port = 44443; - guest.port = 443; - } - ]; + # 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 = { + defaults = { + # invalid server; the systemd service will fail, and we won't get properly signed certificates + # but let's not spam the letsencrypt servers (and we don't own this domain anyways) + server = "https://127.0.0.1"; + email = "none"; + }; + }; + + virtualisation.memorySize = 2048; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 44443; + guest.port = 443; + } + ]; } # mastodon development environment { networking.firewall.allowedTCPPorts = [ 55001 ]; - virtualisation.vmVariant = { config, ... }: { - services.mastodon = { - # needed so we can directly access mastodon at port 55001 - # otherwise, mastodon has to be accessed *from* port 443, which we can't do via port forwarding - enableUnixSocket = false; - extraConfig = { - RAILS_ENV = "development"; - # to be accessible from outside the VM - BIND = "0.0.0.0"; - # for letter_opener (still doesn't work though) - REMOTE_DEV = "true"; - }; + services.mastodon = { + # needed so we can directly access mastodon at port 55001 + # otherwise, mastodon has to be accessed *from* port 443, which we can't do via port forwarding + enableUnixSocket = false; + extraConfig = { + RAILS_ENV = "development"; + # to be accessible from outside the VM + BIND = "0.0.0.0"; + # for letter_opener (still doesn't work though) + REMOTE_DEV = "true"; + LOCAL_DOMAIN = "mastodon.localhost:8443"; }; + }; + # services.nginx.virtualHosts."${config.services.mastodon.localDomain}" = { + # extraConfig = '' + # add_header Content-Security-Policy 'base-uri 'none'; default-src 'none'; frame-ancestors 'none'; font-src 'self' http://mastodon.localhost:8443; img-src * https: data: blob: http://mastodon.localhost:8443; style-src 'self' http://mastodon.localhost:8443 'nonce-QvwdQ3lNRMmEcQnhZ22MAg=='; media-src 'self' https: data: http://mastodon.localhost:8443; frame-src 'self' https:; manifest-src 'self' http://mastodon.localhost:8443; form-action 'self'; child-src 'self' blob: http://mastodon.localhost:8443; worker-src 'self' blob: http://mastodon.localhost:8443; connect-src 'self' data: blob: http://mastodon.localhost:8443 http://mastodon.web.garage.localhost:3902 ws://mastodon.localhost:4000 ws://localhost:3035 http://localhost:3035; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://mastodon.localhost:8443' + # ''; + # }; + # services.nginx.virtualHosts."${config.services.mastodon.localDomain}".locations."/sw.js" = - services.postgresql = { - enable = true; - ensureUsers = [ - { - name = config.services.mastodon.database.user; - ensureClauses.createdb = true; - # ensurePermissions doesn't work anymore - # ensurePermissions = { - # "mastodon_development.*" = "ALL PRIVILEGES"; - # "mastodon_test.*" = "ALL PRIVILEGES"; - # } - } - ]; - # ensureDatabases = [ "mastodon_development_test" "mastodon_test" ]; - }; - - # Currently, nixos seems to be able to create a single database per - # postgres user. This works for the production version of mastodon, which - # is what's packaged in nixpkgs. For development, we need two databases, - # mastodon_development and mastodon_test. This used to be possible with - # ensurePermissions, but that's broken and has been removed. Here I copy - # the mastodon-init-db script from upstream nixpkgs, but add the single - # line `rails db:setup`, which asks mastodon to create the postgres - # databases for us. - # FIXME: the commented out lines were breaking things, but presumably they're necessary for something. - # TODO: see if we can fix the upstream ensurePermissions stuff. See above for what that config would look like. - systemd.services.mastodon-init-db.script = lib.mkForce '' - result="$(psql -t --csv -c \ - "select count(*) from pg_class c \ - join pg_namespace s on s.oid = c.relnamespace \ - where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \ - and s.nspname not like 'pg_temp%';")" || error_code=$? - if [ "''${error_code:-0}" -ne 0 ]; then - echo "Failure checking if database is seeded. psql gave exit code $error_code" - exit "$error_code" - fi - if [ "$result" -eq 0 ]; then - echo "Seeding database" - rails db:setup - # SAFETY_ASSURED=1 rails db:schema:load - rails db:seed - else - # echo "Migrating database (this might be a noop)" - # rails db:migrate - fi - ''; - virtualisation.forwardPorts = [ + services.postgresql = { + enable = true; + ensureUsers = [ { - from = "host"; - host.port = 55001; - guest.port = 55001; + name = config.services.mastodon.database.user; + ensureClauses.createdb = true; + # ensurePermissions doesn't work anymore + # ensurePermissions = { + # "mastodon_development.*" = "ALL PRIVILEGES"; + # "mastodon_test.*" = "ALL PRIVILEGES"; + # } } ]; + # ensureDatabases = [ "mastodon_development_test" "mastodon_test" ]; }; + + # Currently, nixos seems to be able to create a single database per + # postgres user. This works for the production version of mastodon, which + # is what's packaged in nixpkgs. For development, we need two databases, + # mastodon_development and mastodon_test. This used to be possible with + # ensurePermissions, but that's broken and has been removed. Here I copy + # the mastodon-init-db script from upstream nixpkgs, but add the single + # line `rails db:setup`, which asks mastodon to create the postgres + # databases for us. + # FIXME: the commented out lines were breaking things, but presumably they're necessary for something. + # TODO: see if we can fix the upstream ensurePermissions stuff. See above for what that config would look like. + systemd.services.mastodon-init-db.script = lib.mkForce '' + result="$(psql -t --csv -c \ + "select count(*) from pg_class c \ + join pg_namespace s on s.oid = c.relnamespace \ + where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \ + and s.nspname not like 'pg_temp%';")" || error_code=$? + if [ "''${error_code:-0}" -ne 0 ]; then + echo "Failure checking if database is seeded. psql gave exit code $error_code" + exit "$error_code" + fi + if [ "$result" -eq 0 ]; then + echo "Seeding database" + rails db:setup + # SAFETY_ASSURED=1 rails db:schema:load + rails db:seed + # else + # echo "Migrating database (this might be a noop)" + # rails db:migrate + fi + ''; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 55001; + guest.port = 55001; + } + ]; } -] +] diff --git a/tests/fediversity.png b/tests/fediversity.png new file mode 100644 index 0000000000000000000000000000000000000000..24881fb4a0321f00a18d2c85d5859f9a04f6a108 GIT binary patch literal 5640 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