hswaw: add kasownik

Change-Id: I48739f9d4ecb8244a2baff5d38a308f7612940eb
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1990
Reviewed-by: informatic <informatic@hackerspace.pl>
diff --git a/cluster/kube/k0.libsonnet b/cluster/kube/k0.libsonnet
index 40d582d..c7c7c21 100644
--- a/cluster/kube/k0.libsonnet
+++ b/cluster/kube/k0.libsonnet
@@ -385,6 +385,7 @@
                         { namespace: "cebulacamp", dns: "cebula.camp" },
                         { namespace: "engelsystem-prod", dns: "engelsystem.hackerspace.pl" },
                         { namespace: "invoicer", dns: "invoicer.hackerspace.pl" },
+                        { namespace: "kasownik", dns: "kasownik.hackerspace.pl" },
                         { namespace: "labelmaker", dns: "label.hackerspace.pl" },
                         { namespace: "labelmaker", dns: "*.label.hackerspace.pl" },
                         { namespace: "ldapweb", dns: "profile.hackerspace.pl" },
@@ -484,6 +485,9 @@
                     "arsenicum",
                     "radex",
                 ],
+                "kasownik": [
+                    "radex",
+                ],
                 "labelmaker": [
                     "radex",
                 ],
diff --git a/hswaw/kasownik/OWNERS b/hswaw/kasownik/OWNERS
new file mode 100644
index 0000000..27671b7
--- /dev/null
+++ b/hswaw/kasownik/OWNERS
@@ -0,0 +1,3 @@
+owners:
+  - informatic
+  - radex
diff --git a/hswaw/kasownik/README.md b/hswaw/kasownik/README.md
new file mode 100644
index 0000000..a1dc1fa
--- /dev/null
+++ b/hswaw/kasownik/README.md
@@ -0,0 +1,5 @@
+# kasownik
+
+Warsaw Hackerspace Membership Management System
+
+Source and docs: https://code.hackerspace.pl/hswaw/kasownik
diff --git a/hswaw/kasownik/clear_lockfile.jsonnet b/hswaw/kasownik/clear_lockfile.jsonnet
new file mode 100644
index 0000000..f254486
--- /dev/null
+++ b/hswaw/kasownik/clear_lockfile.jsonnet
@@ -0,0 +1,42 @@
+local kube = import "../../kube/hscloud.libsonnet";
+
+// NOTE: Run `kubectl -n kasownik delete job/kasownik-clear-lockfile; kubecfg update hswaw/kasownik/clear_lockfile.jsonnet` to clear lockfile
+// This will not work if cronjob pod is still running (volume won't be mounted)
+{
+    local top = self,
+    local cfg = self.cfg,
+
+    cfg:: {
+        name: 'kasownik',
+        namespace: 'kasownik',
+    },
+
+    local ns = kube.Namespace(cfg.namespace),
+
+    clear_lockfile: ns.Contain(kube.Job(cfg.name + '-clear-lockfile')) {
+        spec+: {
+            ttlSecondsAfterFinished: 10, // NOTE: does not work, requires k8s 1.23
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: 'alpine:3.20.1',
+                            volumeMounts_: {
+                                data: { mountPath: '/data' },
+                            },
+                            command: ["sh", "-c", |||
+                                set -e -x
+                                rm /data/kasownik.lock
+                            |||]
+                        }
+                    },
+                    volumes_: {
+                        data: {
+                            persistentVolumeClaim: { claimName: cfg.name + '-fetch-data' },
+                        },
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/hswaw/kasownik/prod.jsonnet b/hswaw/kasownik/prod.jsonnet
new file mode 100644
index 0000000..41c654c
--- /dev/null
+++ b/hswaw/kasownik/prod.jsonnet
@@ -0,0 +1,183 @@
+local kube = import "../../kube/hscloud.libsonnet";
+local postgres = import "../../kube/postgres.libsonnet";
+
+// Setup:
+// - create `kasownik` and `kasownik-fetch` secrets
+// - run `./manage.py syncdb` to create db
+// - create `cache` folder in the fetch_data PVC
+{
+    local top = self,
+    local cfg = self.cfg,
+
+    cfg:: {
+        name: 'kasownik',
+        namespace: 'kasownik',
+        domain: 'kasownik.hackerspace.pl',
+
+        image: 'registry.k0.hswaw.net/radex/kasownik:20240710124006',
+
+        oauthClientId: 'd5770622-a661-45d1-9356-b8cb77de708c',
+        storageClassName: "waw-hdd-redundant-3",
+    },
+
+    secretRefs:: {
+        oauth: { secretKeyRef: { name: cfg.name, key: 'oauth_secret' } },
+        secret_key: { secretKeyRef: { name: cfg.name, key: 'secret_key' } },
+        ldap: { secretKeyRef: { name: cfg.name, key: 'ldap_password' } },
+        postgres: { secretKeyRef: { name: cfg.name, key: 'postgres_password' } },
+        smtp: { secretKeyRef: { name: cfg.name, key: 'smtp_password' } },
+        fetch: {
+            local name = cfg.name + '-fetch',
+            tdid: { secretKeyRef: { name: name, key: 'tdid' } },
+            alias: { secretKeyRef: { name: name, key: 'alias' } },
+            password: { secretKeyRef: { name: name, key: 'password' } },
+            user_agent: { secretKeyRef: { name: name, key: 'user-agent' } },
+        }
+    },
+
+    local ns = kube.Namespace(cfg.namespace),
+
+    env:: {
+        // Quirk: Must be alphabetically before SQLALCHEMY_DATABASE_URI
+        POSTGRES_PASSWORD: top.secretRefs.postgres,
+        SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://%s:%s@%s:%s/%s' % [
+            top.postgres.cfg.username,
+            '$(POSTGRES_PASSWORD)',
+            top.postgres.svc.host,
+            top.postgres.svc.port,
+            top.postgres.cfg.database,
+        ],
+        SPACEAUTH_CONSUMER_KEY: cfg.oauthClientId,
+        SPACEAUTH_CONSUMER_SECRET: top.secretRefs.oauth,
+        SECRET_KEY: top.secretRefs.secret_key,
+        DISABLE_LDAP: 'false',
+        LDAP_URI: 'ldap://ldap.hackerspace.pl',
+        LDAP_BIND_DN: 'cn=kasownik,ou=Services,dc=hackerspace,dc=pl',
+        LDAP_BIND_PASSWORD: top.secretRefs.ldap,
+        SMTP_USER: 'kasownik',
+        SMTP_PASSWORD: top.secretRefs.smtp,
+        MEMBERSHIP_FEES: std.manifestJson({
+            starving: 75,
+            fatty: 150,
+        }),
+    },
+
+    deployment: ns.Contain(kube.Deployment(cfg.name)) {
+        spec+: {
+            replicas: 1,
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: cfg.image,
+                            ports_: {
+                                http: { containerPort: 5000 },
+                            },
+                            env_: top.env,
+                            livenessProbe: {
+                                httpGet: { path: '/', port: 5000 },
+                                initialDelaySeconds: 20,
+                                periodSeconds: 5 * 60,
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+
+    cronjob: ns.Contain(kube.CronJob(cfg.name + '-cronjob')) {
+        spec+: {
+            schedule: "05 11,15,18 * * *", // after Elixir sessions
+            jobTemplate+: {
+                spec+: {
+                    template+: {
+                        spec+: {
+                            containers_: {
+                                default: kube.Container("default") {
+                                    image: cfg.image,
+                                    volumeMounts_: {
+                                        data: { mountPath: '/data' },
+                                        config: { mountPath: '/config' },
+                                    },
+                                    command: ["sh", "-c", |||
+                                        set -e -x
+                                        python /usr/src/fetch/banking-pekaobiznes.py --config /config/config.ini
+                                        ./manage.py automatch
+                                        ./manage.py ldapsync --dry-run
+                                    |||],
+                                    env_: top.env + {
+                                        SCRAPER_TDID: top.secretRefs.fetch.tdid,
+                                        SCRAPER_ALIAS: top.secretRefs.fetch.alias,
+                                        SCRAPER_PASSWORD: top.secretRefs.fetch.password,
+                                        SCRAPER_USER_AGENT: top.secretRefs.fetch.user_agent,
+                                    },
+                                },
+                            },
+                            volumes_: {
+                                data: top.fetch_data.volume,
+                                config: top.fetch_config.volume,
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+
+    fetch_config: ns.Contain(kube.ConfigMap(cfg.name + '-fetch-config')) {
+        data: {
+            "config.ini": std.manifestIni({
+                sections: {
+                    general: {
+                        cache_dir: '/data/cache',
+                        lockfile: '/data/kasownik.lock',
+                    },
+                    database: {
+                        uri: '${SQLALCHEMY_DATABASE_URI}'
+                    },
+                    scraper: {
+                        tdid: '${SCRAPER_TDID}',
+                        alias: '${SCRAPER_ALIAS}',
+                        password: '${SCRAPER_PASSWORD}',
+                        user_agent: '${SCRAPER_USER_AGENT}',
+                    },
+                    logging: {
+                        level: 'DEBUG',
+                    }
+                }
+            })
+        },
+    },
+
+    // used for lockfile and fetch cache
+    fetch_data: ns.Contain(kube.PersistentVolumeClaim(cfg.name + '-fetch-data')) {
+        storage:: "1Gi",
+        storageClass:: cfg.storageClassName,
+        spec+: {
+            accessModes: ['ReadWriteMany']
+        }
+    },
+
+    postgres: ns.Contain(postgres) {
+        cfg+: {
+            prefix: cfg.name + '-',
+            appName: cfg.name,
+            storageClassName: cfg.storageClassName,
+            version: '15.4',
+
+            database: 'kasownik',
+            username: 'kasownik',
+            password: top.secretRefs.postgres,
+        }
+    },
+
+    service: ns.Contain(kube.Service(cfg.name)) {
+        target:: top.deployment,
+    },
+
+    ingress: ns.Contain(kube.SimpleIngress(cfg.name)) {
+        hosts:: [cfg.domain],
+        target:: top.service,
+    },
+}