diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 98b83852..c7444752 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -32,3 +32,9 @@ jobs: steps: - uses: actions/checkout@v4 - run: nix build .#checks.x86_64-linux.deployment-basic -L + + check-launch: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: cd launch && nix-build -A tests diff --git a/flake.lock b/flake.lock index 4eff9508..d4530ac9 100644 --- a/flake.lock +++ b/flake.lock @@ -201,6 +201,24 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "git-hooks": { "inputs": { "flake-compat": "flake-compat", @@ -323,6 +341,47 @@ "type": "github" } }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "agenix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1703113217, + "narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1743860185, + "narHash": "sha256-TkhfJ+vH+iGxLQL6RJLObMmldAQpysVJ+p1WnnKyIeQ=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "b5e29565131802cc8adee7dccede794226da8614", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, "mk-naked-shell": { "flake": false, "locked": { @@ -341,9 +400,9 @@ }, "nix": { "inputs": { - "flake-compat": "flake-compat_3", - "flake-parts": "flake-parts_4", - "git-hooks-nix": "git-hooks-nix_2", + "flake-compat": "flake-compat_2", + "flake-parts": "flake-parts_3", + "git-hooks-nix": "git-hooks-nix", "nixfmt": "nixfmt", "nixpkgs": [ "nixops4-nixos", @@ -414,6 +473,24 @@ "type": "github" } }, + "nixfmt_2": { + "inputs": { + "flake-utils": "flake-utils_2" + }, + "locked": { + "lastModified": 1736283758, + "narHash": "sha256-hrKhUp2V2fk/dvzTTHFqvtOg000G1e+jyIam+D4XqhA=", + "owner": "NixOS", + "repo": "nixfmt", + "rev": "8d4bd690c247004d90d8554f0b746b1231fe2436", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixfmt", + "type": "github" + } + }, "nixops4": { "inputs": { "flake-parts": "flake-parts_3", @@ -682,6 +759,7 @@ "inputs": { "flake-parts": "flake-parts", "git-hooks": "git-hooks", + "home-manager": "home-manager_2", "nixops4": [ "nixops4-nixos", "nixops4" diff --git a/flake.nix b/flake.nix index 6dd3d3df..24dbd789 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,10 @@ pre-commit.settings.hooks = let ## Add a directory here if pre-commit hooks shouldn't apply to it. - optout = [ "npins" ]; + optout = [ + "npins" + "launch/.terraform" + ]; excludes = map (dir: "^${dir}/") optout; addExcludes = lib.mapAttrs (_: c: c // { inherit excludes; }); in diff --git a/infra/common/nixos/default.nix b/infra/common/nixos/default.nix index b870ab03..572bee46 100644 --- a/infra/common/nixos/default.nix +++ b/infra/common/nixos/default.nix @@ -2,7 +2,6 @@ let inherit (lib) mkDefault; - in { imports = [ diff --git a/infra/common/resource.nix b/infra/common/resource.nix index 7e86467c..e92cf1a3 100644 --- a/infra/common/resource.nix +++ b/infra/common/resource.nix @@ -9,7 +9,7 @@ let inherit (lib.attrsets) concatMapAttrs optionalAttrs; inherit (lib.strings) removeSuffix; sources = import ../../npins; - inherit (sources) nixpkgs agenix disko; + inherit (sources) nixpkgs agenix disko home-manager; secretsPrefix = ../../secrets; secrets = import (secretsPrefix + "/secrets.nix"); @@ -33,8 +33,9 @@ in ## should go into the `./nixos` subdirectory. nixos.module = { imports = [ - (import "${agenix}/modules/age.nix") - (import "${disko}/module.nix") + "${agenix}/modules/age.nix" + "${disko}/module.nix" + "${home-manager}/nixos" ./options.nix ./nixos ]; diff --git a/infra/flake-part.nix b/infra/flake-part.nix index 09f9718e..62fab15e 100644 --- a/infra/flake-part.nix +++ b/infra/flake-part.nix @@ -5,7 +5,7 @@ }: let - inherit (builtins) readDir readFile fromJSON; + inherit (builtins) readDir; inherit (lib) attrNames mkOption @@ -15,32 +15,16 @@ let inherit (lib.attrsets) genAttrs; sources = import ../../npins; - ## Given a machine's name and whether it is a test VM, make a resource module, + ## Given a machine's name, make a resource module, ## except for its missing provider. (Depending on the use of that resource, we ## will provide a different one.) makeResourceModule = - { vmName, isTestVm }: + { vmName }: { - imports = - [ - ./common/resource.nix - ] - ++ ( - if isTestVm then - [ - ./test-machines/${vmName} - { - nixos.module.users.users.root.openssh.authorizedKeys.keys = [ - # allow our panel vm access to the test machines - (import ../keys).panel - ]; - } - ] - else - [ - ./machines/${vmName} - ] - ); + imports = [ + ./common/resource.nix + ./machines/${vmName} + ]; fediversityVm.name = vmName; }; @@ -57,42 +41,12 @@ let inputs.nixops4-nixos.modules.nixops4Resource.nixos (makeResourceModule { inherit vmName; - isTestVm = false; }) ]; }); }; makeDeployment' = vmName: makeDeployment [ vmName ]; - ## Given an attrset of test configurations (key = test machine name, value = - ## NixOS configuration module), make a deployment with those machines' - ## configurations as resources. - makeTestDeployment = - (import ../deployment) - { - inherit lib; - inherit (inputs) nixops4 nixops4-nixos; - fediversity = import ../services/fediversity; - } - { - garageConfigurationResource = makeResourceModule { - vmName = "test01"; - isTestVm = true; - }; - mastodonConfigurationResource = makeResourceModule { - vmName = "test06"; # somehow `test02` has a problem - use test06 instead - isTestVm = true; - }; - peertubeConfigurationResource = makeResourceModule { - vmName = "test05"; - isTestVm = true; - }; - pixelfedConfigurationResource = makeResourceModule { - vmName = "test04"; - isTestVm = true; - }; - }; - nixops4ResourceNixosMockOptions = { ## NOTE: We allow the use of a few options from ## `nixops4-nixos.modules.nixops4Resource.nixos` such that we can @@ -119,18 +73,18 @@ let ## Given a VM name, make a NixOS configuration for this machine. makeConfiguration = - isTestVm: vmName: + vmName: let inherit (sources) nixpkgs; in import "${nixpkgs}/nixos" { modules = [ - (makeResourceConfig { inherit vmName isTestVm; }).nixos.module + (makeResourceConfig { inherit vmName; }).nixos.module ]; }; - makeVmOptions = isTestVm: vmName: { - inherit ((makeResourceConfig { inherit vmName isTestVm; }).fediversityVm) + makeVmOptions = vmName: { + inherit ((makeResourceConfig { inherit vmName; }).fediversityVm) proxmox vmId description @@ -148,33 +102,16 @@ let listSubdirectories = path: attrNames (filterAttrs (_: type: type == "directory") (readDir path)); machines = listSubdirectories ./machines; - testMachines = listSubdirectories ./test-machines; in { - ## - Each normal or test machine gets a NixOS configuration. - ## - Each normal or test machine gets a VM options entry. - ## - Each normal machine gets a deployment. - ## - We add a “default” deployment with all normal machines. - ## - We add a “test” deployment with all test machines. + ## - Each machine gets a NixOS configuration. + ## - Each machine gets a VM options entry. + ## - Each machine gets a deployment. + ## - We add a “default” deployment with all infra machines. nixops4Deployments = genAttrs machines makeDeployment' // { default = makeDeployment machines; - 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 = - genAttrs machines (makeConfiguration false) - // genAttrs testMachines (makeConfiguration true); - flake.vmOptions = - genAttrs machines (makeVmOptions false) - // genAttrs testMachines (makeVmOptions true); + flake.nixosConfigurations = genAttrs machines makeConfiguration; + flake.vmOptions = genAttrs machines makeVmOptions; } diff --git a/infra/machines/fedi201/fedipanel.nix b/infra/machines/fedi201/fedipanel.nix index 5c4236fc..b49471bb 100644 --- a/infra/machines/fedi201/fedipanel.nix +++ b/infra/machines/fedi201/fedipanel.nix @@ -37,6 +37,24 @@ in enable = true; production = true; domain = "demo.fediversity.eu"; + # FIXME: make it work without this duplication + settings = + let + cfg = config.services.${name}; + in + { + STATIC_ROOT = "/var/lib/${name}/static"; + DEBUG = false; + ALLOWED_HOSTS = [ + cfg.domain + cfg.host + "localhost" + "[::1]" + ]; + CSRF_TRUSTED_ORIGINS = [ "https://${cfg.domain}" ]; + COMPRESS_OFFLINE = true; + LIBSASS_OUTPUT_STYLE = "compressed"; + }; secrets = { SECRET_KEY = config.age.secrets.panel-secret-key.path; }; diff --git a/launch/.envrc b/launch/.envrc new file mode 100644 index 00000000..26ef376b --- /dev/null +++ b/launch/.envrc @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# the shebang is ignored, but nice for editors + +# shellcheck shell=bash +if type -P lorri &>/dev/null; then + eval "$(lorri direnv --flake .)" +else + echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' + use flake +fi diff --git a/launch/.gitignore b/launch/.gitignore new file mode 100644 index 00000000..9fd1eb48 --- /dev/null +++ b/launch/.gitignore @@ -0,0 +1,7 @@ +# generated +.auto.tfvars.json +.npins.json +.terraform/ +.terraform.lock.hcl +.terraform.tfstate.lock.info +terraform.tfstate* diff --git a/launch/README.md b/launch/README.md new file mode 100644 index 00000000..c0599bc8 --- /dev/null +++ b/launch/README.md @@ -0,0 +1,28 @@ +# service deployment + +deploys [NixOS](https://nixos.org/) templates using [OpenTofu](https://opentofu.org/). + +## requirements + +- [nix](https://nix.dev/) + +## usage + +### development + +before using other commands, if not using direnv: + +```sh +nix-shell +``` + +then to initialize, or after updating pins or TF providers: + +```sh +setup +``` + +## implementing + +proper documentation TODO. +until then, a reference implementation may be found in [`panel/`](https://git.fediversity.eu/Fediversity/Fediversity/src/branch/main/panel). diff --git a/launch/default.nix b/launch/default.nix new file mode 100644 index 00000000..8615ffd1 --- /dev/null +++ b/launch/default.nix @@ -0,0 +1,41 @@ +{ + system ? builtins.currentSystem, + sources ? import ../npins, + # match the same versions we deploy locally + inputs ? import sources.flake-inputs { + root = ../.; + }, + # match the same version of opentofu that is deployed by the root flake + pkgs ? import inputs.nixpkgs { + inherit system; + }, +}: +let + inherit (pkgs) lib; + setup = pkgs.writeScriptBin "setup" '' + echo '${lib.strings.toJSON sources}' > .npins.json + rm -f .terraform.lock.hcl + rm -rf .terraform/ + tofu init + ''; +in +{ + # shell for testing TF directly + shell = pkgs.mkShellNoCC { + packages = [ + (import ./tf.nix { inherit lib pkgs; }) + pkgs.jaq + setup + ]; + }; + + tests = pkgs.callPackage ./tests.nix { }; + + # re-export inputs so they can be overridden granularly + # (they can't be accessed from the outside any other way) + inherit + sources + system + pkgs + ; +} diff --git a/launch/garage.nix b/launch/garage.nix new file mode 100644 index 00000000..559ca37f --- /dev/null +++ b/launch/garage.nix @@ -0,0 +1,34 @@ +{ pkgs, ... }: +let + ## NOTE: All of these secrets are publicly available in this source file + ## and will end up in the Nix store. We don't care as they are only ever + ## used for testing anyway. + ## + ## FIXME: Generate and store in NixOps4's state. + mastodonS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GK3515373e4c851ebaad366558"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34"; + }; + peertubeS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GK1f9feea9960f6f95ff404c9b"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "7295c4201966a02c2c3d25b5cea4a5ff782966a2415e3a196f91924631191395"; + }; + pixelfedS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GKb5615457d44214411e673b7b"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "5be6799a88ca9b9d813d1a806b64f15efa49482dbe15339ddfaf7f19cf434987"; + }; +in +{ + fediversity = { + garage.enable = true; + pixelfed = pixelfedS3KeyConfig { inherit pkgs; }; + mastodon = mastodonS3KeyConfig { inherit pkgs; }; + peertube = peertubeS3KeyConfig { inherit pkgs; }; + }; +} diff --git a/launch/main.tf b/launch/main.tf new file mode 100644 index 00000000..909577a5 --- /dev/null +++ b/launch/main.tf @@ -0,0 +1,159 @@ +locals { + system = "x86_64-linux" + # dependency paths pre-calculated from npins + pins = jsondecode(file("${path.root}/.npins.json")) + # nix path: expose pins, use nixpkgs in flake commands (`nix run`) + nix_path = "${join(":", [for name, path in local.pins : "${name}=${path}"])}:flake=${local.pins["nixpkgs"]}:flake" + # user-facing applications + application_configs = { + # FIXME: wrap applications at the interface to grab them in one go? + mastodon = { + cfg = var.mastodon + hostname = "test06" + } + pixelfed = { + cfg = var.pixelfed + hostname = "test04" + } + peertube = { + cfg = var.peertube + hostname = "test05" + } + } + # services shared between applications + peripherals = { for name, inst in { + garage = "test01" + } : name => { + hostname = inst + cfg = { + # enable if any user applications are enabled + enable = anytrue([for _, app in local.application_configs: app.cfg.enable]) + } + } + } +} + +# hash of our code directory, used in dev to trigger re-deploy +# FIXME settle for pwd when in /nix/store? +# FIXME calculate separately to reduce false positives +data "external" "hash" { + program = ["sh", "-c", "echo \"{\\\"hash\\\":\\\"$(nix-hash ..)\\\"}\""] +} + +# TF resource to build and deploy NixOS instances. +resource "terraform_data" "nixos" { + + for_each = {for name, inst in merge( + local.peripherals, + local.application_configs, + ) : name => inst if inst.cfg.enable} + + # trigger rebuild/deploy if (FIXME?) any potentially used config/code changed, + # preventing these (20+s, build being bottleneck) when nothing changed. + # terraform-nixos separates these to only deploy if instantiate changed, + # yet building even then - which may be not as bad using deploy on remote. + # having build/deploy one resource reflects wanting to prevent no-op rebuilds + # over preventing (with less false positives) no-op deployments, + # as i could not find a way to do prevent no-op rebuilds without merging them: + # - generic resources cannot have outputs, while we want info from the instantiation (unless built on host?). + # - `data` always runs, which is slow for deploy and especially build. + triggers_replace = [ + data.external.hash.result, + var.domain, + var.initialUser, + local.system, + each.key, + each.value, + ] + + provisioner "local-exec" { + # directory to run the script from. we use the TF project root dir, + # here as a path relative from where TF is run from. + # note that absolute paths can cause false positives in triggers, + # so are generally discouraged in TF. + working_dir = path.root + environment = { + # nix path used on build, lets us refer to e.g. nixpkgs like `` + NIX_PATH = local.nix_path + } + # TODO: refactor back to command="ignoreme" interpreter=concat([]) to protect sensitive data from error logs? + # TODO: build on target? + command = <<-EOF + set -euo pipefail + + # INSTANTIATE + command=( + nix-instantiate + --expr + 'let + os = import { + system = "${local.system}"; + configuration = { + # note interpolations here TF ones + imports = [ + # shared NixOS config + ${path.root}/shared.nix + # FIXME: separate template options by service + ${path.root}/options.nix + # for service `mastodon` import `mastodon.nix` + ${path.root}/${each.key}.nix + # FIXME: get VM details from TF + ${path.root}/../infra/test-machines/${each.value.hostname} + ]; + # nix path for debugging + nix.nixPath = [ "${local.nix_path}" ]; + ## FIXME: switch root authentication to users with password-less sudo, see #24 + users.users.root.openssh.authorizedKeys.keys = let + keys = import ../keys; + in attrValues keys.contributors ++ [ + # allow our panel vm access to the test machines + keys.panel + ]; + } // + # template parameters passed in from TF thru json + builtins.fromJSON "${replace(jsonencode({ + terraform = { + domain = var.domain + hostname = each.value.hostname + initialUser = var.initialUser + } + }), "\"", "\\\"")}"; + }; + in + # info we want to get back out + { + substituters = builtins.concatStringsSep " " os.config.nix.settings.substituters; + trusted_public_keys = builtins.concatStringsSep " " os.config.nix.settings.trusted-public-keys; + drv_path = os.config.system.build.toplevel.drvPath; + out_path = os.config.system.build.toplevel; + }' + ) + # instantiate the config in /nix/store + "$${command[@]}" -A out_path + # get the other info + json="$("$${command[@]}" --eval --strict --json)" + + # DEPLOY + declare substituters trusted_public_keys drv_path + # set our variables using the json object + eval "export $(echo $json | jaq -r 'to_entries | map("\(.key)=\(.value)") | @sh')" + # FIXME: de-hardcode domain + host="root@${each.value.hostname}.abundos.eu" # FIXME: #24 + buildArgs=( + --option extra-binary-caches https://cache.nixos.org/ + --option substituters $substituters + --option trusted-public-keys $trusted_public_keys + ) + sshOpts=( + -o BatchMode=yes + -o StrictHostKeyChecking=no + ) + # get the realized derivation to deploy + outPath=$(nix-store --realize "$drv_path" "$${buildArgs[@]}") + # deploy the config by nix-copy-closure + NIX_SSHOPTS="$${sshOpts[*]}" nix-copy-closure --to "$host" "$outPath" --gzip --use-substitutes + # switch the remote host to the config + ssh "$${sshOpts[@]}" "$host" "nix-env --profile /nix/var/nix/profiles/system --set $outPath; $outPath/bin/switch-to-configuration switch" + EOF + } +} diff --git a/launch/mastodon.nix b/launch/mastodon.nix new file mode 100644 index 00000000..26682f50 --- /dev/null +++ b/launch/mastodon.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: +let + mastodonS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GK3515373e4c851ebaad366558"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34"; + }; +in +{ + fediversity = { + mastodon = mastodonS3KeyConfig { inherit pkgs; } // { + enable = true; + }; + temp.cores = 1; # FIXME: should come from NixOps4 eventually + }; +} diff --git a/launch/options.nix b/launch/options.nix new file mode 100644 index 00000000..22344017 --- /dev/null +++ b/launch/options.nix @@ -0,0 +1,54 @@ +# TODO: could (part of) this be generated somehow? c.f #275 +{ + lib, + ... +}: +let + inherit (lib) types mkOption; + inherit (types) str enum submodule; +in +{ + options.terraform = { + domain = mkOption { + type = enum [ + "fediversity.net" + ]; + description = '' + Apex domain under which the services will be deployed. + ''; + default = "fediversity.net"; + }; + hostname = mkOption { + type = str; + description = '' + Internal name of the host, e.g. test01 + ''; + }; + initialUser = mkOption { + description = '' + Some services require an initial user to access them. + This option sets the credentials for such an initial user. + ''; + type = submodule { + options = { + displayName = mkOption { + type = str; + description = "Display name of the user"; + }; + username = mkOption { + type = str; + description = "Username for login"; + }; + email = mkOption { + type = str; + description = "User's email address"; + }; + password = mkOption { + type = str; + description = "Password for login"; + }; + }; + }; + }; + }; +} diff --git a/launch/peertube.nix b/launch/peertube.nix new file mode 100644 index 00000000..4124568a --- /dev/null +++ b/launch/peertube.nix @@ -0,0 +1,20 @@ +{ pkgs, ... }: +let + peertubeS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GK1f9feea9960f6f95ff404c9b"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "7295c4201966a02c2c3d25b5cea4a5ff782966a2415e3a196f91924631191395"; + }; +in +{ + fediversity = { + peertube = peertubeS3KeyConfig { inherit pkgs; } // { + enable = true; + ## NOTE: Only ever used for testing anyway. + ## + ## FIXME: Generate and store in NixOps4's state. + secretsFile = pkgs.writeText "secret" "574e093907d1157ac0f8e760a6deb1035402003af5763135bae9cbd6abe32b24"; + }; + }; +} diff --git a/launch/pixelfed.nix b/launch/pixelfed.nix new file mode 100644 index 00000000..75790409 --- /dev/null +++ b/launch/pixelfed.nix @@ -0,0 +1,16 @@ +{ pkgs, ... }: +let + pixelfedS3KeyConfig = + { pkgs, ... }: + { + s3AccessKeyFile = pkgs.writeText "s3AccessKey" "GKb5615457d44214411e673b7b"; + s3SecretKeyFile = pkgs.writeText "s3SecretKey" "5be6799a88ca9b9d813d1a806b64f15efa49482dbe15339ddfaf7f19cf434987"; + }; +in +{ + fediversity = { + pixelfed = pixelfedS3KeyConfig { inherit pkgs; } // { + enable = true; + }; + }; +} diff --git a/launch/resource.nix b/launch/resource.nix new file mode 100644 index 00000000..ec719e20 --- /dev/null +++ b/launch/resource.nix @@ -0,0 +1,43 @@ +{ + lib, + config, + ... +}: + +let + inherit (lib) attrValues elem mkDefault; + inherit (lib.attrsets) concatMapAttrs optionalAttrs; + inherit (lib.strings) removeSuffix; + + secretsPrefix = ../secrets; + secrets = import (secretsPrefix + "/secrets.nix"); + keys = import ../keys; + +in +{ + fediversityVm.hostPublicKey = mkDefault keys.systems.${config.fediversityVm.name}; + + ## The configuration of the machine. We strive to keep in this file only the + ## options that really need to be injected from the resource. Everything else + ## should go into the `./nixos` subdirectory. + imports = [ + ../infra/common/options.nix + ../infra/common/nixos + ]; + + ## Read all the secrets, filter the ones that are supposed to be readable + ## with this host's public key, and add them correctly to the configuration + ## as `age.secrets..file`. + age.secrets = concatMapAttrs ( + name: secret: + optionalAttrs (elem config.fediversityVm.hostPublicKey secret.publicKeys) { + ${removeSuffix ".age" name}.file = secretsPrefix + "/${name}"; + } + ) secrets; + + ## FIXME: switch root authentication to users with password-less sudo, see #24 + users.users.root.openssh.authorizedKeys.keys = attrValues keys.contributors ++ [ + # allow our panel vm access to the test machines + keys.panel + ]; +} diff --git a/launch/shared.nix b/launch/shared.nix new file mode 100644 index 00000000..46d6ccc5 --- /dev/null +++ b/launch/shared.nix @@ -0,0 +1,26 @@ +{ + pkgs, + config, + ... +}: +let + inherit (config.terraform) hostname domain initialUser; +in +{ + imports = [ + + + ../services/fediversity + ./resource.nix + ]; + fediversityVm.name = hostname; + fediversity = { + inherit domain; + temp.initialUser = { + inherit (initialUser) username email displayName; + # FIXME: disgusting, but nvm, this is going to be replaced by + # proper central authentication at some point + passwordFile = pkgs.writeText "password" initialUser.password; + }; + }; +} diff --git a/launch/shell.nix b/launch/shell.nix new file mode 100644 index 00000000..a6bdf202 --- /dev/null +++ b/launch/shell.nix @@ -0,0 +1 @@ +(import ./. { }).shell diff --git a/launch/tests.nix b/launch/tests.nix new file mode 100644 index 00000000..b86c7747 --- /dev/null +++ b/launch/tests.nix @@ -0,0 +1,26 @@ +{ lib, pkgs }: +let + defaults = { + virtualisation = { + memorySize = 2048; + cores = 2; + }; + }; + tf = pkgs.callPackage ./tf.nix { }; + tfEnv = pkgs.callPackage ./tf-env.nix { }; +in +lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; })) { + tf-validate = { + inherit defaults; + nodes.server = { + environment.systemPackages = [ + tf + tfEnv + ]; + }; + testScript = '' + server.wait_for_unit("multi-user.target") + server.succeed("${lib.getExe tf} -chdir='${tfEnv}/launch' validate") + ''; + }; +} diff --git a/launch/tf-env.nix b/launch/tf-env.nix new file mode 100644 index 00000000..a29114f9 --- /dev/null +++ b/launch/tf-env.nix @@ -0,0 +1,34 @@ +{ + lib, + pkgs, + sources ? import ../npins, + ... +}: +pkgs.stdenv.mkDerivation { + name = "tf-repo"; + src = + with lib.fileset; + toSource { + root = ../.; + # don't copy ignored files + fileset = intersection (gitTracked ../.) ../.; + }; + buildInputs = [ + (import ./tf.nix { inherit lib pkgs; }) + ]; + buildPhase = '' + runHook preBuild + pushd launch/ + # calculated pins + echo '${lib.strings.toJSON sources}' > .npins.json + # generate TF lock for nix's TF providers + tofu init -input=false + popd + runHook postBuild + ''; + installPhase = '' + runHook preInstall + cp -r . $out + runHook postInstall + ''; +} diff --git a/launch/tf.nix b/launch/tf.nix new file mode 100644 index 00000000..43a59298 --- /dev/null +++ b/launch/tf.nix @@ -0,0 +1,24 @@ +# FIXME: use overlays so this gets imported just once? +{ + lib, + pkgs, + ... +}: +let + tofuProvider = + provider: + provider.override (oldArgs: { + provider-source-address = + lib.replaceStrings [ "https://registry.terraform.io/providers" ] [ "registry.opentofu.org" ] + oldArgs.homepage; + }); + tf = pkgs.opentofu; + tfPlugins = ( + p: [ + p.external + ] + ); +in +# tf.withPlugins tfPlugins +# https://github.com/NixOS/nixpkgs/pull/358522 +tf.withPlugins (p: pkgs.lib.lists.map tofuProvider (tfPlugins p)) diff --git a/launch/variables.tf b/launch/variables.tf new file mode 100644 index 00000000..05f1b2f6 --- /dev/null +++ b/launch/variables.tf @@ -0,0 +1,51 @@ +# TODO: (partially) generate, say from nix modules, c.f. #275 + +variable "domain" { + type = string + default = "fediversity.net" +} + +variable "mastodon" { + type = object({ + enable = bool + }) + default = { + enable = false + } +} + +variable "pixelfed" { + type = object({ + enable = bool + }) + default = { + enable = false + } +} + +variable "peertube" { + type = object({ + enable = bool + }) + default = { + enable = false + } +} + +variable "initialUser" { + type = object({ + displayName = string + username = string + email = string + # TODO: mark (nested) credentials as sensitive + # https://discuss.hashicorp.com/t/is-it-possible-to-mark-an-attribute-of-an-object-as-sensitive/24649/2 + password = string + }) + # FIXME: remove default when the form provides this value, see #285 + default = { + displayName = "Testy McTestface" + username = "test" + email = "test@test.com" + password = "testtest" + } +} diff --git a/npins/sources.json b/npins/sources.json index 4971590b..a28d3256 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -25,6 +25,35 @@ "url": null, "hash": "1w2gsy6qwxa5abkv8clb435237iifndcxq0s79wihqw11a5yb938" }, + "disko": { + "type": "GitRelease", + "repository": { + "type": "GitHub", + "owner": "nix-community", + "repo": "disko" + }, + "pre_releases": false, + "version_upper_bound": null, + "release_prefix": null, + "submodules": false, + "version": "v1.11.0", + "revision": "cdf8deded8813edfa6e65544f69fdd3a59fa2bb4", + "url": "https://api.github.com/repos/nix-community/disko/tarball/v1.11.0", + "hash": "13brimg7z7k9y36n4jc1pssqyw94nd8qvgfjv53z66lv4xkhin92" + }, + "flake-inputs": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "fricklerhandwerk", + "repo": "flake-inputs" + }, + "branch": "main", + "submodules": false, + "revision": "559574c9cbb8af262f3944b67d60fbf0f6ad03c3", + "url": "https://github.com/fricklerhandwerk/flake-inputs/archive/559574c9cbb8af262f3944b67d60fbf0f6ad03c3.tar.gz", + "hash": "0gbhmp6x2vdzvfnsvqzal3g8f8hx2ia6r73aibc78kazf78m67x6" + }, "flake-parts": { "type": "Git", "repository": { diff --git a/panel/default.nix b/panel/default.nix index a9c20f84..85ea2afd 100644 --- a/panel/default.nix +++ b/panel/default.nix @@ -21,11 +21,18 @@ in pkgs.npins manage ]; - env = import ./env.nix { inherit lib pkgs; } // { - NPINS_DIRECTORY = toString ../npins; - CREDENTIALS_DIRECTORY = toString ./.credentials; - DATABASE_URL = "sqlite:///${toString ./src}/db.sqlite3"; - }; + env = + let + inherit (builtins) toString; + in + import ./env.nix { inherit lib pkgs; } + // { + NPINS_DIRECTORY = toString ../npins; + CREDENTIALS_DIRECTORY = toString ./.credentials; + DATABASE_URL = "sqlite:///${toString ./src}/db.sqlite3"; + # locally: use a fixed relative reference, so we can use our newest files without copying to the store + REPO_DIR = toString ../.; + }; shellHook = '' ${lib.concatStringsSep "\n" ( map (file: "ln -sf ${file.from} ${toString ./src/${file.to}}") package.generated diff --git a/panel/env.nix b/panel/env.nix index 07ce4193..b4ecffc5 100644 --- a/panel/env.nix +++ b/panel/env.nix @@ -3,16 +3,14 @@ pkgs, ... }: -let - inherit (builtins) toString; -in { - REPO_DIR = toString ../.; - # explicitly use nix, as e.g. lix does not have configurable-impure-env BIN_PATH = lib.makeBinPath [ - # explicitly use nix, as e.g. lix does not have configurable-impure-env - pkgs.nix - # nixops error maybe due to our flake git hook: executing 'git': No such file or directory + pkgs.lix + pkgs.bash + pkgs.coreutils + pkgs.openssh pkgs.git + pkgs.jaq # tf + (import ../launch/tf.nix { inherit lib pkgs; }) ]; } diff --git a/panel/nix/configuration.nix b/panel/nix/configuration.nix index ecf06e0f..14160936 100644 --- a/panel/nix/configuration.nix +++ b/panel/nix/configuration.nix @@ -29,6 +29,7 @@ let ((pkgs.formats.pythonVars { }).generate "settings.py" cfg.settings) (builtins.toFile "extra-settings.py" cfg.extra-settings) ]; + REPO_DIR = import ../../launch/tf-env.nix { inherit lib pkgs; }; }; python-environment = pkgs.python3.withPackages ( @@ -157,9 +158,7 @@ in }; }; - users.users.${name} = { - isNormalUser = true; - }; + users.users.${name}.isNormalUser = true; users.groups.${name} = { }; systemd.services.${name} = { @@ -167,6 +166,7 @@ in after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = [ + pkgs.openssh python-environment manage-service ]; diff --git a/panel/nix/package.nix b/panel/nix/package.nix index e13e015a..083170d6 100644 --- a/panel/nix/package.nix +++ b/panel/nix/package.nix @@ -1,5 +1,6 @@ { lib, + pkgs, sqlite, python3, python3Packages, @@ -14,7 +15,7 @@ let root = ../src; fileset = intersection (gitTracked ../../.) ../src; }; - pyproject = with lib; fromTOML pyproject-toml; + pyproject = fromTOML pyproject-toml; # TODO: define this globally name = "panel"; # TODO: we may want this in a file so it's easier to read statically @@ -89,7 +90,9 @@ python3.pkgs.buildPythonPackage { mkdir -p $out/bin cp -v ${src}/manage.py $out/bin/manage.py chmod +x $out/bin/manage.py - wrapProgram $out/bin/manage.py --prefix PYTHONPATH : "$PYTHONPATH" + wrapProgram $out/bin/manage.py \ + --set REPO_DIR "${import ../../launch/tf-env.nix { inherit lib pkgs; }}" \ + --prefix PYTHONPATH : "$PYTHONPATH" ${lib.concatStringsSep "\n" ( map (file: "cp ${file.from} $out/${python3.sitePackages}/${file.to}") generated )} diff --git a/panel/nix/python-packages/django-pydantic-field/default.nix b/panel/nix/python-packages/django-pydantic-field/default.nix index 2648a0db..74349e3e 100644 --- a/panel/nix/python-packages/django-pydantic-field/default.nix +++ b/panel/nix/python-packages/django-pydantic-field/default.nix @@ -1,3 +1,4 @@ +# TODO upstream, see #248 { lib, buildPythonPackage, diff --git a/panel/src/panel/views.py b/panel/src/panel/views.py index 341a9135..e44202ed 100644 --- a/panel/src/panel/views.py +++ b/panel/src/panel/views.py @@ -1,6 +1,7 @@ from enum import Enum import json import subprocess +import logging import os from django.urls import reverse_lazy @@ -19,6 +20,8 @@ from pydantic import BaseModel from panel import models, settings from panel.configuration import schema +logger = logging.getLogger(__name__) + class Index(TemplateView): template_name = 'index.html' @@ -106,22 +109,23 @@ class DeploymentStatus(ConfigurationForm): } env = { "PATH": settings.bin_path, + # "TF_LOG": "info", + } | { # pass in form info to our deployment - "DEPLOYMENT": config.json() + # FIXME: ensure sensitive info is protected + f"TF_VAR_{k}": v if isinstance(v, str) else json.dumps(v) for k, v in config.json().items() } + logger.info("env: %s", env) + cwd = f"{settings.repo_dir}/launch" cmd = [ - "nix", - "develop", - "--extra-experimental-features", - "configurable-impure-env", - "--command", - "nixops4", + "tofu", + # f"-chdir={cwd}", "apply", - "test", + f"-state={cwd}/terraform.tfstate", # FIXME: separate users' state + "--auto-approve", + "-lock=false", + "-parallelism=1" # limit OOM risk ] - deployment_result = subprocess.run( - cmd, - cwd=settings.repo_dir, - env=env, - ) + deployment_result = subprocess.run(cmd, cwd=cwd, env=env) + logger.debug("deployment_result: %s", deployment_result) return deployment_result, config