From a4a106ef97910a1b5556c18a2e363c75d482d6a4 Mon Sep 17 00:00:00 2001 From: Kiara Grouwstra Date: Mon, 23 Jun 2025 09:06:52 +0200 Subject: [PATCH 1/2] data model: add run-time configuration --- default.nix | 3 + deployment/data-model-test.nix | 67 ++++++++++++++-------- deployment/data-model.nix | 100 ++++++++++++++++++++++++--------- deployment/function.nix | 39 +++++++++++++ 4 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 deployment/function.nix diff --git a/default.nix b/default.nix index 7c536a03..ea7b2557 100644 --- a/default.nix +++ b/default.nix @@ -9,6 +9,8 @@ let git-hooks gitignore ; + inherit (import sources.flake-inputs) import-flake; + inputs = (import-flake { src = ./.; }).inputs; inherit (pkgs) lib; pre-commit-check = (import "${git-hooks}/nix" { @@ -67,6 +69,7 @@ in # re-export inputs so they can be overridden granularly # (they can't be accessed from the outside any other way) inherit + inputs sources system pkgs diff --git a/deployment/data-model-test.nix b/deployment/data-model-test.nix index 17c85f77..f6903ad2 100644 --- a/deployment/data-model-test.nix +++ b/deployment/data-model-test.nix @@ -1,9 +1,13 @@ let - inherit (import ../default.nix { }) pkgs; + inherit (import ../default.nix { }) pkgs inputs; inherit (pkgs) lib; + inherit (lib) mkOption; eval = module: (lib.evalModules { + specialArgs = { + inherit inputs; + }; modules = [ module ./data-model.nix @@ -14,32 +18,51 @@ in test-eval = { expr = let - example = eval { - runtime-environments.bar.nixos = { - module = - { ... }: - { - system.stateVersion = "25.05"; + fediversity = eval ( + { config, ... }: + { + config = { + applications.hello = + { ... }: + { + description = ''Command-line tool that will print "Hello, world!" on the terminal''; + module = + { ... }: + { + options = { + enable = lib.mkEnableOption "Hello in the shell"; + }; + }; + implementation = + cfg: + lib.optionalAttrs cfg.enable { + dummy.login-shell.packages.hello = pkgs.hello; + }; + }; + }; + options = { + example-configuration = mkOption { + type = config.configuration; + readOnly = true; + default = { + enable = true; + applications.hello.enable = true; + }; }; - }; - applications.foo = { - module = - { pkgs, ... }: - { - environment.systemPackages = [ - pkgs.hello - ]; - }; - }; - }; + }; + } + ); in { - has-runtime = lib.isAttrs example.runtime-environments.bar.nixos.module; - has-application = lib.isAttrs example.applications.foo.module; + inherit (fediversity) + example-configuration + ; }; expected = { - has-runtime = true; - has-application = true; + example-configuration = { + enable = true; + applications.hello.enable = true; + }; }; }; } diff --git a/deployment/data-model.nix b/deployment/data-model.nix index af867d55..7ab24570 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -1,43 +1,87 @@ { lib, + config, ... }: let - inherit (lib) types mkOption; + inherit (lib) mkOption types; + inherit (lib.types) + attrsOf + attrTag + deferredModuleWith + submodule + optionType + functionTo + ; + + functionType = import ./function.nix; + application-resources = { + options.resources = mkOption { + # TODO: maybe transpose, and group the resources by type instead + type = attrsOf ( + attrTag (lib.mapAttrs (_name: resource: mkOption { type = resource.request; }) config.resources) + ); + }; + }; in -with types; { options = { - runtime-environments = mkOption { - description = "Collection of runtime environments into which applications can be deployed"; - type = attrsOf (attrTag { - nixos = mkOption { - description = "A single NixOS machine"; - type = submodule { - options = { - module = mkOption { - description = "The NixOS module describing the base configuration for that machine"; - type = deferredModule; + applications = mkOption { + description = "Collection of Fediversity applications"; + type = attrsOf ( + submodule (application: { + _class = "fediversity-application"; + options = { + description = mkOption { + description = "Description to be shown in the application overview"; + type = types.str; + }; + module = mkOption { + description = "Operator-facing configuration options for the application"; + type = deferredModuleWith { staticModules = [ { _class = "fediversity-application-config"; } ]; }; + }; + implementation = mkOption { + description = "Mapping of application configuration to deployment resources, a description of what an application needs to run"; + type = application.config.config-mapping.function-type; + }; + resources = mkOption { + description = "Compute resources required by an application"; + type = functionTo application.config.config-mapping.output-type; + readOnly = true; + default = input: (application.config.implementation input).output; + }; + config-mapping = mkOption { + description = "Function type for the mapping from application configuration to required resources"; + type = submodule functionType; + readOnly = true; + default = { + input-type = application.config.module; + output-type = application-resources; }; }; }; - }; - }); + }) + ); }; - applications = mkOption { - description = "Collection of Fediversity applications"; - type = attrsOf (submoduleWith { - modules = [ - { - options = { - module = mkOption { - description = "The NixOS module for that application, for configuring that application"; - type = deferredModule; - }; - }; - } - ]; - }); + configuration = mkOption { + description = "Configuration type declaring options to be set by operators"; + type = optionType; + readOnly = true; + default = submodule { + options = { + enable = lib.mkEnableOption { + description = "your Fediversity configuration"; + }; + applications = lib.mapAttrs ( + _name: application: + mkOption { + description = application.description; + type = submodule application.module; + default = { }; + } + ) config.applications; + }; + }; }; }; } diff --git a/deployment/function.nix b/deployment/function.nix new file mode 100644 index 00000000..4f473107 --- /dev/null +++ b/deployment/function.nix @@ -0,0 +1,39 @@ +/** + Modular function type +*/ +{ config, lib, ... }: +let + inherit (lib) mkOption types; + inherit (types) + deferredModule + submodule + functionTo + optionType + ; +in +{ + options = { + input-type = mkOption { + type = deferredModule; + }; + output-type = mkOption { + type = deferredModule; + }; + function-type = mkOption { + type = optionType; + readOnly = true; + default = functionTo ( + submodule (function: { + options = { + input = mkOption { + type = submodule config.input-type; + }; + output = mkOption { + type = submodule config.output-type; + }; + }; + }) + ); + }; + }; +} -- 2.48.1 From 18032cd2888f1f4f8e97552d7685b18b2c6a2835 Mon Sep 17 00:00:00 2001 From: Kiara Grouwstra Date: Sun, 6 Jul 2025 12:54:40 +0200 Subject: [PATCH 2/2] implement `applications.mastodon` as a sample use-case of our data model --- deployment/applications/default.nix | 5 + deployment/applications/mastodon.nix | 141 +++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 deployment/applications/default.nix create mode 100644 deployment/applications/mastodon.nix diff --git a/deployment/applications/default.nix b/deployment/applications/default.nix new file mode 100644 index 00000000..ff8ebce1 --- /dev/null +++ b/deployment/applications/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./mastodon.nix + ]; +} diff --git a/deployment/applications/mastodon.nix b/deployment/applications/mastodon.nix new file mode 100644 index 00000000..f848d54c --- /dev/null +++ b/deployment/applications/mastodon.nix @@ -0,0 +1,141 @@ +{ + lib, + pkgs, + config, + ... +}: +{ + applications.mastodon = + { ... }: + { + description = '' + Mastodon: Your self-hosted, globally interconnected microblogging community + ''; + module = + # # usage: + # { + # config.fediversity.domain = lib.mkOption { + # type = lib.types.str; + # default = "fediversity.tld"; + # }; + # } + { lib, config, ... }: + { + options = import ../../../services/fediversity/sharedOptions.nix { + inherit config lib; + serviceName = "mastodon"; + serviceDocName = "Mastodon"; + }; + }; + # this is ported from `services/fediversity/mastodon/default.nix`. what of `services/vm/mastodon-vm.nix`? + implementation = + cfg: + # { + # config, + # lib, + # pkgs, + # ... + # }: + let + inherit (lib) mkIf mkMerge readFile; + inherit (pkgs) writeText; + in + lib.optionalAttrs cfg.enable { + # # not using this part as we split this out in our data model. + # imports = [ ./options.nix ]; + + config = mkMerge [ + (mkIf + (config.fediversity.garage.enable && cfg.s3AccessKeyFile != null && cfg.s3SecretKeyFile != null) + { + fediversity.garage = { + ensureBuckets = { + mastodon = { + website = true; + corsRules = { + enable = true; + allowedHeaders = [ "*" ]; + allowedMethods = [ "GET" ]; + allowedOrigins = [ "*" ]; + }; + }; + }; + + ensureKeys = { + mastodon = { + inherit (cfg) s3AccessKeyFile s3SecretKeyFile; + ensureAccess = { + mastodon = { + read = true; + write = true; + owner = true; + }; + }; + }; + }; + }; + } + ) + + (mkIf cfg.enable { + + services.mastodon.extraConfig = rec { + S3_ENABLED = "true"; + # TODO: this shouldn't be hard-coded, it should come from the garage configuration + S3_ENDPOINT = config.fediversity.garage.api.url; + S3_REGION = "garage"; + S3_BUCKET = "mastodon"; + # use . + S3_OVERRIDE_PATH_STLE = "true"; + S3_PROTOCOL = "http"; + S3_ALIAS_HOST = config.fediversity.garage.web.domainForBucket S3_BUCKET; + # SEE: the last section in https://docs.joinmastodon.org/admin/optional/object-storage/ + # TODO: can we set up ACLs with garage? + S3_PERMISSION = ""; + }; + + ## FIXME: secrets management; we should have a service that writes the + ## `.env` files based on all the secrets that we need to put there. + services.mastodon.extraEnvFiles = [ + (writeText "s3AccessKey" '' + AWS_ACCESS_KEY_ID=${readFile cfg.s3AccessKeyFile} + '') + (writeText "s3SecretKey" '' + AWS_SECRET_ACCESS_KEY=${readFile cfg.s3SecretKeyFile} + '') + ]; + + # open up access to the mastodon web interface. 80 is necessary if only for ACME + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.mastodon = { + enable = true; + + localDomain = cfg.domain; + configureNginx = true; + + # from the documentation: recommended is the amount of your CPU cores minus + # one. but it also must be a positive integer + streamingProcesses = lib.max 1 (config.fediversity.temp.cores - 1); + + # TODO: configure a mailserver so this works + smtp = { + fromAddress = "noreply@${cfg.domain}"; + createLocally = false; + }; + }; + + security.acme = { + acceptTerms = true; + preliminarySelfsigned = true; + # TODO: configure a mailserver so we can set up acme + # defaults.email = "test@example.com"; + }; + }) + ]; + }; + }; +} -- 2.48.1