Compare commits

...

10 commits

Author SHA1 Message Date
350245c097 use deployed environment for launching nixops4 from the panel
this still needs more work to clean up fully
2025-06-06 12:14:34 +02:00
631ce8a92d use Nixpkgs from npins in the flake 2025-06-06 12:13:51 +02:00
09119803e8 Deployment: handle nullable config fields
This is quite frustrating. In the meantime, it does get the deployment
working again.
2025-06-06 11:50:48 +02:00
4dd1491e71 FediPanel: fix deployment status
also remove unused `dummy_user`
2025-06-06 11:02:40 +02:00
2f55e1512a FediPanel: bump nginx timeout to an hour 2025-06-06 10:57:19 +02:00
b59f8a4183 simplify login tests (#352)
don't go through template generation but use underlying the tag
implementation directly

Co-authored-by: Nicolas Jeannerod <nicolas.jeannerod@moduscreate.com>
Reviewed-on: Fediversity/Fediversity#352
2025-06-06 10:56:34 +02:00
56b953526b Deployment tests: Check status of services before deploying 2025-06-06 10:54:06 +02:00
1f8677e83d FediPanel: better logging of NixOps4 2025-06-06 10:53:22 +02:00
2fae356d0a Deployment tests: also make acmeNodeIP available in NixOS test 2025-06-06 10:52:49 +02:00
046f7c5998 Deployment tests: comment on Pebble's certificate 2025-06-06 10:52:18 +02:00
11 changed files with 85 additions and 48 deletions

View file

@ -79,10 +79,16 @@ in
## and check that they are working properly. ## and check that they are working properly.
extraTestScript = '' extraTestScript = ''
with subtest("Check the status of the services - there should be none"):
garage.fail("systemctl status garage.service")
mastodon.fail("systemctl status mastodon-web.service")
peertube.fail("systemctl status peertube.service")
pixelfed.fail("systemctl status phpfpm-pixelfed.service")
with subtest("Run deployment with no services enabled"): with subtest("Run deployment with no services enabled"):
deployer.succeed("nixops4 apply check-deployment-cli-nothing --show-trace --no-interactive 1>&2") deployer.succeed("nixops4 apply check-deployment-cli-nothing --show-trace --no-interactive 1>&2")
with subtest("Check the status of the services - there should be none"): with subtest("Check the status of the services - there should still be none"):
garage.fail("systemctl status garage.service") garage.fail("systemctl status garage.service")
mastodon.fail("systemctl status mastodon-web.service") mastodon.fail("systemctl status mastodon-web.service")
peertube.fail("systemctl status peertube.service") peertube.fail("systemctl status peertube.service")

View file

@ -53,6 +53,7 @@ in
}; };
config = { config = {
acmeNodeIP = config.nodes.acme.networking.primaryIPAddress;
nodes = nodes =
{ {

View file

@ -50,13 +50,16 @@ in
}; };
security.pki.certificateFiles = [ security.pki.certificateFiles = [
## NOTE: This certificate is the one used by the Pebble HTTPS server.
## This is NOT the root CA of the Pebble server. We do add it here so
## that Pebble clients can talk to its API, but this will not allow
## those machines to verify generated certificates.
testCerts.ca.cert testCerts.ca.cert
]; ];
## FIXME: it is a bit sad that all this logistics is necessary. look into ## FIXME: it is a bit sad that all this logistics is necessary. look into
## better DNS stuff ## better DNS stuff
networking.extraHosts = "${config.acmeNodeIP} acme.test"; networking.extraHosts = "${config.acmeNodeIP} acme.test";
}) })
]; ];
} }

View file

@ -33,11 +33,25 @@
## information coming from the FediPanel. ## information coming from the FediPanel.
## ##
## FIXME: lock step the interface with the definitions in the FediPanel ## FIXME: lock step the interface with the definitions in the FediPanel
panelConfig: panelConfigNullable:
let let
inherit (lib) mkIf; inherit (lib) mkIf;
nonNull = x: v: if x == null then v else x;
panelConfig = {
domain = nonNull panelConfigNullable.domain "fediversity.net";
initialUser = nonNull panelConfigNullable.initialUser {
displayName = "Testy McTestface";
username = "test";
password = "testtest";
email = "test@test.com";
};
mastodon = nonNull panelConfigNullable.mastodon { enable = false; };
peertube = nonNull panelConfigNullable.peertube { enable = false; };
pixelfed = nonNull panelConfigNullable.pixelfed { enable = false; };
};
in in
## Regular arguments of a NixOps4 deployment module. ## Regular arguments of a NixOps4 deployment module.
@ -122,7 +136,7 @@ in
{ pkgs, ... }: { pkgs, ... }:
mkIf (cfg.mastodon.enable || cfg.peertube.enable || cfg.pixelfed.enable) { mkIf (cfg.mastodon.enable || cfg.peertube.enable || cfg.pixelfed.enable) {
fediversity = { fediversity = {
inherit (panelConfig) domain; inherit (cfg) domain;
garage.enable = true; garage.enable = true;
pixelfed = pixelfedS3KeyConfig { inherit pkgs; }; pixelfed = pixelfedS3KeyConfig { inherit pkgs; };
mastodon = mastodonS3KeyConfig { inherit pkgs; }; mastodon = mastodonS3KeyConfig { inherit pkgs; };

View file

@ -1,6 +1,5 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; # consumed by flake-parts
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";
git-hooks.url = "github:cachix/git-hooks.nix"; git-hooks.url = "github:cachix/git-hooks.nix";
nixops4.follows = "nixops4-nixos/nixops4"; nixops4.follows = "nixops4-nixos/nixops4";
@ -11,9 +10,11 @@
inputs@{ flake-parts, ... }: inputs@{ flake-parts, ... }:
let let
sources = import ./npins; sources = import ./npins;
inherit (import "${flake-inputs}/lib.nix") import-flake;
inherit (sources) git-hooks agenix; inherit (sources) git-hooks agenix;
nixpkgs = import-flake sources.nixpkgs;
in in
flake-parts.lib.mkFlake { inherit inputs; } { flake-parts.lib.mkFlake { inputs = inputs // { inherit nixpkgs; }; } {
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"

View file

@ -25,6 +25,22 @@
"url": null, "url": null,
"hash": "1w2gsy6qwxa5abkv8clb435237iifndcxq0s79wihqw11a5yb938" "hash": "1w2gsy6qwxa5abkv8clb435237iifndcxq0s79wihqw11a5yb938"
}, },
"flake-inputs": {
"type": "GitRelease",
"repository": {
"type": "GitHub",
"owner": "fricklerhandwerk",
"repo": "flake-inputs"
},
"pre_releases": false,
"version_upper_bound": null,
"release_prefix": null,
"submodules": false,
"version": "1.1",
"revision": "6461d0b56e790bf289af07c5e5261abbf4f536af",
"url": "https://api.github.com/repos/fricklerhandwerk/flake-inputs/tarball/1.1",
"hash": "03mwisvr1mc3nd33nvg4bvcyxjxpm4lwhwym39r0768cm1007ixl"
},
"flake-parts": { "flake-parts": {
"type": "Git", "type": "Git",
"repository": { "repository": {

View file

@ -9,10 +9,4 @@ in
{ {
REPO_DIR = toString ../.; REPO_DIR = toString ../.;
# explicitly use nix, as e.g. lix does not have configurable-impure-env # explicitly use nix, as e.g. lix does not have configurable-impure-env
BIN_PATH = lib.makeBinPath [
# explicitly use nix, as e.g. lix does not have configurable-impure-env
pkgs.nix
# nixops error maybe due to our flake git hook: executing 'git': No such file or directory
pkgs.git
];
} }

View file

@ -146,7 +146,19 @@ in
${cfg.domain} = ${cfg.domain} =
{ {
locations = { locations = {
"/".proxyPass = "http://localhost:${toString cfg.port}"; "/" = {
proxyPass = "http://localhost:${toString cfg.port}";
extraConfig = ''
## FIXME: The following is necessary because /deployment/status
## can take aaaaages to respond. I think this is horrendous
## design from the panel and should be changed there, but in the
## meantime we bump nginx's timeouts to one hour.
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
send_timeout 3600;
'';
};
"/static/".alias = "/var/lib/${name}/static/"; "/static/".alias = "/var/lib/${name}/static/";
}; };
} }
@ -166,10 +178,18 @@ in
description = "${name} ASGI server"; description = "${name} ASGI server";
after = [ "network.target" ]; after = [ "network.target" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = [ path =
[
python-environment python-environment
manage-service manage-service
]; ]
++ (
with pkgs;
lib.mkBinPath [
nix
git
]
);
preStart = '' preStart = ''
# Auto-migrate on first run or if the package has changed # Auto-migrate on first run or if the package has changed
versionFile="/var/lib/${name}/package-version" versionFile="/var/lib/${name}/package-version"

View file

@ -240,8 +240,6 @@ if user_settings_file is not None:
# The correct thing to do here would be using a helper function such as with `get_secret()` that will catch the exception and explain what's wrong and where to put the right values. # The correct thing to do here would be using a helper function such as with `get_secret()` that will catch the exception and explain what's wrong and where to put the right values.
# Replacing the `USER_SETTINGS_FILE` mechanism following the comment there would probably be a good thing. # Replacing the `USER_SETTINGS_FILE` mechanism following the comment there would probably be a good thing.
# PATH to expose to launch button
bin_path=env['BIN_PATH']
# path of the root flake to trigger nixops from, see #94. # path of the root flake to trigger nixops from, see #94.
# to deploy this should be specified, for dev just use a relative path. # to deploy this should be specified, for dev just use a relative path.
repo_dir = env["REPO_DIR"] repo_dir = env["REPO_DIR"]

View file

@ -1,9 +1,10 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template import Template, Context
from urllib.parse import unquote from urllib.parse import unquote
from panel.templatetags.custom_tags import auth_url
class Login(TestCase): class Login(TestCase):
def setUp(self): def setUp(self):
self.username = 'testuser' self.username = 'testuser'
@ -27,8 +28,7 @@ class Login(TestCase):
# check that the expected login URL is in the response # check that the expected login URL is in the response
context = response.context[0] context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'login' %}") login_url = auth_url(context, 'login')
login_url = template.render(context)
self.assertIn(login_url, response.content.decode('utf-8')) self.assertIn(login_url, response.content.decode('utf-8'))
# log in # log in
@ -49,8 +49,7 @@ class Login(TestCase):
# check that the expected logout URL is present # check that the expected logout URL is present
context = response.context[0] context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'logout' %}") logout_url = auth_url(context, 'logout')
logout_url = template.render(context)
self.assertIn(logout_url, response.content.decode('utf-8')) self.assertIn(logout_url, response.content.decode('utf-8'))
# log out again # log out again
@ -88,8 +87,7 @@ class Login(TestCase):
# check that the expected logout URL is present # check that the expected logout URL is present
context = response.context[0] context = response.context[0]
template = Template("{% load custom_tags %}{% auth_url 'logout' %}") logout_url = auth_url(context, 'logout')
logout_url = template.render(context)
self.assertIn(logout_url, response.content.decode('utf-8')) self.assertIn(logout_url, response.content.decode('utf-8'))
# log out # log out
@ -97,8 +95,7 @@ class Login(TestCase):
# check that we're at the expected location, logged out # check that we're at the expected location, logged out
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
template = Template("{% load custom_tags %}{% auth_url 'login' %}") login_url = auth_url(context, 'login')
login_url = template.render(context)
location, status = response.redirect_chain[-1] location, status = response.redirect_chain[-1]
self.assertEqual(location, unquote(login_url)) self.assertEqual(location, unquote(login_url))
self.assertFalse(response.context['user'].is_authenticated) self.assertFalse(response.context['user'].is_authenticated)

View file

@ -65,9 +65,6 @@ class ConfigurationForm(LoginRequiredMixin, APIView):
config.save() config.save()
return redirect(self.success_url) return redirect(self.success_url)
# TODO(@fricklerhandwerk):
# this is broken after changing the form view.
# but if there's no test for it, how do we know it ever worked in the first place?
class DeploymentStatus(ConfigurationForm): class DeploymentStatus(ConfigurationForm):
def post(self, request): def post(self, request):
@ -84,28 +81,15 @@ class DeploymentStatus(ConfigurationForm):
config.save() config.save()
deployment_result, deployment_params = self.deployment(config.parsed_value) deployment_result, deployment_params = self.deployment(config.parsed_value)
if deployment_result.returncode == 0:
deployment_status = "Deployment Succeeded"
else:
deployment_status = "Deployment Failed"
return render(self.request, "partials/deployment_result.html", { return render(self.request, "partials/deployment_result.html", {
"deployment_status": deployment_status, "deployment_succeeded": (deployment_result.returncode == 0),
"services": deployment_params.json(), "services": deployment_params.json(),
}) })
def deployment(self, config: BaseModel): def deployment(self, config: BaseModel):
# FIXME: let the user specify these from the form (#190)
dummy_user = {
"initialUser": {
"displayName": "Testy McTestface",
"username": "test",
"password": "testtest",
"email": "test@test.com",
},
}
env = { env = {
"PATH": settings.bin_path, "PATH": os.environ.get("PATH"),
# pass in form info to our deployment # pass in form info to our deployment
"DEPLOYMENT": config.json() "DEPLOYMENT": config.json()
} }
@ -118,10 +102,13 @@ class DeploymentStatus(ConfigurationForm):
"nixops4", "nixops4",
"apply", "apply",
"test", "test",
"--show-trace",
"--no-interactive",
] ]
deployment_result = subprocess.run( deployment_result = subprocess.run(
cmd, cmd,
cwd=settings.repo_dir, cwd = settings.repo_dir,
env=env, env = env,
stderr = subprocess.STDOUT,
) )
return deployment_result, config return deployment_result, config