Procházet zdrojové kódy

feat(odin): add Gogs container with dark theme, admin provisioning, and cloudflared ingress

- Add NixOS module for Gogs with theme (dark variant selection) and adminUser options
- Add gogs-themes package (kristuff/gogs-themes) with 7 dark accent variants
- Add gogs container config at hosts/odin/containers/gogs.nix
- Wire container into odin default.nix with bind-mounted age secret
- Route git.t5.st through Cloudflare tunnel to container
- Move cloudflared ingress config from immich.nix to cloudflared.nix
- Register gogs module in modules/nixos/default.nix
- Add gogs-admin age secret (mode 0444)
- Wrap gogs binary with git and openssh in PATH
- Update flake.lock (secrets input rev bump)
- Rename grist-core package from directory to flat file
Zander Hawke před 1 dnem
rodič
revize
1dd1940b17

+ 4 - 4
flake.lock

@@ -674,11 +674,11 @@
     },
     "secrets": {
       "locked": {
-        "lastModified": 1766322995,
-        "narHash": "sha256-0pCdXKlpRNIei0sb3iMzWI8pytMRNa9Nx7Jr6AY4d14=",
+        "lastModified": 1781947547,
+        "narHash": "sha256-Z4OGXsSdFPCcyh67diybzoR2LqmrfuIWab0x1m7DBI4=",
         "ref": "refs/heads/master",
-        "rev": "fc077853141b2f902b2767251c73379c915f20c7",
-        "revCount": 26,
+        "rev": "35fbe2dc257c7f7b92541755b8936926db55ba17",
+        "revCount": 27,
         "type": "git",
         "url": "ssh://[email protected]/control/secrets.git"
       },

+ 49 - 0
hosts/odin/containers/gogs.nix

@@ -0,0 +1,49 @@
+{ outputs, ... }:
+{
+  imports = [
+    # TODO: auto-import via `outputs.modules.nixos`
+    outputs.modules.global.nix-config
+    outputs.modules.nixos.gogs
+  ];
+
+  services.gogs = {
+    enable = true;
+
+    theme = "dark-blue";
+    adminUser = {
+      name = "control";
+      email = "[email protected]";
+      passwordFile = "/run/secrets/gogs-admin";
+    };
+
+    settings = {
+      auth.DISABLE_REGISTRATION = true;
+
+      server = {
+        DOMAIN = "git.t5.st";
+        EXTERNAL_URL = "https://git.t5.st/";
+        HTTP_PORT = 3000;
+        SSH_PORT = 2222;
+        START_SSH_SERVER = true;
+      };
+
+      service.SHOW_REGISTRATION_BUTTON = false;
+    };
+  };
+
+  networking = {
+    firewall.allowedTCPPorts = [ 3000 2222 ];
+    interfaces.eth0 = {
+      ipv4.addresses = [{
+        address = "192.168.1.3";
+        prefixLength = 24;
+      }];
+    };
+    defaultGateway = "192.168.1.1";
+    nameservers = [ "8.8.8.8" ];
+    useDHCP = false;
+  };
+
+  boot.isContainer = true;
+  system.stateVersion = "26.05";
+}

+ 20 - 0
hosts/odin/default.nix

@@ -4,6 +4,9 @@
 , outputs
 , ...
 }:
+let
+  age = config.age;
+in
 {
   imports = [
     # TODO: auto-import via `outputs.modules.nixos`
@@ -65,6 +68,23 @@
     config = import ./containers/grist.nix;
   };
 
+  containers.gogs = {
+    autoStart = false;
+    privateNetwork = true;
+    hostAddress = "192.168.1.1";
+    localAddress = "192.168.1.3";
+    specialArgs = { inherit outputs; };
+
+    bindMounts = {
+      "/run/secrets/gogs-admin" = {
+        hostPath = config.age.secrets."odin/services/gogs-admin".path;
+        isReadOnly = true;
+      };
+    };
+
+    config = import ./containers/gogs.nix;
+  };
+
   services.caddy.virtualHosts.grist = {
     hostName = "grist.{$DOMAIN}";
     extraConfig = ''

+ 18 - 2
hosts/odin/services/cloudflared.nix

@@ -1,5 +1,21 @@
 { config, ... }:
+let
+  immich = config.services.immich;
+  gogs = config.containers.gogs;
+in
 {
-  services.cloudflared.enable = true;
-  services.cloudflared.certificateFile = config.age.secrets."odin/services/cloudflared".path;
+  services.cloudflared = {
+    enable = true;
+    certificateFile = config.age.secrets."odin/services/cloudflared".path;
+
+    tunnels."71c89a7f-2467-444c-9fda-4864860dc8c4" = {
+      default = "http_status:404";
+      credentialsFile = config.age.secrets."odin/services/cloudflared-tunnel".path;
+
+      ingress = {
+        "photos.t5.st".service = "http://${immich.host}:${toString immich.port}";
+        "git.t5.st".service = "http://${gogs.localAddress}:3000";
+      };
+    };
+  };
 }

+ 0 - 6
hosts/odin/services/immich.nix

@@ -33,12 +33,6 @@ in
     };
   };
 
-  services.cloudflared.tunnels."71c89a7f-2467-444c-9fda-4864860dc8c4" = {
-    credentialsFile = config.age.secrets."odin/services/cloudflared-tunnel".path;
-    default = "http_status:404";
-    ingress."${domain}".service = "http://${cfg.host}:${toString cfg.port}";
-  };
-
   services.caddy.virtualHosts.immich = {
     hostName = "photos.odin.t5.st";
     extraConfig = ''

+ 4 - 0
hosts/odin/system/age.nix

@@ -16,6 +16,10 @@ in
     };
     "odin/services/cloudflared".file = secrets."odin/services/cloudflared.age";
     "odin/services/cloudflared-tunnel".file = secrets."odin/services/cloudflared-tunnel.age";
+    "odin/services/gogs-admin" = {
+      file = secrets."odin/services/gogs-admin.age";
+      mode = "0444";
+    };
     "odin/services/mollysocket".file = secrets."odin/services/mollysocket.age";
     "odin/services/nullmailer" = {
       file = secrets."odin/services/nullmailer.age";

+ 1 - 0
modules/nixos/default.nix

@@ -1,3 +1,4 @@
 {
   grist = import ./grist.nix;
+  gogs = import ./gogs.nix;
 }

+ 120 - 20
modules/nixos/gogs.nix

@@ -1,16 +1,43 @@
-{
-  config,
-  lib,
-  pkgs,
-  ...
-}:
+{ config
+, lib
+, pkgs
+, ... }:
+
+with lib;
 
 let
   cfg = config.services.gogs;
-  iniFormat = pkgs.formats.ini { };
-  configFile = iniFormat.generate "gogs.ini" cfg.settings;
-in
-{
+
+  iniFormat = pkgs.formats.ini {};
+  defaultConfig = lib.recursiveUpdate {
+    server.RUN_USER = cfg.user;
+    security.INSTALL_LOCK = true;
+    database.TYPE = "sqlite3";
+    database.PATH = "${cfg.stateDir}/data/gogs.db";
+  } cfg.settings;
+  configFile = iniFormat.generate "gogs.ini" defaultConfig;
+
+  themeVariants = {
+    dark         = "kristuff.gogs.dark.min.css";
+    dark-blue    = "kristuff.gogs.dark-accent-blue.min.css";
+    dark-green   = "kristuff.gogs.dark-accent-green.min.css";
+    dark-magenta = "kristuff.gogs.dark-accent-magenta.min.css";
+    dark-orange  = "kristuff.gogs.dark-accent-orange.min.css";
+    dark-red     = "kristuff.gogs.dark-accent-red.min.css";
+    dark-yellow  = "kristuff.gogs.dark-accent-yellow.min.css";
+  };
+  themeFile = if cfg.theme == null then null
+    else if builtins.hasAttr cfg.theme themeVariants
+    then "${pkgs.gogs-themes}/dist/${themeVariants.${cfg.theme}}"
+    else cfg.theme;
+  themeSetupScript = pkgs.writeShellScript "gogs-theme-setup" ''
+    mkdir -p ${cfg.stateDir}/custom/public/css ${cfg.stateDir}/custom/templates/inject
+    cp -f ${themeFile} ${cfg.stateDir}/custom/public/css/theme.css
+    cat > ${cfg.stateDir}/custom/templates/inject/head.tmpl << 'EOF'
+    <link rel="stylesheet" href="{{AppSubURL}}/css/theme.css">
+    EOF
+  '';
+in {
   options.services.gogs = {
     enable = lib.mkEnableOption "Gogs Git service";
 
@@ -18,13 +45,13 @@ in
 
     user = lib.mkOption {
       type = lib.types.str;
-      default = "gogs";
+      default = "git";
       description = "User account under which Gogs runs.";
     };
 
     group = lib.mkOption {
       type = lib.types.str;
-      default = "gogs";
+      default = "git";
       description = "Group under which Gogs runs.";
     };
 
@@ -45,6 +72,61 @@ in
       '';
     };
 
+    theme = lib.mkOption {
+      type = lib.types.nullOr (
+        lib.types.either (lib.types.enum [
+          "dark"
+          "dark-blue"
+          "dark-green"
+          "dark-magenta"
+          "dark-orange"
+          "dark-red"
+          "dark-yellow"
+        ]) lib.types.path
+      );
+      default = null;
+      example = "dark-blue";
+      description = ''
+        Dark theme for Gogs. Can be:
+        - A string selecting a built-in variant from pkgs.gogs-themes:
+          dark, dark-blue, dark-green, dark-magenta, dark-orange, dark-red, dark-yellow.
+        - A path or package pointing to a custom CSS file.
+        Set to null to disable theming.
+      '';
+    };
+
+    adminUser = lib.mkOption {
+      type = lib.types.nullOr (lib.types.submodule {
+        options = {
+          name = lib.mkOption {
+            type = lib.types.str;
+            description = "Admin username.";
+          };
+          email = lib.mkOption {
+            type = lib.types.str;
+            description = "Admin email address.";
+          };
+          passwordFile = lib.mkOption {
+            type = lib.types.path;
+            description = ''
+              File containing the admin password. Must be readable by root.
+              Recommended to use an age secret managed by agenix.
+            '';
+          };
+        };
+      });
+      default = null;
+      example = {
+        email = "[email protected]";
+        passwordFile = "/run/secrets/gogs-admin-password";
+      };
+      description = ''
+        Admin user to create on first startup. Uses gogs admin create-user
+        via ExecStartPost. Idempotent — silently skips if the user already
+        exists. Set to null to skip.
+      '';
+    };
+
     settings = lib.mkOption {
       type = iniFormat.type;
       default = { };
@@ -73,7 +155,7 @@ in
     };
   };
 
-  config = lib.mkIf cfg.enable {
+  config = mkIf cfg.enable {
 
     users.users.${cfg.user} = {
       isSystemUser = true;
@@ -89,6 +171,8 @@ in
       "d ${cfg.stateDir}/repositories 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.stateDir}/data 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.stateDir}/log 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.stateDir}/custom/public/css 0755 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.stateDir}/custom/templates/inject 0755 ${cfg.user} ${cfg.group} -"
     ];
 
     systemd.services.gogs = {
@@ -97,29 +181,45 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      environment = {
+        GOGS_WORK_DIR = cfg.stateDir;
+        GOGS_CUSTOM = "${cfg.stateDir}/custom";
+      };
+
       serviceConfig = {
         Type = "simple";
 
         User = cfg.user;
         Group = cfg.group;
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
 
+        StateDirectory = "gogs";
+        StateDirectoryMode = "0750";
         WorkingDirectory = cfg.stateDir;
 
-        ExecStart = "${lib.getExe cfg.package} web --config ${configFile}";
+        ExecStart =
+          "${getExe cfg.package} web --config ${configFile}";
+        ExecStartPre = lib.mkIf (themeFile != null) [
+          "+${themeSetupScript}"
+        ];
 
         Restart = "on-failure";
 
-        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
-
         NoNewPrivileges = true;
         PrivateTmp = true;
         ProtectSystem = "strict";
         ProtectHome = true;
-
-        ReadWritePaths = [
-          cfg.stateDir
-        ];
       };
+
+      postStart = lib.mkIf (cfg.adminUser != null) ''
+        ${lib.getExe cfg.package} admin create-user \
+          --name ${lib.escapeShellArg cfg.adminUser.name} \
+          --password "$(cat ${cfg.adminUser.passwordFile})" \
+          --email ${lib.escapeShellArg cfg.adminUser.email} \
+          --admin \
+          --config ${configFile} \
+          >/dev/null 2>&1 || true
+      '';
     };
   };
 }

+ 2 - 1
packages/default.nix

@@ -17,9 +17,10 @@
 {
   aerospace-tmux-focus = pkgs.callPackage ./aerospace-tmux-focus.nix { };
   default = pkgs.callPackage ./quick-setup.nix { };
-  grist-core = pkgs.callPackage ./grist-core { };
+  grist-core = pkgs.callPackage ./grist-core.nix { };
   go-avahi-cname = pkgs.callPackage ./go-avahi-cname.nix { };
   gogs = pkgs.callPackage ./gogs.nix { };
+  gogs-themes = pkgs.callPackage ./gogs-themes.nix { };
   hello = pkgs.callPackage ./hello.nix { };
   nct6775-pwm-udev-package = pkgs.callPackage ./nct6775-pwm-udev-package.nix { };
   photo-cli = pkgs.callPackage ./photo-cli.nix { };

+ 24 - 0
packages/gogs-themes.nix

@@ -0,0 +1,24 @@
+{ lib, fetchFromGitHub, stdenvNoCC }:
+stdenvNoCC.mkDerivation {
+  pname = "gogs-themes";
+  version = "unstable-2022-01-01";
+
+  src = fetchFromGitHub {
+    owner = "kristuff";
+    repo = "gogs-themes";
+    rev = "main";
+    hash = "sha256-GWcKFOTBYDOSRPImtODuTrppJQlYZyIBGN/7rVgzZ2Q=";
+  };
+
+  installPhase = ''
+    mkdir -p $out/dist
+    cp -r dist/*.css $out/dist/
+  '';
+
+  meta = {
+    description = "Dark & responsive themes for Gogs";
+    homepage = "https://github.com/kristuff/gogs-themes";
+    license = lib.licenses.mit;
+    platforms = lib.platforms.all;
+  };
+}

+ 7 - 0
packages/gogs.nix

@@ -2,6 +2,9 @@
   lib,
   fetchFromGitHub,
   buildGoModule,
+  makeWrapper,
+  git,
+  openssh,
   pam,
   stdenv,
 }:
@@ -18,6 +21,7 @@ buildGoModule rec {
   };
 
   vendorHash = "sha256-k+PdCynhcnerlk7Ut+GbA0P7KEC/eDW8RY1tCmIEeWQ=";
+  nativeBuildInputs = [ makeWrapper ];
   buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ pam ];
 
   tags = [
@@ -31,6 +35,9 @@ buildGoModule rec {
   postInstall = ''
     mkdir -p $out/share/gogs
     cp -r conf templates public $out/share/gogs/
+
+    wrapProgram $out/bin/gogs \
+      --prefix PATH : ${lib.makeBinPath [ git openssh ]}
   '';
 
   meta = {

+ 4 - 6
packages/grist-core/default.nix → packages/grist-core.nix

@@ -7,7 +7,6 @@
 , nodejs
 , node-pre-gyp
 , node-gyp
-, node-gyp-build
 , prefetch-yarn-deps
 , fixup-yarn-lock
 , makeWrapper
@@ -46,18 +45,18 @@ in
 
 stdenv.mkDerivation rec {
   pname = "grist-core";
-  version = "1.7.15";
+  version = "1.7.8";
 
   src = fetchFromGitHub {
     owner = "gristlabs";
     repo = "grist-core";
     tag = "v${version}";
-    hash = "sha256-NOD2TOYcOXG+VmJBtg555cuHAS4neGvJKLYuOrdXXLE=";
+    hash = "sha256-e68PtWdlgKdjCj97fp+Rh3WwwwIcvKSXlx4Ry1qUeLg=";
   };
 
   offlineCache = fetchYarnDeps {
     yarnLock = "${src}/yarn.lock";
-    hash = "sha256-OvJMfHlToOJkPe8wP3zGRXvqHr4mhO1wGVKrec9Q/h4=";
+    hash = "sha256-7zyuBxheftgCXGjjJ+rdwSslIro9IEd/uvmo4xp6I+Q=";
   };
 
   gristPython = python3.withPackages (pkgs: with pkgs; [
@@ -78,7 +77,6 @@ stdenv.mkDerivation rec {
     nodejs
     node-pre-gyp
     node-gyp
-    node-gyp-build
     prefetch-yarn-deps
     fixup-yarn-lock
     makeWrapper
@@ -122,7 +120,7 @@ stdenv.mkDerivation rec {
     fixup-yarn-lock yarn.lock
 
     yarn config --offline set yarn-offline-mirror ${offlineCache}
-    yarn --offline --frozen-lockfile --ignore-platform --ignore-engines --no-progress --non-interactive --ignore-scripts install
+    yarn --offline --frozen-lockfile --ignore-platform --ignore-engines --no-progress --non-interactive install
 
     patchShebangs node_modules
     patchShebangs buildtools