Compare commits

..

No commits in common. "ce5126c0fac2ce08d9e2bea927daf66cb27b6b28" and "98c4490b4e40a33724593a9ee2299a248dea04a4" have entirely different histories.

11 changed files with 54 additions and 104 deletions

View file

@ -27,9 +27,3 @@ 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,13 +58,9 @@
packages = [ packages = [
pkgs.nil pkgs.nil
inputs'.agenix.packages.default inputs'.agenix.packages.default
pkgs.openssh inputs'.nixops4.packages.default
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,17 +143,7 @@ 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 ( test = makeTestDeployment (fromJSON (readFile ./test-machines/configuration.json));
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,10 +4,15 @@
}: }:
let let
name = "panel"; name = "panel";
panel = (import ../../../panel/default.nix { }).package;
in in
{ {
imports = [ imports = [
(import ../../../panel { }).module ../../../panel/nix/configuration.nix
];
environment.systemPackages = [
panel
]; ];
security.acme = { security.acme = {
@ -17,11 +22,18 @@ 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
inherit (pkgs) lib; package = pkgs.callPackage ./nix/package.nix { };
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 = [ (pkgs.callPackage ./nix/package.nix { }) ]; inputsFrom = [ package ];
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_DIRECTORY mkdir -p .credentials
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"
''; '';
}; };
module = import ./nix/configuration.nix; tests = pkgs'.callPackage ./nix/tests.nix { };
tests = pkgs.callPackage ./nix/tests.nix { }; inherit package;
# 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,6 +12,7 @@ let
mkEnableOption mkEnableOption
mkIf mkIf
mkOption mkOption
mkPackageOption
optionalString optionalString
types types
; ;
@ -21,15 +22,23 @@ 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: with ps; [ ps:
package with ps;
[
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" [
@ -39,7 +48,7 @@ let
manage-service = writeShellApplication { manage-service = writeShellApplication {
name = "manage"; name = "manage";
text = ''exec ${package}/bin/manage.py "$@"''; text = ''exec ${cfg.package}/bin/manage.py "$@"'';
}; };
manage-admin = writeShellApplication { manage-admin = writeShellApplication {
@ -49,13 +58,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 != [ ]) (
@ -74,6 +83,8 @@ 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;
@ -134,8 +145,6 @@ in
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
nixpkgs.overlays = [ (import ./overlay.nix) ];
environment.systemPackages = [ manage-admin ]; environment.systemPackages = [ manage-admin ];
services = { services = {
@ -172,15 +181,16 @@ 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) != ${package} ]]; then if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.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 ${package} > "$versionFile" echo ${cfg.package} > "$versionFile"
fi fi
''; '';
script = '' script = ''
uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port} export PYTHONPATH=$PYTHONPATH:${cfg.package}/lib/python3.12/site-packages
${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,8 +3,6 @@ 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;
@ -28,7 +26,6 @@ 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}")
''; '';
}; };
@ -37,6 +34,7 @@ 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")
@ -47,11 +45,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,10 +39,11 @@ 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.NET, default=Domain.EU,
description="DNS domain where to expose services" description="DNS domain where to expose services"
) )

View file

@ -26,8 +26,6 @@ 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:
@ -173,9 +171,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(@fricklerhandwerk): # TODO: we may want to do this with a flat environment instead, and get all values from `os.environ.get()`.
# 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:
spec = importlib.util.spec_from_file_location("user_settings", user_settings_file) spec = importlib.util.spec_from_file_location("user_settings", user_settings_file)
@ -185,15 +182,3 @@ 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" type="submit" name="deploy">Deploy</button> <button class="button" disabled>Deploy</button>
<button class="button" type="submit" name="save">Save</button> <button class="button" type="submit" >Save</button>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,6 +1,4 @@
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
@ -8,7 +6,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, settings from panel import models
from panel.configuration import forms from panel.configuration import forms
@ -43,40 +41,6 @@ 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):