From 026a0d0180f921be989ba65b567bc92410727d01 Mon Sep 17 00:00:00 2001 From: Andrea Ciceri Date: Tue, 11 Apr 2023 23:45:52 +0200 Subject: [PATCH] `hydra`, `cgit` and `vm-sala` on `mothership` --- hosts/mothership/default.nix | 5 + modules/cgit/config.nix | 57 +++++++++ modules/cgit/default.nix | 5 + modules/hydra/config.nix | 8 ++ modules/hydra/default.nix | 206 +++++++++++++++++++++++++++++++++ modules/hydra/jobsets.nix | 73 ++++++++++++ modules/nginx-base/default.nix | 19 +++ modules/nix-serve/default.nix | 29 +++++ modules/vm-sala/default.nix | 87 ++++++++++++++ 9 files changed, 489 insertions(+) create mode 100644 modules/cgit/config.nix create mode 100644 modules/hydra/config.nix create mode 100644 modules/hydra/default.nix create mode 100644 modules/hydra/jobsets.nix create mode 100644 modules/nginx-base/default.nix create mode 100644 modules/nix-serve/default.nix create mode 100644 modules/vm-sala/default.nix diff --git a/hosts/mothership/default.nix b/hosts/mothership/default.nix index 04857e4..b9524d7 100644 --- a/hosts/mothership/default.nix +++ b/hosts/mothership/default.nix @@ -13,6 +13,11 @@ "mosh" "ccr" "nix" + "vm-sala" + "hydra" + "nix-serve" + "cgit" + "docker" ]; ccr = { diff --git a/modules/cgit/config.nix b/modules/cgit/config.nix new file mode 100644 index 0000000..89820ff --- /dev/null +++ b/modules/cgit/config.nix @@ -0,0 +1,57 @@ +{ + lib, + pkgs, + ... +}: let + repos-path = "/var/lib/cgit-repos"; + cgit-setup-repos = + pkgs.writers.writePython3 "cgit-setup-repos" { + libraries = with pkgs.python3Packages; [PyGithub]; + } '' + from github import Github + from pathlib import Path + + c = Path("${repos-path}") + c.unlink(missing_ok=True) + + with open(c, "w") as f: + for repo in Github().get_user("aciceri").get_repos(): + f.writelines([ + f"repo.url={repo.name}\n" + f"repo.path=/home/ccr/projects/aciceri/{repo.name}/.git\n" + f"repo.desc={repo.description}\n" + ]) + ''; +in { + services.nginx.virtualHosts."git.aciceri.dev" = { + cgit = { + enable = true; + css = "/custom.css"; + # scan-path = "/home/ccr/projects/aciceri"; + virtual-root = "/"; + cache-size = 1000; + include = [ + (builtins.toString (pkgs.writeText "cgit-extra" '' + source-filter=${pkgs.cgit-pink}/lib/cgit/filters/syntax-highlighting.py + about-filter=${pkgs.cgit-pink}/lib/cgit/filters/about-formatting.sh + '')) + repos-path + ]; + }; + forceSSL = true; + enableACME = true; + # locations."/" = { + # proxyPass = "http://127.0.0.1:${builtins.toString config.services.hydra.port}"; + # }; + }; + + systemd.services.cgit-setup-repos = { + description = "Update GitHub personal repos for cgit"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + wantedBy = ["multi-user.target"]; + script = builtins.toString cgit-setup-repos; + }; +} diff --git a/modules/cgit/default.nix b/modules/cgit/default.nix index 00ce56d..a6439f8 100644 --- a/modules/cgit/default.nix +++ b/modules/cgit/default.nix @@ -24,6 +24,11 @@ with lib; let pkgs.writeText name (lib.generators.toKeyValue {listsAsDuplicateKeys = true;} values); }; in { + imports = [ + ../nginx-base + ./config.nix + ]; + options.services.nginx.virtualHosts = mkOption { type = types.attrsOf (types.submodule ({config, ...}: let cfg = config.cgit; diff --git a/modules/hydra/config.nix b/modules/hydra/config.nix new file mode 100644 index 0000000..39e8e7e --- /dev/null +++ b/modules/hydra/config.nix @@ -0,0 +1,8 @@ +{ + services.my-hydra.repos = { + emacs = {}; + nixfleet = {}; + trotten = {}; + blog = {}; + }; +} diff --git a/modules/hydra/default.nix b/modules/hydra/default.nix new file mode 100644 index 0000000..e9fe3d7 --- /dev/null +++ b/modules/hydra/default.nix @@ -0,0 +1,206 @@ +{ + lib, + config, + pkgs, + ... +}: let + cfg = config.services.my-hydra; + toSpec = { + name, + owner, + ... + }: let + spec = { + enabled = 1; + hidden = false; + description = "Declarative specification jobset automatically generated"; + checkinterval = 120; + schedulingshares = 10000; + enableemail = false; + emailoverride = ""; + keepnr = 1; + nixexprinput = "src"; + nixexprpath = "jobsets.nix"; + inputs = { + src = { + type = "path"; + value = pkgs.writeTextFile { + name = "src"; + text = builtins.readFile ./jobsets.nix; + destination = "/jobsets.nix"; + }; + emailresponsible = false; + }; + repoInfoPath = { + type = "path"; + value = pkgs.writeTextFile { + name = "repo"; + text = builtins.toJSON { + inherit name owner; + }; + }; + emailresponsible = false; + }; + prs = { + type = "githubpulls"; + value = "${owner} ${name}"; + emailresponsible = false; + }; + }; + }; + drv = pkgs.writeTextFile { + name = "hydra-jobset-specification-${name}"; + text = builtins.toJSON spec; + destination = "/spec.json"; + }; + in "${drv}"; +in { + imports = [ + ./config.nix + ../nginx-base + ]; + + options.services.my-hydra = { + domain = lib.mkOption { + type = lib.types.str; + default = "hydra.aciceri.dev"; + }; + repos = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule ({ + name, + config, + ... + }: { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + }; + owner = lib.mkOption { + type = lib.types.str; + default = "aciceri"; + }; + description = lib.mkOption { + type = lib.types.str; + default = config.homepage; + }; + homepage = lib.mkOption { + type = lib.types.str; + default = "https://github.com/${config.owner}/${config.name}"; + }; + reportStatus = lib.mkOption { + type = lib.types.bool; + default = true; + }; + }; + })); + default = {}; + }; + }; + + config = { + # TODO manage `hydra` user ssh key declaratively + nix.extraOptions = '' + allowed-uris = https://github.com/ git://git.savannah.gnu.org/ + ''; + + services.hydra = { + enable = true; + hydraURL = "https://${cfg.domain}"; + notificationSender = "hydra@mothership.fleet"; + buildMachinesFiles = []; + useSubstitutes = true; + extraConfig = + '' + + include ${config.age.secrets.hydra-github-token.path} + + '' + + (lib.concatMapStrings (repo: + lib.optionalString repo.reportStatus + '' + + jobs = ${repo.name}.* + excludeBuildFromContext = 1 + useShortContext = 1 + + '') (builtins.attrValues cfg.repos)); + }; + + systemd.services.hydra-setup = { + description = "Hydra CI setup"; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + wantedBy = ["multi-user.target"]; + requires = ["hydra-init.service"]; + after = ["hydra-init.service"]; + environment = builtins.removeAttrs (config.systemd.services.hydra-init.environment) ["PATH"]; + script = + '' + PATH=$PATH:${lib.makeBinPath (with pkgs; [yq-go curl config.services.hydra.package])} + PASSWORD="$(cat ${config.age.secrets.hydra-admin-password.path})" + if [ ! -e ~hydra/.setup-is-complete ]; then + hydra-create-user admin \ + --full-name "Andrea Ciceri" \ + --email-address "andrea.ciceri@autistici.org" \ + --password "$PASSWORD" \ + --role admin + touch ~hydra/.setup-is-complete + fi + + curl --head -X GET --retry 5 --retry-connrefused --retry-delay 1 http://localhost:3000 + + CURRENT_REPOS=$(curl -s -H "Accept: application/json" http://localhost:3000 | yq ".[].name") + DECLARED_REPOS="${lib.concatStringsSep " " (builtins.attrNames cfg.repos)}" + + curl -H "Accept: application/json" \ + -H 'Origin: http://localhost:3000' \ + -H 'Content-Type: application/json' \ + -d "{\"username\": \"admin\", \"password\": \"$PASSWORD\"}" \ + --request "POST" localhost:3000/login \ + --cookie-jar cookie + + for repo in $CURRENT_REPOS; do + echo $repo + [[ ! "$DECLARED_REPOS" =~ (\ |^)$repo(\ |$) ]] && \ + curl -H "Accept: application/json" \ + --request "DELETE" \ + --cookie cookie \ + http://localhost:3000/project/$repo + done + '' + + lib.concatMapStrings (repo: '' + curl -H "Accept: application/json" \ + -H 'Content-Type: application/json' \ + --request "PUT" \ + localhost:3000/project/${repo.name} \ + --cookie cookie \ + -d '{ + "name": "${repo.name}", + "displayname": "${repo.name}", + "description": "${repo.description}", + "homepage": "${repo.homepage}", + "owner": "admin", + "enabled": true, + "visible": true, + "declarative": { + "file": "spec.json", + "type": "path", + "value": "${toSpec repo}" + } + }' + '') (builtins.attrValues cfg.repos) + + '' + rm cookie + ''; + }; + + services.nginx.virtualHosts."${cfg.domain}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${builtins.toString config.services.hydra.port}"; + }; + }; + }; +} diff --git a/modules/hydra/jobsets.nix b/modules/hydra/jobsets.nix new file mode 100644 index 0000000..fed7df8 --- /dev/null +++ b/modules/hydra/jobsets.nix @@ -0,0 +1,73 @@ +{ + repoInfoPath, + prs, + ... +}: let + minutes = 60; + hours = 60 * minutes; + days = 24 * hours; + filterAttrs = pred: set: + builtins.listToAttrs (builtins.concatMap (name: let + v = set.${name}; + in + if pred name v + then [ + { + inherit name; + value = v; + } + ] + else []) (builtins.attrNames set)); + mapAttrs' = f: set: + builtins.listToAttrs (map (attr: f attr set.${attr}) (builtins.attrNames set)); + + mkJobset = { + enabled ? 1, + hidden ? false, + type ? 1, + description ? "", + checkinterval ? 5 * minutes, + schedulingshares ? 100, + enableemail ? false, + emailoverride ? "", + keepnr ? 1, + flake, + } @ args: {inherit enabled hidden type description checkinterval schedulingshares enableemail emailoverride keepnr flake;}; + + mkSpec = contents: let + escape = builtins.replaceStrings [''"''] [''\"'']; + contentsJson = builtins.toJSON contents; + in + builtins.derivation { + name = "spec.json"; + system = "x86_64-linux"; + preferLocalBuild = true; + allowSubstitutes = false; + builder = "/bin/sh"; + args = [ + (builtins.toFile "builder.sh" '' + echo "${escape contentsJson}" > $out + '') + ]; + }; + + repo = builtins.fromJSON (builtins.readFile repoInfoPath); + + pullRequests = builtins.fromJSON (builtins.readFile prs); + pullRequestsToBuild = filterAttrs (n: pr: pr.head.repo != null && pr.head.repo.owner.login == repo.owner && pr.head.repo.name == repo.name) pullRequests; +in { + jobsets = mkSpec ({ + master = mkJobset { + description = "${repo.name}'s master branch"; + flake = "git+ssh://git@github.com/${repo.owner}/${repo.name}?ref=master"; + }; + } + // (mapAttrs' (n: pr: { + name = "pullRequest_${n}"; + value = mkJobset { + description = pr.title; + flake = "git+ssh://git@github.com/${repo.owner}/${repo.name}?ref=${pr.head.ref}"; + }; + }) + pullRequests)); +} diff --git a/modules/nginx-base/default.nix b/modules/nginx-base/default.nix new file mode 100644 index 0000000..8baeece --- /dev/null +++ b/modules/nginx-base/default.nix @@ -0,0 +1,19 @@ +{ + security.acme = { + acceptTerms = true; + defaults.email = "andrea.ciceri@autistici.org"; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + }; +} diff --git a/modules/nix-serve/default.nix b/modules/nix-serve/default.nix new file mode 100644 index 0000000..7fd1f2c --- /dev/null +++ b/modules/nix-serve/default.nix @@ -0,0 +1,29 @@ +{ + config, + lib, + ... +}: let + cfg = config.services.my-nix-serve; +in { + imports = [../nginx-base]; + options.services.my-nix-serve = { + domain = lib.mkOption { + type = lib.types.str; + default = "cache.aciceri.dev"; + }; + }; + config = { + services.nix-serve = { + enable = true; + secretKeyFile = config.age.secrets.cache-private-key.path; + # Public key: cache.aciceri.dev:4e9sFjWPUOjGwTJE98PXinJJZLwPz0m5nKsAe63MY3E= + }; + services.nginx.virtualHosts."${cfg.domain}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${builtins.toString config.services.nix-serve.port}"; + }; + }; + }; +} diff --git a/modules/vm-sala/default.nix b/modules/vm-sala/default.nix new file mode 100644 index 0000000..6347edf --- /dev/null +++ b/modules/vm-sala/default.nix @@ -0,0 +1,87 @@ +{ + pkgs, + lib, + fleetFlake, + ... +}: { + security.polkit.enable = true; + virtualisation.libvirtd.enable = true; + + networking.firewall.allowedTCPPorts = [ + 2222 + ]; + + imports = [../nginx-base]; + + services.nginx.virtualHosts."git.slavni.aciceri.dev" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:13000"; + }; + }; + + systemd.services.vm-sala = let + initial-config = fleetFlake.inputs.nixos-generators.nixosGenerate { + system = "x86_64-linux"; + modules = [ + fleetFlake.inputs.nixos-vscode-server.nixosModule + ({ + modulesPath, + lib, + config, + ... + }: { + services.vscode-server = { + enable = true; + enableFHS = true; + }; + system.build.qcow = lib.mkForce (import "${toString modulesPath}/../lib/make-disk-image.nix" { + inherit lib config pkgs; + diskSize = 50 * 1024; + format = "qcow2"; + partitionTableType = "hybrid"; + }); + services.openssh.enable = true; + environment.systemPackages = with pkgs; [ + vim + git + htop + ]; + users.users.root = { + password = "password"; + openssh.authorizedKeys.keys = [ + (import "${fleetFlake.outPath}/lib").keys.users.ccr-ssh + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC7qikwR0a4LDoMQIVvtX+gyJ41OsAOWe8RcXc4ksIBP9x1nCcSrItlgC2soADB77QGIgyeyLGmnTCtMj5/s8NdREAycPeXLii1WRakbT7oZ/hTEmvAgObpadeYJn3LhaUDNtmsnAqqh2pRpCpSsAdhfIt+YyV4VgRYSfaa12Ozp/H6NI9bJMNttmG8TmY9V4zyskV9bE+up9y8Yuck2bZV/GjQe6UWgxsiC3XPSrFFGuxyaFMRGsc8h86xVwTAmwaHESEFhRvHD4EtdPNss0jqmSI6m4AoSZQ2wq7eiH8ZiYzERF0FnEFf4UsyOTM7j78bfogNLfKrdcEIPLrNNFFc3Iarfe9CJn3DdSnwwPnhFU1MBBXSbGOp1IyN3+gpjHwLMPzozlDAVqOwx6XpnpF78VpeknFBHCbkcKC/R0MXzqf900wH3i2HvfB7v9e9EUFzCQ0vUC+1Og+BFw3F5VSo0QtZyLc4BJ/akBs5mEE6TnuWQa/GhlY8Lz7wbcV1AaBOAQdx+NTbL/+Q31SJ1XsXtGtXCrwMY9noUTyVfpGVXo7Mn4HSslmeQ9SKfYKjyetkBR/1f8a47O3rCggjBy1AlfLjgbERnXy+0Ma4T8lnPZAKt3s9Ya1JupZ7SO7D5j7WfPKP+60c372/RrX1wXsxEeLvBJ0jd8GnSCXDOuvHTQ==" + ]; + }; + }) + ]; + format = "qcow"; + }; + image = "${initial-config}/nixos.qcow2"; + start-vm = pkgs.writeShellApplication { + name = "start-vm"; + runtimeInputs = with pkgs; [qemu]; + text = '' + [ ! -f /var/lib/vm-sala/nixos.qcow2 ] && \ + install ${image} /var/lib/vm-sala + + qemu-system-x86_64 \ + -enable-kvm \ + -cpu host \ + -smp 2 \ + -m 4096 \ + -nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22,hostfwd=tcp::13000-:3000 \ + -nographic \ + -drive file=/var/lib/vm-sala/nixos.qcow2 + ''; + }; + in { + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + serviceConfig = { + ExecStart = "${start-vm}/bin/start-vm"; + }; + }; +}