app/mastodon: deploy

Change-Id: I88c104d1a8d5627355b01a8c48dc235635fca5ed
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1421
Reviewed-by: implr <implr@hackerspace.pl>
diff --git a/app/mastodon/kube/mastodon.libsonnet b/app/mastodon/kube/mastodon.libsonnet
new file mode 100644
index 0000000..383c3ae
--- /dev/null
+++ b/app/mastodon/kube/mastodon.libsonnet
@@ -0,0 +1,326 @@
+local kube = import "../../../kube/kube.libsonnet";
+local postgres = import "../../../kube/postgres.libsonnet";
+local redis = import "../../../kube/redis.libsonnet";
+
+{
+    local app = self,
+    local cfg = app.cfg,
+
+    cfg:: {
+        namespace: error "cfg.namespace must be set",
+        # Domain as seen in the fediverse.
+        localDomain: error "cfg.localDomain must be set",
+        # Domain where the web interface is running. If different,
+        # localDomain's real server must be configured to forward
+        # /.well-known/webfinger to webDomain.
+        webDomain: cfg.localDomain,
+        images: {
+            mastodon: "tootsuite/mastodon:v4.0.2@sha256:21c20181a5d44ff553e9e8f7d8d2e53b2551cc8c7ac900760e056445b88e7438",
+        },
+        passwords: {
+            # generate however you like
+            postgres: error "cfg.secrets.postgres must be set",
+            # generate however you like
+            redis: error "cfg.secrets.redis must be set",
+        },
+        smtp: {
+            user: "mastodon",
+            from: "mastodon-noreply@hackerspace.pl",
+            # from mail server
+            password: error "cfg.smtp.password must be set",
+        },
+        secrets: {
+            # generate with podman run --rm -it tootsuite/mastodon:v4.0.2 bundle exec rake secret
+            keyBase: error "cfg.secrets.keyBase must be set",
+            # generate with podman run --rm -it tootsuite/mastodon:v4.0.2 bundle exec rake secret
+            otp: error "cfg.secrets.otp must be set",
+            # generate with podman run --rm -it tootsuite/mastodon:v4.0.2 bundle exec rake mastodon:webpush:generate_vapid_key
+            vapid: {
+                private: error "cfg.secrets.vapid.private must be set",
+                public: error "cfg.secrets.vapid.public must be set",
+            }
+        },
+        oidc: {
+            clientID: error "cfg.oidc.clientID must be set",
+            clientSecret: error "cfg.oidc.clientSecret must be set",
+        },
+        objectStorage: {
+            bucket: error "cfg.objectStorage.bucket must be set",
+            accessKeyId: error "cfg.objectStorage.accessKeyId must be set",
+            secretAccessKey: error "cfg.objectStorage.secretAccessKey must be set",
+        },
+
+        scaling: {
+            web: 1,
+            sidekiq: 1,
+        },
+    },
+
+    // Unified env var based config used for {web, streaming, sidekiq}.
+    // Sample available in https://github.com/mastodon/mastodon/blob/main/.env.production.sample
+    env:: {
+        LOCAL_DOMAIN: cfg.localDomain,
+        WEB_DOMAIN: cfg.webDomain,
+
+        // REDIS_PASS is not used directly by the apps, it's just used to embed
+        // a secret fragment into REDIS_URL.
+        REDIS_PASS: kube.SecretKeyRef(app.config, "redis-pass"),
+        REDIS_URL: "redis://:$(REDIS_PASS)@%s" % [app.redis.svc.host_colon_port],
+
+        DB_HOST: app.postgres.svc.host,
+        DB_USER: "mastodon",
+        DB_NAME: "mastodon",
+        DB_PASS: kube.SecretKeyRef(app.config, "postgres-pass"),
+        DB_PORT: "5432",
+
+        ES_ENABLED: "false",
+
+        SECRET_KEY_BASE: kube.SecretKeyRef(app.config, "secret-key-base"),
+        OTP_SECRET: kube.SecretKeyRef(app.config, "otp-secret"),
+
+        VAPID_PRIVATE_KEY: kube.SecretKeyRef(app.config, "vapid-private"),
+        VAPID_PUBLIC_KEY: kube.SecretKeyRef(app.config, "vapid-public"),
+
+        SMTP_SERVER: "mail.hackerspace.pl",
+        SMTP_PORT: "587",
+        SMTP_LOGIN: "mastodon",
+        SMTP_PASSWORD: kube.SecretKeyRef(app.config, "smtp-password"),
+        SMTP_FROM_ADDRESS: "mastodon-noreply@hackerspace.pl",
+
+        S3_ENABLED: "true",
+        S3_BUCKET: cfg.objectStorage.bucket,
+        AWS_ACCESS_KEY_ID: kube.SecretKeyRef(app.config, "object-access-key-id"),
+        AWS_SECRET_ACCESS_KEY: kube.SecretKeyRef(app.config, "object-secret-access-key"),
+        S3_HOSTNAME: "object.ceph-waw3.hswaw.net",
+        S3_ENDPOINT: "https://object.ceph-waw3.hswaw.net",
+
+        IP_RETENTION_PERIOD: "31556952",
+        SESSION_RETENTION_PERIOD: "31556952",
+
+        OIDC_ENABLED: "true",
+        OIDC_DISPLAY_NAME: "Use Warsaw Hackerspace SSO",
+        OIDC_ISSUER: "https://sso.hackerspace.pl",
+        OIDC_DISCOVERY: "false",
+        OIDC_SCOPE: "openid,profile:read",
+        OIDC_UID_FIELD: "uid",
+        OIDC_CLIENT_ID: cfg.oidc.clientId,
+        OIDC_REDIRECT_URI: "https://%s/auth/auth/openid_connect/callback" % [cfg.webDomain],
+        OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED: "true",
+        OIDC_CLIENT_SECRET: kube.SecretKeyRef(app.config, "oidc-client-secret"),
+        OIDC_AUTH_ENDPOINT: "https://sso.hackerspace.pl/oauth/authorize",
+        OIDC_TOKEN_ENDPOINT: "https://sso.hackerspace.pl/oauth/token",
+        OIDC_USER_INFO_ENDPOINT: "https://sso.hackerspace.pl/api/1/userinfo",
+        OIDC_JWKS_URI: "https://sso.hackerspace.pl/.well-known/jwks.json",
+    },
+
+    namespace: kube.Namespace(cfg.namespace),
+    local ns = self.namespace,
+
+    postgres: postgres {
+        cfg+: {
+            namespace: cfg.namespace,
+            appName: "mastodon",
+            database: "mastodon",
+            username: "mastodon",
+            prefix: "waw3-",
+            password: kube.SecretKeyRef(app.config, "postgres-pass"),
+            storageClassName: "waw-hdd-redundant-3",
+            storageSize: "100Gi",
+        },
+    },
+
+    redis: redis {
+        cfg+: {
+            namespace: cfg.namespace,
+            appName: "mastodon",
+            storageClassName: "waw-hdd-redundant-3",
+            prefix: "waw3-",
+            password: kube.SecretKeyRef(app.config, "redis-pass"),
+        },
+    },
+
+    web: ns.Contain(kube.Deployment("web")) {
+        spec+: {
+            minReadySeconds: 10,
+            replicas: cfg.scaling.web,
+            template+: {
+                spec+: {
+                    initContainers_: {
+                        migrate: kube.Container("migrate") {
+                            image: cfg.images.mastodon,
+                            env_: app.env {
+                                SKIP_POST_DEPLOYMENT_MIGRATIONS: "true",
+                            },
+                            command: [
+                                "bundle", "exec",
+                                "rails", "db:migrate",
+                            ],
+                        },
+                    },
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: cfg.images.mastodon,
+                            env_: app.env,
+                            command: [
+                                "bundle", "exec",
+                                "rails", "s", "-p", "3000",
+                            ],
+                            ports_: {
+                                web: { containerPort: 3000 },
+                            },
+                            readinessProbe: {
+                                httpGet: {
+                                    path: "/health",
+                                    port: "web",
+                                },
+                                failureThreshold: 10,
+                                periodSeconds: 5,
+                            },
+                            resources: {
+                                requests: {
+                                    cpu: "250m",
+                                    memory: "1024M",
+                                },
+                                limits: {
+                                    cpu: "1",
+                                    memory: "1024M",
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    sidekiq: ns.Contain(kube.Deployment("sidekiq")) {
+        spec+: {
+            replicas: cfg.scaling.sidekiq,
+            minReadySeconds: 10,
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: cfg.images.mastodon,
+                            env_: app.env,
+                            command: [
+                                "bundle", "exec",
+                                "sidekiq",
+                            ],
+                            resources: {
+                                requests: {
+                                    cpu: "250m",
+                                    memory: "1024M",
+                                },
+                                limits: {
+                                    cpu: "1",
+                                    memory: "1024M",
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    streaming: ns.Contain(kube.Deployment("streaming")) {
+        spec+: {
+            minReadySeconds: 10,
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: cfg.images.mastodon,
+                            env_: app.env {
+                                "STREAMING_CLUSTER_NUM": "1",
+                            },
+                            command: [
+                                "node", "./streaming",
+                            ],
+                            ports_: {
+                                web: { containerPort: 4000 },
+                            },
+                            readinessProbe: {
+                                httpGet: {
+                                    path: "/api/v1/streaming/health",
+                                    port: "web",
+                                },
+                                failureThreshold: 1,
+                                periodSeconds: 5,
+                            },
+                            resources: {
+                                requests: {
+                                    cpu: "250m",
+                                    memory: "1024M",
+                                },
+                                limits: {
+                                    cpu: "1",
+                                    memory: "1024M",
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    svcWeb: ns.Contain(kube.Service("web")) {
+        target_pod: app.web.spec.template,
+    },
+
+    svcStreaming: ns.Contain(kube.Service("streaming")) {
+        target_pod: app.streaming.spec.template,
+    },
+
+
+    ingress: ns.Contain(kube.Ingress("mastodon")) {
+        metadata+: {
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+            },
+        },
+        spec+: {
+            tls: [
+                {
+                    hosts: [cfg.webDomain],
+                    secretName: "mastodon-ingress-tls",
+                },
+            ],
+            rules: [
+                {
+                    host: cfg.webDomain,
+                    http: {
+                        paths: [
+                            { path: "/", backend: app.svcWeb.name_port },
+                            { path: "/api/v1/streaming", backend: app.svcStreaming.name_port },
+                        ],
+                    },
+                },
+            ],
+        },
+    },
+
+    config: ns.Contain(kube.Secret("config")) {
+        data_: {
+            "postgres-pass": cfg.passwords.postgres,
+            "redis-pass": cfg.passwords.redis,
+
+            "secret-key-base": cfg.secrets.keyBase,
+            "otp-secret": cfg.secrets.otp,
+
+            "vapid-private": cfg.secrets.vapid.private,
+            "vapid-public": cfg.secrets.vapid.public,
+
+            "smtp-password": cfg.smtp.password,
+
+            "object-access-key-id": cfg.objectStorage.accessKeyId,
+            "object-secret-access-key": cfg.objectStorage.secretAccessKey,
+
+            "oidc-client-secret": cfg.oidc.clientSecret,
+        },
+    },
+}