app/codehosting: forgejo deployment

Change-Id: Icfe6e0b17932a3248e1bdb807f431c59c48430de
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1685
Reviewed-by: q3k <q3k@hackerspace.pl>
diff --git a/app/codehosting/forgejo.libsonnet b/app/codehosting/forgejo.libsonnet
new file mode 100644
index 0000000..9abbcf1
--- /dev/null
+++ b/app/codehosting/forgejo.libsonnet
@@ -0,0 +1,253 @@
+/*
+
+    Deploy a Forgejo instance with PostgreSQL database and additional PV for git data.
+    Pre-provision the secrets with:
+
+    kubectl -n $KUBE_NAMESPACE create secret generic forgejo \
+      --from-literal=postgres_password=$(pwgen -s 24 1) \
+      --from-literal=secret_key=$(pwgen -s 128 1) \
+      --from-literal=admin_password=$(pwgen -s 128 1) \
+      --from-literal=oauth2_client_id=$SSO_CLIENT_ID \
+      --from-literal=oauth2_client_secret=$SSO_CLIENT_SECRET \
+      --from-literal=ldap_bind_dn=$LDAP_BIND_DN \
+      --from-literal=ldap_bind_password=$LDAP_BIND_PASSWORD \
+      --from-literal=smtp_password=$SMTP_PASSWORD
+
+    Import objectstore secret:
+
+    ceph_ns=ceph-waw3; ceph_pool=waw-hdd-redundant-3
+    kubectl -n $ceph_ns get secrets rook-ceph-object-user-${ceph_pool}-object-codehosting -o json | jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid) | .metadata.creationTimestamp=null' | kubectl apply -f - -n $KUBE_NAMESPACE
+
+    Import oidc auth trigger:
+
+    kubectl -n $KUBE_NAMESPACE exec deploy/postgres -i -- psql -U forgejo forgejo < create-oidc-binding.sql
+
+*/
+
+local kube = import "../../kube/kube.libsonnet";
+local postgres = import "../../kube/postgres.libsonnet";
+
+{
+    local forgejo = self,
+    local cfg = forgejo.cfg,
+    cfg:: {
+        namespace: error "namespace must be set",
+        prefix: "",
+
+        image: "codeberg.org/forgejo/forgejo:1.20.5-0",
+        storageClassName: "waw-hdd-redundant-3",
+        storageSize: { git: "200Gi" },
+
+        admin_username: error "admin_username must be set",
+        admin_email: error "admin_email must be set",
+
+        # Forgejo configuration, roughly representing the structure of app.ini
+        instanceName: error "instanceName (e.g. 'Warsaw Hackerspace Forgejo') must be set",
+        runMode: "prod",
+        server: {
+            domain: error "domain (e.g. git.hackerspace.pl) must be set",
+            sshDomain: cfg.server.domain,
+            rootURL: "https://" + cfg.server.domain + "/",
+            offlineMode: "true",
+        },
+        security: {
+            installLock: "true",
+        },
+        service: {
+            disableRegistration: "false",
+            allowOnlyExternalRegistration: "true",
+        },
+
+        s3: {
+            endpoint: "rook-ceph-rgw-waw-hdd-redundant-3-object.ceph-waw3.svc:80", #{ secretKeyRef: {name: "rook-ceph-object-user-waw-hdd-redundant-3-object-codehosting", key: "Endpoint" } },
+            accessKey: { secretKeyRef: {name: "rook-ceph-object-user-waw-hdd-redundant-3-object-codehosting", key: "AccessKey" } },
+            secretKey: { secretKeyRef: {name: "rook-ceph-object-user-waw-hdd-redundant-3-object-codehosting", key: "SecretKey" } },
+            bucket: "codehosting",
+        },
+
+        mailer: {
+            from: "forgejo@hackerspace.pl",
+            host: "mail.hackerspace.pl",
+            port: 465,
+            user: "forgejo",
+            password: { secretKeyRef: { name: "forgejo", key: "smtp_password" } },
+        },
+    },
+
+    name(suffix):: cfg.prefix + suffix,
+    ns: kube.Namespace(cfg.namespace),
+
+    postgres: postgres {
+        cfg+: {
+            namespace: cfg.namespace,
+            appName: "forgejo",
+            database: "forgejo",
+            username: "forgejo",
+            password: { secretKeyRef: { name: "forgejo", key: "postgres_password" } },
+            storageClassName: cfg.storageClassName,
+        },
+    },
+
+    configMap: forgejo.ns.Contain(kube.ConfigMap(forgejo.name("forgejo"))) {
+        data: {
+            "app.ini.template": importstr 'app.ini.template',
+            "entrypoint.sh": importstr 'entrypoint.sh',
+            "bootstrap-auth.sh": importstr 'bootstrap-auth.sh',
+        },
+    },
+
+    dataVolume: forgejo.ns.Contain(kube.PersistentVolumeClaim(forgejo.name("forgejo"))) {
+        spec+: {
+            storageClassName: cfg.storageClassName,
+            accessModes: [ "ReadWriteOnce" ],
+            resources: {
+                requests: {
+                    storage: cfg.storageSize.git,
+                },
+            },
+        },
+    },
+
+    forgejoCustom: forgejo.ns.Contain(kube.ConfigMap(forgejo.name("forgejo-custom"))) {
+        data: {
+            "signin_inner.tmpl": importstr 'signin_inner.tmpl',
+        },
+    },
+
+    statefulSet: forgejo.ns.Contain(kube.StatefulSet(forgejo.name("forgejo"))) {
+        spec+: {
+            replicas: 1,
+            template+: {
+                spec+: {
+                    securityContext: {
+                        runAsUser: 1000,
+                        runAsGroup: 1000,
+                        fsGroup: 1000,
+                    },
+                    volumes_: {
+                        configmap: kube.ConfigMapVolume(forgejo.configMap),
+                        custom: kube.ConfigMapVolume(forgejo.forgejoCustom),
+                        data: kube.PersistentVolumeClaimVolume(forgejo.dataVolume),
+                        empty: kube.EmptyDirVolume(),
+                    },
+                    containers_: {
+                        server: kube.Container(forgejo.name("forgejo")) {
+                            image: cfg.image,
+                            command: [ "bash", "/usr/bin/entrypoint" ],
+                            ports_: {
+                                server: { containerPort: 3000 },
+                                ssh: { containerPort: 22 },
+                            },
+                            readinessProbe: {
+                                tcpSocket: {
+                                    port: "server",
+                                },
+                                initialDelaySeconds: 5,
+                                periodSeconds: 5,
+                                successThreshold: 1,
+                                failureThreshold: 3
+                            },
+                            env_: {
+                                APP_NAME: cfg.instanceName,
+                                RUN_MODE: cfg.runMode,
+                                INSTALL_LOCK: cfg.security.installLock,
+                                SECRET_KEY: { secretKeyRef: { name: "forgejo", key: "secret_key" } },
+                                DB_TYPE: "postgres",
+                                DB_HOST: "postgres:5432",
+                                DB_USER: forgejo.postgres.cfg.username,
+                                DB_PASSWD: forgejo.postgres.cfg.password,
+                                DB_NAME: forgejo.postgres.cfg.appName,
+                                DOMAIN: cfg.server.domain,
+                                SSH_DOMAIN: cfg.server.sshDomain,
+                                SSH_LISTEN_PORT: "2222",
+                                ROOT_URL: forgejo.cfg.server.rootURL,
+                                DISABLE_REGISTRATION: cfg.service.disableRegistration,
+                                ALLOW_ONLY_EXTERNAL_REGISTRATION: cfg.service.allowOnlyExternalRegistration,
+                                OFFLINE_MODE: cfg.server.offlineMode,
+                                USER_UID: "1000",
+                                USER_GID: "1000",
+                                GITEA_CUSTOM: "/custom",
+                                MINIO_ENDPOINT: cfg.s3.endpoint,
+                                MINIO_BUCKET: cfg.s3.bucket,
+                                MINIO_ACCESS_KEY_ID: cfg.s3.accessKey,
+                                MINIO_SECRET_ACCESS_KEY: cfg.s3.secretKey,
+                                MAILER_FROM: cfg.mailer.from,
+                                MAILER_HOST: cfg.mailer.host,
+                                MAILER_PORT: cfg.mailer.port,
+                                MAILER_USER: cfg.mailer.user,
+                                MAILER_PASSWORD: cfg.mailer.password,
+                            },
+                            volumeMounts: [
+                                { name: "configmap", subPath: "entrypoint.sh", mountPath: "/usr/bin/entrypoint" },
+                                { name: "configmap", subPath: "app.ini.template", mountPath: "/etc/templates/app.ini" },
+                                { name: "data", mountPath: "/data" },
+                                { name: "empty", mountPath: "/custom" },
+                                { name: "custom", subPath: "signin_inner.tmpl", mountPath: "/custom/templates/user/auth/signin_inner.tmpl" },
+                            ],
+                        },
+                    },
+                    initContainers: [
+                        kube.Container(forgejo.name("forgejo-dbmigrate")) {
+                            image: forgejo.statefulSet.spec.template.spec.containers_.server.image,
+                            command: [ "bash", "/usr/bin/entrypoint", "/app/gitea/gitea", "migrate" ],
+                            env_: forgejo.statefulSet.spec.template.spec.containers_.server.env_,
+                            volumeMounts: forgejo.statefulSet.spec.template.spec.containers_.server.volumeMounts,
+                        },
+                        kube.Container(forgejo.name("forgejo-bootstrap-auth")) {
+                            image: forgejo.statefulSet.spec.template.spec.containers_.server.image,
+                            command: [
+                              "bash", "/bootstrap-auth.sh"
+                            ],
+                            env_: forgejo.statefulSet.spec.template.spec.containers_.server.env_ + {
+                              ADMIN_PASSWORD: { secretKeyRef: { name: "forgejo", key: "admin_password" } },
+                              SSO_CLIENT_ID: { secretKeyRef: { name: "forgejo", key: "oauth2_client_id" } },
+                              SSO_CLIENT_SECRET: { secretKeyRef: { name: "forgejo", key: "oauth2_client_secret" } },
+                              LDAP_BIND_DN: { secretKeyRef: { name: "forgejo", key: "ldap_bind_dn" } },
+                              LDAP_BIND_PASSWORD: { secretKeyRef: { name: "forgejo", key: "ldap_bind_password" } },
+                            },
+                            volumeMounts: forgejo.statefulSet.spec.template.spec.containers_.server.volumeMounts + [
+                                { name: "configmap", subPath: "bootstrap-auth.sh", mountPath: "/bootstrap-auth.sh" },
+                            ]
+                        },
+                    ],
+                },
+            },
+        },
+    },
+
+    svc: forgejo.ns.Contain(kube.Service(forgejo.name("forgejo"))) {
+        target_pod:: forgejo.statefulSet.spec.template,
+        spec+: {
+            ports: [
+                { name: "server", port: 80, targetPort: 3000, protocol: "TCP" },
+                { name: "ssh", port: 22, targetPort: 2222, protocol: "TCP" },
+            ],
+        },
+    },
+
+    ingress: forgejo.ns.Contain(kube.Ingress(forgejo.name("forgejo"))) {
+        metadata+: {
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "cert-manager.io/cluster-issuer": "letsencrypt-prod",
+                "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+            },
+        },
+        spec+: {
+            tls: [
+                { hosts: [cfg.server.domain], secretName: forgejo.name("acme") },
+            ],
+            rules: [
+                {
+                    host: cfg.server.domain,
+                    http: {
+                        paths: [
+                            { path: "/", backend: forgejo.svc.name_port },
+                        ],
+                    },
+                }
+            ],
+        },
+    },
+
+}