Compare commits

...

10 commits

Author SHA1 Message Date
ce5126c0fa add CI tests for the panel 2025-03-20 10:57:58 +01:00
c98663ae71
pass nix binary explicitly rather than thru PATH 2025-03-20 09:44:24 +01:00
3700b6e383 remove option fediversity.eu (#257)
removing fediversity.eu from form options as its subdomains are running live services

Reviewed-on: Fediversity/Fediversity#257
Reviewed-by: Kevin Muller <kevin@procolix.com>
Co-authored-by: Kiara Grouwstra <kiara@procolix.eu>
Co-committed-by: Kiara Grouwstra <kiara@procolix.eu>
2025-03-19 16:01:03 +01:00
e3b816d85e
revert change on USER_SETTINGS_FILE, fixing dev shell 2025-03-19 10:22:52 +01:00
afbbcbc22d simplify configuration via environment 2025-03-19 10:06:38 +01:00
c5fe0157b0
factor reading env vars out to settings.py 2025-03-19 09:51:23 +01:00
53d3791eaa
move NIX_DIR to env, making its use more explicit 2025-03-19 09:51:23 +01:00
53658e9880
trigger nixops from panel
adds a deploy button to the panel form - covers the local part of #76.

As a workaround to pass info (from our user form) into nixops4 uses
environment variable `DEPLOYMENT` thru nix's
`--extra-experimental-features configurable-impure-env`.
2025-03-19 09:51:23 +01:00
3364d6c972 fix: NixOS deployment code
- simplify the configuration module

  the `package` attribute makes little sense to be user-configurable,
  since it will always need to be the derivation defined in this very
  repository. for debugging one may as well change the original code itself.

- unbreak deployment

  setting `CREDENTIALS_DIRECTORY` disabled the systemd mechanism set up
  in the configuration module.

- remove unneeded configuration for deployment

- unbreak integration tests

  before that missed waiting for the service to create some
  state before running the application-level tests.
2025-03-19 09:48:41 +01:00
8f0bcc35f0 fix: run manage in service directory
for everything else it will error on CHDIR, even if $PWD has the right owner and permissions.
2025-03-18 09:52:14 +01:00
11 changed files with 104 additions and 54 deletions

View file

@ -27,3 +27,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: nix build .#checks.x86_64-linux.peertube -L - run: nix build .#checks.x86_64-linux.peertube -L
check-panel:
runs-on: native
steps:
- uses: actions/checkout@v4
- run: cd panel && nix-build -A tests

View file

@ -58,9 +58,13 @@
packages = [ packages = [
pkgs.nil pkgs.nil
inputs'.agenix.packages.default inputs'.agenix.packages.default
inputs'.nixops4.packages.default pkgs.openssh
pkgs.httpie pkgs.httpie
pkgs.jq pkgs.jq
# exposing this env var as a hack to pass info in from form
(inputs'.nixops4.packages.default.overrideAttrs {
impureEnvVars = [ "DEPLOYMENT" ];
})
]; ];
shellHook = config.pre-commit.installationScript; shellHook = config.pre-commit.installationScript;
}; };

View file

@ -143,7 +143,17 @@ in
## - We add a “test” deployment with all test machines. ## - We add a “test” deployment with all test machines.
nixops4Deployments = genAttrs machines makeDeployment' // { nixops4Deployments = genAttrs machines makeDeployment' // {
default = makeDeployment machines; default = makeDeployment machines;
test = makeTestDeployment (fromJSON (readFile ./test-machines/configuration.json)); test = makeTestDeployment (
fromJSON (
let
env = builtins.getEnv "DEPLOYMENT";
in
if env != "" then
env
else
builtins.trace "env var DEPLOYMENT not set, falling back to ./test-machines/configuration.json!" (readFile ./test-machines/configuration.json)
)
);
}; };
flake.nixosConfigurations = flake.nixosConfigurations =
genAttrs machines (makeConfiguration false) genAttrs machines (makeConfiguration false)

View file

@ -4,15 +4,10 @@
}: }:
let let
name = "panel"; name = "panel";
panel = (import ../../../panel/default.nix { }).package;
in in
{ {
imports = [ imports = [
../../../panel/nix/configuration.nix (import ../../../panel { }).module
];
environment.systemPackages = [
panel
]; ];
security.acme = { security.acme = {
@ -22,18 +17,11 @@ in
services.${name} = { services.${name} = {
enable = true; enable = true;
package = panel;
production = true; production = true;
domain = "demo.fediversity.eu"; domain = "demo.fediversity.eu";
host = "0.0.0.0";
secrets = { secrets = {
SECRET_KEY = config.age.secrets.panel-secret-key.path; SECRET_KEY = config.age.secrets.panel-secret-key.path;
}; };
port = 8000; port = 8000;
settings = {
DATABASE_URL = "sqlite:///var/lib/${name}/db.sqlite3";
CREDENTIALS_DIRECTORY = "/var/lib/${name}/.credentials";
STATIC_ROOT = "/var/lib/${name}/static";
};
}; };
} }

View file

@ -8,36 +8,36 @@
}, },
}: }:
let let
package = pkgs.callPackage ./nix/package.nix { }; inherit (pkgs) lib;
pkgs' = pkgs.extend (_final: _prev: { panel = package; });
manage = pkgs.writeScriptBin "manage" '' manage = pkgs.writeScriptBin "manage" ''
exec ${pkgs.lib.getExe pkgs.python3} ${toString ./src/manage.py} $@ exec ${pkgs.lib.getExe pkgs.python3} ${toString ./src/manage.py} $@
''; '';
in in
{ {
shell = pkgs.mkShellNoCC { shell = pkgs.mkShellNoCC {
inputsFrom = [ package ]; inputsFrom = [ (pkgs.callPackage ./nix/package.nix { }) ];
packages = [ packages = [
pkgs.npins pkgs.npins
manage manage
]; ];
env = { env = {
NPINS_DIRECTORY = toString ../npins; NPINS_DIRECTORY = toString ../npins;
# explicitly use nix, as e.g. lix does not have configurable-impure-env
NIX_BIN = lib.getExe pkgs.nix;
REPO_DIR = toString ../.;
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
DATABASE_URL = "sqlite:///${toString ./src}/db.sqlite3";
}; };
shellHook = '' shellHook = ''
# in production, secrets are passed via CREDENTIALS_DIRECTORY by systemd. # in production, secrets are passed via CREDENTIALS_DIRECTORY by systemd.
# use this directory for testing with local secrets # use this directory for testing with local secrets
mkdir -p .credentials mkdir -p $CREDENTIALS_DIRECTORY
echo secret > ${builtins.toString ./.credentials}/SECRET_KEY echo secret > ${builtins.toString ./.credentials}/SECRET_KEY
export CREDENTIALS_DIRECTORY=${builtins.toString ./.credentials}
export DATABASE_URL="sqlite:///${toString ./src}/db.sqlite3"
''; '';
}; };
tests = pkgs'.callPackage ./nix/tests.nix { }; module = import ./nix/configuration.nix;
inherit package; tests = pkgs.callPackage ./nix/tests.nix { };
# re-export inputs so they can be overridden granularly # re-export inputs so they can be overridden granularly
# (they can't be accessed from the outside any other way) # (they can't be accessed from the outside any other way)

View file

@ -12,7 +12,6 @@ let
mkEnableOption mkEnableOption
mkIf mkIf
mkOption mkOption
mkPackageOption
optionalString optionalString
types types
; ;
@ -22,23 +21,15 @@ let
name = "panel"; name = "panel";
cfg = config.services.${name}; cfg = config.services.${name};
package = pkgs.callPackage ./package.nix { };
database-url = "sqlite:////var/lib/${name}/db.sqlite3"; database-url = "sqlite:////var/lib/${name}/db.sqlite3";
python-environment = pkgs.python3.withPackages ( python-environment = pkgs.python3.withPackages (
ps: ps: with ps; [
with ps; package
[
uvicorn uvicorn
cfg.package
dj-database-url
django-compressor
django-debug-toolbar
django-libsass
django_4
setuptools
] ]
++ cfg.package.propagatedBuildInputs
); );
configFile = pkgs.concatText "configuration.py" [ configFile = pkgs.concatText "configuration.py" [
@ -48,7 +39,7 @@ let
manage-service = writeShellApplication { manage-service = writeShellApplication {
name = "manage"; name = "manage";
text = ''exec ${cfg.package}/bin/manage.py "$@"''; text = ''exec ${package}/bin/manage.py "$@"'';
}; };
manage-admin = writeShellApplication { manage-admin = writeShellApplication {
@ -58,13 +49,13 @@ let
text = text =
'' ''
systemd-run --pty \ systemd-run --pty \
--same-dir \
--wait \ --wait \
--collect \ --collect \
--service-type=exec \ --service-type=exec \
--unit "manage-${name}.service" \ --unit "manage-${name}.service" \
--property "User=${name}" \ --property "User=${name}" \
--property "Group=${name}" \ --property "Group=${name}" \
--property "WorkingDirectory=/var/lib/${name}" \
--property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \ --property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \
'' ''
+ optionalString (credentials != [ ]) ( + optionalString (credentials != [ ]) (
@ -83,8 +74,6 @@ in
{ {
options.services.${name} = { options.services.${name} = {
enable = mkEnableOption "Service configuration for `${name}`"; enable = mkEnableOption "Service configuration for `${name}`";
# NOTE: this requires that the package is present in `pkgs`
package = mkPackageOption pkgs name { };
production = mkOption { production = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
@ -145,6 +134,8 @@ in
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
nixpkgs.overlays = [ (import ./overlay.nix) ];
environment.systemPackages = [ manage-admin ]; environment.systemPackages = [ manage-admin ];
services = { services = {
@ -181,16 +172,15 @@ in
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"
if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then if [[ $(cat "$versionFile" 2>/dev/null) != ${package} ]]; then
manage migrate --no-input manage migrate --no-input
manage collectstatic --no-input --clear manage collectstatic --no-input --clear
manage compress --force manage compress --force
echo ${cfg.package} > "$versionFile" echo ${package} > "$versionFile"
fi fi
''; '';
script = '' script = ''
export PYTHONPATH=$PYTHONPATH:${cfg.package}/lib/python3.12/site-packages uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port}
${python-environment}/bin/python -m uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port}
''; '';
serviceConfig = { serviceConfig = {
Restart = "always"; Restart = "always";

View file

@ -3,6 +3,8 @@ let
# TODO: specify project/service name globally # TODO: specify project/service name globally
name = "panel"; name = "panel";
defaults = { defaults = {
# XXX: we have to duplicate this here despite it being defined in the service module, otherwise the test framework will error out
nixpkgs.overlays = lib.mkForce [ (import ./overlay.nix) ];
services.${name} = { services.${name} = {
enable = true; enable = true;
production = false; production = false;
@ -26,6 +28,7 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
# run all application-level tests managed by Django # run all application-level tests managed by Django
# https://docs.djangoproject.com/en/5.0/topics/testing/overview/ # https://docs.djangoproject.com/en/5.0/topics/testing/overview/
testScript = '' testScript = ''
server.wait_for_unit("${name}.service")
server.succeed("manage test ${name}") server.succeed("manage test ${name}")
''; '';
}; };
@ -34,7 +37,6 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
nodes.server = _: { imports = [ ./configuration.nix ]; }; nodes.server = _: { imports = [ ./configuration.nix ]; };
# check that the admin interface is served # check that the admin interface is served
testScript = '' testScript = ''
server.wait_for_unit("multi-user.target")
server.wait_for_unit("${name}.service") server.wait_for_unit("${name}.service")
server.wait_for_open_port(8000) server.wait_for_open_port(8000)
server.succeed("curl --fail -L -H 'Host: example.org' http://localhost/admin") server.succeed("curl --fail -L -H 'Host: example.org' http://localhost/admin")
@ -45,11 +47,11 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
inherit defaults; inherit defaults;
nodes.server = _: { imports = [ ./configuration.nix ]; }; nodes.server = _: { imports = [ ./configuration.nix ]; };
extraPythonPackages = ps: with ps; [ beautifulsoup4 ]; extraPythonPackages = ps: with ps; [ beautifulsoup4 ];
# type checking on `beautifulsoup4` will error out
skipTypeCheck = true; skipTypeCheck = true;
# check that stylesheets are pre-processed and served # check that stylesheets are pre-processed and served
testScript = '' testScript = ''
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
server.wait_for_unit("multi-user.target")
server.wait_for_unit("${name}.service") server.wait_for_unit("${name}.service")
server.wait_for_open_port(8000) server.wait_for_open_port(8000)
stdout = server.succeed("curl --fail -H 'Host: example.org' http://localhost") stdout = server.succeed("curl --fail -H 'Host: example.org' http://localhost")

View file

@ -39,11 +39,10 @@ class Configuration(BaseModel):
# XXX: hard-code available apex domains for now, # XXX: hard-code available apex domains for now,
# they will be prefixed by the user name # they will be prefixed by the user name
class Domain(Enum): class Domain(Enum):
EU = "fediversity.eu"
NET = "fediversity.net" NET = "fediversity.net"
domain: Domain = Field( domain: Domain = Field(
default=Domain.EU, default=Domain.NET,
description="DNS domain where to expose services" description="DNS domain where to expose services"
) )

View file

@ -26,6 +26,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
def get_secret(name: str, encoding: str = "utf-8") -> str: def get_secret(name: str, encoding: str = "utf-8") -> str:
# In the NixOS deployment, this variable is set by `systemd` via `LoadCredential`
# https://systemd.io/CREDENTIALS/
credentials_dir = env.get("CREDENTIALS_DIRECTORY") credentials_dir = env.get("CREDENTIALS_DIRECTORY")
if credentials_dir is None: if credentials_dir is None:
@ -171,7 +173,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Customization via user settings # Customization via user settings
# This must be at the end, as it must be able to override the above # This must be at the end, as it must be able to override the above
# TODO: we may want to do this with a flat environment instead, and get all values from `os.environ.get()`. # TODO(@fricklerhandwerk):
# we may want to do this with a flat environment instead, and get all values from `os.environ.get()`.
# this would make it more obvious which moving parts there are, if that environment is specified for development/staging/production in a visible place. # this would make it more obvious which moving parts there are, if that environment is specified for development/staging/production in a visible place.
user_settings_file = env.get("USER_SETTINGS_FILE", None) user_settings_file = env.get("USER_SETTINGS_FILE", None)
if user_settings_file is not None: if user_settings_file is not None:
@ -182,3 +185,15 @@ if user_settings_file is not None:
spec.loader.exec_module(module) spec.loader.exec_module(module)
sys.modules["user_settings"] = module sys.modules["user_settings"] = module
from user_settings import * # noqa: F403 # pyright: ignore [reportMissingImports] from user_settings import * # noqa: F403 # pyright: ignore [reportMissingImports]
# non-Django application settings
# TODO(@fricklerhandwerk):
# 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.
# a dir of nix supporting experimental feature `configurable-impure-env`.
nix_bin=env['NIX_BIN']
# path of the root flake to trigger nixops from, see #94.
# to deploy this should be specified, for dev just use a relative path.
repo_dir = env["REPO_DIR"]

View file

@ -5,7 +5,7 @@
{{ form.as_p }} {{ form.as_p }}
<button class="button" disabled>Deploy</button> <button class="button" type="submit" name="deploy">Deploy</button>
<button class="button" type="submit" >Save</button> <button class="button" type="submit" name="save">Save</button>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,6 @@
from enum import Enum from enum import Enum
import json
import subprocess
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@ -6,7 +8,7 @@ from django.contrib.auth.models import User
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from panel import models from panel import models, settings
from panel.configuration import forms from panel.configuration import forms
@ -41,6 +43,40 @@ class ConfigurationForm(LoginRequiredMixin, FormView):
operator=self.request.user, operator=self.request.user,
) )
# Check for deploy button
if "deploy" in self.request.POST.keys():
submission = obj.parsed_value.model_dump_json()
# FIXME: let the user specify these from the form (#190)
dummy_user = {
"initialUser": {
"displayName": "Testy McTestface",
"username": "test",
"password": "testtest",
"email": "test@test.com",
},
}
# serialize back and forth now we still need to manually inject the dummy user
deployment = json.dumps(dummy_user | json.loads(submission))
env = {
# pass in form info to our deployment
"DEPLOYMENT": deployment,
}
cmd = [
settings.nix_bin,
"develop",
# workaround to pass in info to nixops4 thru env vars, tho impure :(
"--extra-experimental-features",
"configurable-impure-env",
"--command",
"nixops4",
"apply",
"test",
]
subprocess.run(
cmd,
cwd=settings.repo_dir,
env=env,
)
return obj return obj
# TODO(@fricklerhandwerk): # TODO(@fricklerhandwerk):