Add Taiga

Taiga is a project management software, and we desperately need a system
to manage our tasks. After trying several others (OpenProject, Redmine,
leantime) we decided to use Taiga as it appears to be the only system
that provides the possibility of OIDC without an Enterprise subscription.

Having said that, we haven't set up OIDC yet, as the plugin to set it up
appears to be unmaintained and does not support our version of postgres.

This is not the final state of Taiga. After Jenkins and Quay (or
alternatives) are up we will be revisiting Taiga to create an updated
version of the plugin and set up OIDC. For now, only the infrastructure
tasks will be on Taiga.

Change-Id: Iaccc3cd8a2d94762a70b8e6f96a516ca241b552d
Reviewed-on: https://git.clicks.codes/c/Clicks/NixFiles/+/83
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/modules/common/nginx-routes.nix b/modules/common/nginx-routes.nix
index 865a171..e94d30f 100644
--- a/modules/common/nginx-routes.nix
+++ b/modules/common/nginx-routes.nix
@@ -13,6 +13,7 @@
       (ReverseProxy "127.0.0.1:3000")
     ]))
     (Hosts [ "vaultwarden.clicks.codes" "passwords.clicks.codes" ] (ReverseProxy "generic:1028"))
+    (Hosts [ "taiga.clicks.codes" "projects.clicks.codes" "tasks.clicks.codes" "issues.clicks.codes" "kanban.clicks.codes" ] (ReverseProxy "generic:1029"))
     (Host "login.clicks.codes" (ReverseProxy "127.0.0.1:9083"))
     (Hosts [ "gerrit.clicks.codes" "git.clicks.codes" ]
       (ReverseProxy "generic:1024"))
diff --git a/modules/common/postgres.nix b/modules/common/postgres.nix
index f77346a..30103e8 100644
--- a/modules/common/postgres.nix
+++ b/modules/common/postgres.nix
@@ -4,7 +4,7 @@
 
     package = pkgs.postgresql;
     settings = {
-      listen_addresses = lib.mkForce "standard";
+      listen_addresses = lib.mkForce "standard, 172.20.0.1";
       log_connections = true;
       logging_collector = true;
       log_disconnections = true;
@@ -12,7 +12,7 @@
     };
 
     ensureDatabases =
-      [ "vaultwarden" "gerrit" "privatebin" "keycloak" "nextcloud" "synapse" ];
+      [ "vaultwarden" "gerrit" "privatebin" "keycloak" "nextcloud" "synapse" "taiga" ];
 
     ensureUsers = [
       {
@@ -42,11 +42,17 @@
         name = "nextcloud";
         ensurePermissions = { "DATABASE nextcloud" = "ALL PRIVILEGES"; };
       }
+      {
+        name = "taiga";
+        ensurePermissions = { "DATABASE taiga" = "ALL PRIVILEGES"; };
+      }
     ] ++ (map (name: ({
       inherit name;
       ensurePermissions = { "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; };
     })) [ "minion" "coded" "pineafan" ]);
 
+    #                 method database user address auth-method
+    authentication = "host   all      all  samenet scram-sha-256";
   };
 
   systemd.services.postgresql.postStart = lib.mkMerge [
@@ -85,6 +91,10 @@
         user = "nextcloud";
         passwordFile = config.sops.secrets.clicks_nextcloud_db_password.path;
       }
+      {
+        user = "taiga";
+        passwordFile = config.sops.secrets.clicks_taiga_db_password.path;
+      }
     ] [
       (map (userData: ''
         $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
@@ -99,6 +109,7 @@
     "clicks_vaultwarden_db_password"
     "clicks_privatebin_db_password"
     "clicks_nextcloud_db_password"
+    "clicks_taiga_db_password"
   ] [
     (map (name: {
       inherit name;
diff --git a/modules/common/taiga.nix b/modules/common/taiga.nix
new file mode 100644
index 0000000..0f64fb3
--- /dev/null
+++ b/modules/common/taiga.nix
@@ -0,0 +1,169 @@
+{ config, pkgs, ... }: let
+  openid_environment = {
+    ENABLE_OPENID = "True";
+    OPENID_USER_URL = "https://login.clicks.codes/realms/master/protocol/openid-connect/userinfo";
+    OPENID_TOKEN_URL = "https://login.clicks.codes/realms/master/protocol/openid-connect/token";
+    OPENID_CLIENT_ID = "taiga";
+    OPENID_NAME = "Clicks Keycloak";
+
+    # PUBLIC_REGISTER_ENABLED = "True";
+
+    OPENID_ID_FIELD = "sub";
+    OPENID_USERNAME_FIELD = "preferred_username";
+    OPENID_FULLNAME_FIELD = "name";
+    OPENID_EMAIL_FIELD = "email";
+    OPENID_SCOPE="openid email";
+
+    OPENID_FILTER = "enabled";
+    OPENID_FILTER_FIELD = "taiga_access";
+  };
+  backend_environment = openid_environment // {
+    POSTGRES_DB = "taiga";
+    POSTGRES_USER = "taiga";
+    POSTGRES_HOST = "172.20.0.1";
+
+    TAIGA_SCHEME = "https";
+    TAIGA_DOMAIN = "taiga.clicks.codes";
+    TAIGA_SUBPATH = "";
+
+    EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
+    DEFAULT_FROM_EMAIL = "taiga@clicks.codes";
+    EMAIL_USE_TLS = "True";
+    EMAIL_USE_SSL = "False"; # not needed when using TLS
+    EMAIL_HOST = "mail.clicks.codes";
+    EMAIL_PORT = "587";
+    EMAIL_HOST_USER = "taiga@clicks.codes";
+
+    RABBITMQ_USER = "taiga";
+
+    ENABLE_TELEMETRY = "False";
+  };
+  credential_environment_files = [
+    config.sops.secrets.taiga_credentials_env.path
+    # TODO: OPENID_CLIENT_SECRET
+  ];
+
+  backend_volumes = [
+    "/var/taiga/back/static:/taiga-back/static"
+    "/var/taiga/back/media:/taiga-back/media"
+  ];
+
+  taiga_version = "latest";
+  taiga_base_version = "latest";  # events, etc. only have X.X.0 versions
+in {
+  sops.secrets.taiga_credentials_env = {
+    mode = "0660";
+    owner = config.users.users.root.name;
+    group = config.users.users.root.group;
+    sopsFile = ../../secrets/taiga.env.bin;
+    format = "binary";
+  };
+
+  networking.firewall.interfaces.taiga.allowedTCPPorts = [ 5432 ];
+
+  systemd.services = {
+    "docker-network-taiga" = {
+      serviceConfig.Type = "oneshot";
+      wantedBy = [
+        "docker-taiga-back.service"
+        "docker-taiga-async.service"
+        "docker-taiga-async-rabbitmq.service"
+        "docker-taiga-front.service"
+        "docker-taiga-events.service"
+        "docker-taiga-events-rabbitmq.service"
+        "docker-taiga-protected.service"
+        "docker-taiga-gateway.service"
+      ];
+      script = ''
+        ${pkgs.docker}/bin/docker network inspect taiga > /dev/null 2>&1 || ${pkgs.docker}/bin/docker network create taiga --gateway 172.20.0.1 --subnet 172.20.0.0/16 --opt com.docker.network.bridge.name=taiga
+      '';
+    };
+    docker-taiga-back.requires = [
+      "docker-taiga-events-rabbitmq.service"
+      "docker-taiga-async-rabbitmq.service"
+      "postgresql.service"
+    ];
+    docker-taiga-async.requires = [
+      "docker-taiga-events-rabbitmq.service"
+      "docker-taiga-async-rabbitmq.service"
+      "postgresql.service"
+    ];
+    docker-taiga-gateway.requires = [
+      "docker-taiga-front.service"
+      "docker-taiga-back.service"
+      "docker-taiga-events.service"
+    ];
+    docker-taiga-events.requires = [
+      "docker-taiga-events-rabbitmq.service"
+    ];
+  };
+  virtualisation.oci-containers.containers = {
+    taiga-back = {
+      image = "taigaio/taiga-back:${taiga_version}";
+      environment = backend_environment;
+      environmentFiles = credential_environment_files;
+      volumes = backend_volumes;
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-async = {
+      image = "taigaio/taiga-back:${taiga_version}";
+      environment = backend_environment;
+      environmentFiles = credential_environment_files;
+      volumes = backend_volumes;
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-async-rabbitmq = {
+      image = "rabbitmq:3.8-management-alpine";
+      environment = {
+        RABBITMQ_DEFAULT_USER = "taiga";
+        RABBITMQ_DEFAULT_VHOST = "taiga";
+      };
+      environmentFiles = credential_environment_files;
+      volumes = [ "/var/taiga/rabbitmq/async:/var/lib/rabbitmq" ];
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-front = {
+      image = "taigaio/taiga-front:${taiga_version}";
+      environment = openid_environment // {
+        TAIGA_URL = "https://taiga.clicks.codes";
+        TAIGA_WEBSOCKETS_URL = "wss://taiga.clicks.codes";
+        TAIGA_SUBPATH = "";
+      };
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-events = {
+      image = "taigaio/taiga-events:${taiga_base_version}";
+      environment = {
+        RABBITMQ_USER = "taiga";
+      };
+      environmentFiles = credential_environment_files;
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-events-rabbitmq = {
+      image = "rabbitmq:3.8-management-alpine";
+      environment = {
+        RABBITMQ_DEFAULT_USER = "taiga";
+        RABBITMQ_DEFAULT_VHOST = "taiga";
+      };
+      environmentFiles = credential_environment_files;
+      volumes = [ "/var/taiga/rabbitmq/events:/var/lib/rabbitmq" ];
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-protected = {
+      image = "taigaio/taiga-protected:${taiga_base_version}";
+      environment = {
+        MAX_AGE = "600";
+      };
+      environmentFiles = credential_environment_files;
+      extraOptions = [ "--network=taiga" ];
+    };
+    taiga-gateway = {
+      image = "nginx:1.19-alpine";
+      ports = [ "127.0.0.255:1029:80/tcp" ];
+      volumes = [
+        "${./taiga/taiga-gateway.conf}:/etc/nginx/conf.d/default.conf"
+      ] ++ backend_volumes;
+      extraOptions = [ "--network=taiga" ];
+    };
+  };
+}
diff --git a/modules/common/taiga/taiga-gateway.conf b/modules/common/taiga/taiga-gateway.conf
new file mode 100644
index 0000000..ebaeb68
--- /dev/null
+++ b/modules/common/taiga/taiga-gateway.conf
@@ -0,0 +1,75 @@
+server {
+    listen 80 default_server;
+
+    client_max_body_size 100M;
+    charset utf-8;
+
+    # Frontend
+    location / {
+        proxy_pass http://taiga-front/;
+        proxy_pass_header Server;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Scheme $scheme;
+    }
+
+    # API
+    location /api/ {
+        proxy_pass http://taiga-back:8000/api/;
+        proxy_pass_header Server;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Scheme $scheme;
+    }
+
+    # Admin
+    location /admin/ {
+        proxy_pass http://taiga-back:8000/admin/;
+        proxy_pass_header Server;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Scheme $scheme;
+    }
+
+    # Static
+    location /static/ {
+        alias /taiga/static/;
+    }
+
+    # Media
+    location /_protected/ {
+        internal;
+        alias /taiga/media/;
+        add_header Content-disposition "attachment";
+    }
+
+    # Unprotected section
+    location /media/exports/ {
+        alias /taiga/media/exports/;
+        add_header Content-disposition "attachment";
+    }
+
+    location /media/ {
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Scheme $scheme;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_pass http://taiga-protected:8003/;
+        proxy_redirect off;
+    }
+
+    # Events
+    location /events {
+        proxy_pass http://taiga-events:8888/events;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_connect_timeout 7d;
+        proxy_send_timeout 7d;
+        proxy_read_timeout 7d;
+    }
+}