diff --git a/deployment/default.nix b/deployment/default.nix index ae7ff5f1..8db424d7 100644 --- a/deployment/default.nix +++ b/deployment/default.nix @@ -44,6 +44,17 @@ in { providers, ... }: { + options = { + deployment = lib.mkOption { + description = '' + Configuration to be deployed + ''; + # XXX(@fricklerhandwerk): + # misusing this will produce obscure errors that will be truncated by NixOps4 + type = lib.types.submodule ./options.nix; + }; + }; + config = { providers = { inherit (nixops4.modules.nixops4Provider) local; }; diff --git a/deployment/options.nix b/deployment/options.nix new file mode 100644 index 00000000..0c5fa078 --- /dev/null +++ b/deployment/options.nix @@ -0,0 +1,102 @@ +/** + Deployment options as to be presented in the front end. + + These are converted to JSON schema in order to generate front-end forms etc. + For this to work, options must not have types `functionTo` or `package`, and must not access `config` for their default values. + + The options are written in a cumbersome way because the JSON schema converter can't evaluate a submodule option's default value, which thus all must be set to `null`. + This can be fixed if we made the converter aware of [`$defs`], but that would likely amount to half a rewrite. + + [`$defs`]: https://json-schema.org/understanding-json-schema/structuring#defs +*/ +{ + lib, + ... +}: +let + inherit (lib) types mkOption; +in +{ + options = { + enable = lib.mkEnableOption "Fediversity configuration"; + domain = mkOption { + type = + with types; + enum [ + "fediversity.net" + ]; + description = '' + Apex domain under which the services will be deployed. + ''; + default = "fediversity.net"; + }; + pixelfed = mkOption { + description = '' + Configuration for the Pixelfed service + ''; + type = + with types; + nullOr (submodule { + options = { + enable = lib.mkEnableOption "Pixelfed"; + }; + }); + default = null; + }; + peertube = mkOption { + description = '' + Configuration for the PeerTube service + ''; + type = + with types; + nullOr (submodule { + options = { + enable = lib.mkEnableOption "Peertube"; + }; + }); + default = null; + }; + mastodon = mkOption { + description = '' + Configuration for the Mastodon service + ''; + type = + with types; + nullOr (submodule { + options = { + enable = lib.mkEnableOption "Mastodon"; + }; + }); + default = null; + }; + initialUser = mkOption { + description = '' + Some services require an initial user to access them. + This option sets the credentials for such an initial user. + ''; + type = + with types; + nullOr (submodule { + options = { + displayName = mkOption { + type = types.str; + description = "Display name of the user"; + }; + username = mkOption { + type = types.str; + description = "Username for login"; + }; + email = mkOption { + type = types.str; + description = "User's email address"; + }; + password = mkOption { + type = types.str; + description = "Password for login"; + }; + }; + }); + default = null; + }; + }; +} diff --git a/npins/sources.json b/npins/sources.json index ea7efb09..1c8bf296 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -1,5 +1,17 @@ { "pins": { + "clan-core": { + "type": "Git", + "repository": { + "type": "Git", + "url": "https://git.clan.lol/clan/clan-core" + }, + "branch": "main", + "submodules": false, + "revision": "ce55397ef778bc5d460b26698fed0d1eaa8f8bd6", + "url": null, + "hash": "1w2gsy6qwxa5abkv8clb435237iifndcxq0s79wihqw11a5yb938" + }, "htmx": { "type": "GitRelease", "repository": { diff --git a/panel/.gitignore b/panel/.gitignore index 5ab365e7..bcdd5369 100644 --- a/panel/.gitignore +++ b/panel/.gitignore @@ -11,4 +11,5 @@ db.sqlite3 src/db.sqlite3 src/static src/panel/static/htmx* +src/panel/configuration/schema.py .credentials diff --git a/panel/nix/package.nix b/panel/nix/package.nix index 267aafe6..e13e015a 100644 --- a/panel/nix/package.nix +++ b/panel/nix/package.nix @@ -2,6 +2,9 @@ lib, sqlite, python3, + python3Packages, + callPackage, + runCommand, sources ? import ../../npins, }: let @@ -25,11 +28,28 @@ let packages = [ "${name}" ] include-package-data = true ''; + generated = [ { from = "${sources.htmx}/dist/htmx.min.js"; to = "./panel/static/htmx.min.js"; } + { + from = + let + jsonschema = callPackage "${sources.clan-core}/lib/jsonschema" { } { }; + frontend-options = jsonschema.parseModule ../../deployment/options.nix; + schema = with builtins; toFile "schema.json" (toJSON frontend-options); + codegen = "${python3Packages.datamodel-code-generator}/bin/datamodel-codegen"; + pydantic = runCommand "schema.py" { } '' + # replace plain `pydantic` with `drf_pydantic` so we can create forms automatically + ${codegen} --input ${schema} | sed '/from pydantic/a\ + from drf_pydantic import BaseModel' > $out + ''; + in + "${pydantic}"; + to = "./panel/configuration/schema.py"; + } ]; in python3.pkgs.buildPythonPackage { @@ -51,6 +71,7 @@ python3.pkgs.buildPythonPackage { django-debug-toolbar django-libsass django-pydantic-field + drf-pydantic django_4 setuptools ]; diff --git a/panel/nix/python-packages/drf-pydantic/default.nix b/panel/nix/python-packages/drf-pydantic/default.nix new file mode 100644 index 00000000..2fb017a6 --- /dev/null +++ b/panel/nix/python-packages/drf-pydantic/default.nix @@ -0,0 +1,40 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + django, + pydantic, + hatchling, + djangorestframework, +}: + +buildPythonPackage rec { + pname = "drf-pydantic"; + version = "v2.7.1"; + pyproject = true; + + src = fetchFromGitHub { + owner = "georgebv"; + repo = pname; + rev = version; + hash = "sha256-ABtSoxj/+HHq4hj4Yb6bEiyOl00TCO/9tvBzhv6afxM="; + }; + + nativeBuildInputs = [ + setuptools + hatchling + ]; + + propagatedBuildInputs = [ + django + pydantic + djangorestframework + ]; + + meta = with lib; { + description = ""; + homepage = "https://github.com/${src.owner}/${pname}"; + license = licenses.mit; + }; +} diff --git a/panel/src/panel/configuration/forms.py b/panel/src/panel/configuration/forms.py deleted file mode 100644 index e79ecf5c..00000000 --- a/panel/src/panel/configuration/forms.py +++ /dev/null @@ -1,117 +0,0 @@ -from django import forms -from django.db import models -from pydantic import BaseModel, Field -from enum import Enum -from django_pydantic_field import SchemaField - -# TODO(@fricklerhandwerk): -# Eventually should probably maintain a separate series of configuration schema versions for each service. -# I didn't start it here yet, mainly to keep it readable. - - -class PeerTube(BaseModel): - enable: bool = Field( - default=False, - description="Enable PeerTube", - ) - - -class Pixelfed(BaseModel): - enable: bool = Field( - default=False, - description="Enable Pixelfed", - ) - - -class Mastodon(BaseModel): - enable: bool = Field( - default=False, - description="Enable Mastodon", - ) - - -class Configuration(BaseModel): - enable: bool = Field( - default=False, - description="Enable the configuration", - ) - - # XXX: hard-code available apex domains for now, - # they will be prefixed by the user name - class Domain(Enum): - NET = "fediversity.net" - - domain: Domain = Field( - default=Domain.NET, - description="DNS domain where to expose services" - ) - - peertube: PeerTube = Field( - default=PeerTube(), - description="Configuration for PeerTube", - ) - - pixelfed: Pixelfed = Field( - default=Pixelfed(), - description="Configuration for Pixelfed", - ) - - mastodon: Mastodon = Field( - default=Mastodon(), - description="Configuration for Mastodon", - ) - -# TODO@(fricklerhandwerk): -# generate this automatically from the Pydantic model so this code can go away: -# - add custom types, splicing `forms.Form` into `pydantic.BaseModel` and `forms.Field` into `pydantic.Field` so we can attach Django validators to Pydantic models -# - map primitive or well-known field types as captured by `pydantic.Field` to `forms.Field` constructors, and `BaseModel` to `Form` using `BaseModel.model_fields` -# - use a custom'{"name": "John Doe", "age": 30 }' widget that renders nested forms in e.g. a `