From 8fd1e2b75cae1216abfe419f58bf5a13e7da632c Mon Sep 17 00:00:00 2001
From: Valentin Gagarin <valentin.gagarin@tweag.io>
Date: Mon, 31 Mar 2025 15:44:23 +0200
Subject: [PATCH] WIP: simplify deployment code

---
 deployment/options.nix   | 42 +++++++++++++++++++--------------
 panel/src/panel/views.py | 50 ++++++++++++++++++++++------------------
 2 files changed, 52 insertions(+), 40 deletions(-)

diff --git a/deployment/options.nix b/deployment/options.nix
index df04b994..27d38bce 100644
--- a/deployment/options.nix
+++ b/deployment/options.nix
@@ -2,7 +2,7 @@
   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` (or `submodule` until [submodule introspection](https://github.com/NixOS/nixpkgs/pull/391544) is merged), and must not access `config` for their default values.
+  For this to work, options must not have types `functionTo` or `package`, and must not access `config` for their default values.
 */
 {
   lib,
@@ -34,22 +34,30 @@ in
     mastodon = {
       enable = lib.mkEnableOption "Mastodon";
     };
-    initialUser = {
-      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";
+    initialUser = mkOption {
+      description = ''
+        Some services require an initial user to access them.
+        This option sets the credentials for such an initial user.
+      '';
+      type = types.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";
+          };
+        };
       };
     };
   };
diff --git a/panel/src/panel/views.py b/panel/src/panel/views.py
index 9c7e9de1..771289d7 100644
--- a/panel/src/panel/views.py
+++ b/panel/src/panel/views.py
@@ -14,6 +14,8 @@ from rest_framework.renderers import TemplateHTMLRenderer
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
+from pydantic import BaseModel
+
 from panel import models, settings
 from panel.configuration import schema
 
@@ -39,7 +41,6 @@ class ConfigurationForm(LoginRequiredMixin, APIView):
     success_url = reverse_lazy('configuration_form')
 
     def get_object(self):
-        """Get or create the configuration object for the current user"""
         obj, created = models.Configuration.objects.get_or_create(
             operator=self.request.user,
         )
@@ -67,31 +68,36 @@ class ConfigurationForm(LoginRequiredMixin, APIView):
         config.save()
         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):
-    def form_valid(self, form):
-        obj = self.get_object()
-        obj.value = form.to_python().model_dump_json()
-        obj.save()
 
-        # Check for deploy button
-        if "deploy" in self.request.POST.keys():
-            deployment_result, deployment_params = self.deployment(obj)
-            if deployment_result.returncode == 0:
-                deployment_status = "Deployment Succeeded"
-            else:
-                deployment_status = "Deployment Failed"
+    def post(self, request):
+        config = self.get_object()
+        serializer = schema.Model.drf_serializer(
+            instance=config.parsed_value,
+            data=request.data
+        )
+
+        if not serializer.is_valid():
+            return Response({'serializer': serializer})
+
+        config.value = json.dumps(serializer.validated_data)
+        config.save()
+
+        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", {
             "deployment_status": deployment_status,
-            "services": {
-                "peertube": deployment_params['peertube']['enable'],
-                "pixelfed": deployment_params['pixelfed']['enable'],
-                "mastodon": deployment_params['mastodon']['enable']
-                }
+            "services": deployment_params.json(),
             })
 
-    def deployment(self, obj):
-        submission = obj.parsed_value.model_dump_json()
+    def deployment(self, config: BaseModel):
         # FIXME: let the user specify these from the form (#190)
         dummy_user = {
             "initialUser": {
@@ -101,12 +107,10 @@ class DeploymentStatus(ConfigurationForm):
                 "email": "test@test.com",
             },
         }
-        # serialize back and forth now we still need to manually inject the dummy user
-        deployment_params = json.dumps(dummy_user | json.loads(submission))
         env = {
             "PATH": settings.bin_path,
             # pass in form info to our deployment
-            "DEPLOYMENT": deployment_params,
+            "DEPLOYMENT": config.json()
         }
         cmd = [
             "nix",
@@ -123,4 +127,4 @@ class DeploymentStatus(ConfigurationForm):
             cwd=settings.repo_dir,
             env=env,
         )
-        return deployment_result, json.loads(deployment_params)
+        return deployment_result, config