#! Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0

#@ load("@ytt:data", "data")
#@ load("@ytt:sha256", "sha256")
#@ load("@ytt:yaml", "yaml")

#@ def ldapLIDIF():
#@yaml/text-templated-strings
ldap.ldif: |
  # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! ***
  # Here's a good explanation of LDIF:
  # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system

  # pinniped.dev (organization, root)
  dn: dc=pinniped,dc=dev
  objectClass: dcObject
  objectClass: organization
  dc: pinniped
  o: example

  # users, pinniped.dev (organization unit)
  dn: ou=users,dc=pinniped,dc=dev
  objectClass: organizationalUnit
  ou: users

  # groups, pinniped.dev (organization unit)
  dn: ou=groups,dc=pinniped,dc=dev
  objectClass: organizationalUnit
  ou: groups

  # beach-groups, groups, pinniped.dev (organization unit)
  dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev
  objectClass: organizationalUnit
  ou: beach-groups

  # pinny, users, pinniped.dev (user)
  dn: cn=pinny,ou=users,dc=pinniped,dc=dev
  objectClass: inetOrgPerson
  objectClass: posixAccount
  objectClass: shadowAccount
  cn: pinny
  sn: Seal
  givenName: Pinny the 🦭
  mail: pinny.ldap@example.com
  userPassword: (@= data.values.pinny_ldap_password @)
  uid: pinny
  uidNumber: 1000
  gidNumber: 1000
  homeDirectory: /home/pinny
  loginShell: /bin/bash
  gecos: pinny-the-seal

  # wally, users, pinniped.dev (user without password)
  dn: cn=wally,ou=users,dc=pinniped,dc=dev
  objectClass: inetOrgPerson
  objectClass: posixAccount
  objectClass: shadowAccount
  cn: wally
  sn: Walrus
  givenName: Wally
  mail: wally.ldap@example.com
  mail: wally.alternate@example.com
  uid: wally
  uidNumber: 1001
  gidNumber: 1001
  homeDirectory: /home/wally
  loginShell: /bin/bash
  gecos: wally-the-walrus

  # olive, users, pinniped.dev (user without password)
  dn: cn=olive,ou=users,dc=pinniped,dc=dev
  objectClass: inetOrgPerson
  objectClass: posixAccount
  objectClass: shadowAccount
  cn: olive
  sn: Boston Terrier
  givenName: Olive
  mail: olive.ldap@example.com
  uid: olive
  uidNumber: 1002
  gidNumber: 1002
  homeDirectory: /home/olive
  loginShell: /bin/bash
  gecos: olive-the-dog

  # ball-game-players, beach-groups, groups, pinniped.dev (group of users)
  dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev
  cn: ball-game-players
  objectClass: groupOfNames
  member: cn=pinny,ou=users,dc=pinniped,dc=dev
  member: cn=olive,ou=users,dc=pinniped,dc=dev

  # seals, groups, pinniped.dev (group of users)
  dn: cn=seals,ou=groups,dc=pinniped,dc=dev
  cn: seals
  objectClass: groupOfNames
  member: cn=pinny,ou=users,dc=pinniped,dc=dev

  # walruses, groups, pinniped.dev (group of users)
  dn: cn=walruses,ou=groups,dc=pinniped,dc=dev
  cn: walruses
  objectClass: groupOfNames
  member: cn=wally,ou=users,dc=pinniped,dc=dev

  # pinnipeds, users, pinniped.dev (group of groups)
  dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
  cn: pinnipeds
  objectClass: groupOfNames
  member: cn=seals,ou=groups,dc=pinniped,dc=dev
  member: cn=walruses,ou=groups,dc=pinniped,dc=dev

  # mammals, groups, pinniped.dev (group of both groups and users)
  dn: cn=mammals,ou=groups,dc=pinniped,dc=dev
  cn: mammals
  objectClass: groupOfNames
  member: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
  member: cn=olive,ou=users,dc=pinniped,dc=dev

  # ball-game-players group again, but this time defined as a posixGroup
  dn: cn=ball-game-players-posix,ou=groups,dc=pinniped,dc=dev
  objectClass: posixGroup
  objectClass: top
  cn: ball-game-players-posix
  gidNumber: 1002
  memberUid: pinny
  memberUid: olive

  # seals group again, but this time defined as a posixGroup
  dn: cn=seals-posix,ou=groups,dc=pinniped,dc=dev
  objectClass: posixGroup
  objectClass: top
  cn: seals-posix
  gidNumber: 1001
  memberUid: pinny

  # walruses group again, but this time defined as a posixGroup
  dn: cn=walruses-posix,ou=groups,dc=pinniped,dc=dev
  objectClass: posixGroup
  objectClass: top
  cn: walruses-posix
  gidNumber: 1000
  memberUid: wally
#@ end

---
apiVersion: v1
kind: Secret
metadata:
  name: ldap-ldif-files
  namespace: tools
type: Opaque
stringData: #@ ldapLIDIF()
---
apiVersion: v1
kind: Secret
metadata:
  name: ldap-server-additional-schema-ldif-files
  namespace: tools
type: Opaque
stringData:
  #! From https://github.com/bitnami/containers/issues/982#issuecomment-1220354408
  memberof.ldif: |
    dn: cn=module,cn=config
    cn: module
    objectClass: olcModuleList
    olcModulePath: /opt/bitnami/openldap/lib/openldap
    olcModuleLoad: memberof.so
    olcModuleLoad: refint.so

    dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config
    objectClass: olcMemberOf
    objectClass: olcOverlayConfig
    olcOverlay: memberof

    dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config
    objectClass: olcConfig
    objectClass: olcOverlayConfig
    objectClass: olcRefintConfig
    objectClass: top
    olcOverlay: refint
    olcRefintAttribute: memberof member manager owner
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ldap
  namespace: tools
  labels:
    app: ldap
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ldap
  template:
    metadata:
      labels:
        app: ldap
      annotations:
        #! Cause the pod to get recreated whenever the LDIF file changes.
        ldifConfigHash: #@ sha256.sum(yaml.encode(ldapLIDIF()))
    spec:
      containers:
        - name: ldap
          #! Use our own fork of docker.io/bitnami/openldap for now, because we added the
          #! LDAP_SERVER_CONFIG_BEFORE_CUSTOM_LDIF_DIR and LDAP_SERVER_CONFIG_AFTER_CUSTOM_LDIF_DIR options.
          #! See https://github.com/pinniped-ci-bot/bitnami-docker-openldap/tree/pinniped
          image: #@ data.values.ldap_image
          imagePullPolicy: IfNotPresent
          ports:
            - name: ldap
              containerPort: 1389
            - name: ldaps
              containerPort: 1636
          readinessProbe:
            tcpSocket:
              port: ldap
            initialDelaySeconds: 2
            timeoutSeconds: 90
            periodSeconds: 2
            failureThreshold: 9
          env:
            #! Example ldapsearch commands that can be run from within the container based on these env vars.
            #! These will print the whole LDAP tree starting at our root.
            #! Using StartTLS (-ZZ) on the ldap port...
            #!   LDAPTLS_CACERT=/var/certs/ca.pem ldapsearch -x -ZZ -H 'ldap://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev'
            #! Using ldaps...
            #!   LDAPTLS_CACERT=/var/certs/ca.pem ldapsearch -x -H 'ldaps://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev'
            #! Note that the memberOf attribute is special and not returned by default. It must be specified as one of attributes to return in the search, e.g.:
            #!   LDAPTLS_CACERT=/var/certs/ca.pem ldapsearch -x -H 'ldaps://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev' cn uidNumber mail member memberOf
            #! This should fail and report "TLS confidentiality required" because we require TLS and this does not use TLS or StartTLS...
            #!   ldapsearch -x -H 'ldap://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev'
            - name: BITNAMI_DEBUG
              value: "true"
            - name: LDAP_ADMIN_USERNAME
              value: "admin"
            - name: LDAP_ADMIN_PASSWORD
              value: "password" #! ok to hardcode: the LDAP server will not be available from outside the cluster
            - name: LDAP_ENABLE_TLS
              value: "yes"
            - name: LDAP_REQUIRE_TLS
              value: "yes"
            - name: LDAP_TLS_CERT_FILE
              value: "/var/certs/ldap.pem"
            - name: LDAP_TLS_KEY_FILE
              value: "/var/certs/ldap-key.pem"
            - name: LDAP_TLS_CA_FILE
              value: "/var/certs/ca.pem"
              #! Note that the custom LDIF file is only read at pod start-up time.
            - name: LDAP_CUSTOM_LDIF_DIR
              value: "/var/ldifs"
              #! Seems like LDAP_ROOT is still required when using LDAP_CUSTOM_LDIF_DIR because it effects the admin user.
              #! Presumably this needs to match the root that we create in the LDIF file.
            - name: LDAP_ROOT
              value: "dc=pinniped,dc=dev"
            - name: LDAP_EXTRA_SCHEMAS
              value: "cosine,inetorgperson,nis,memberof"
          volumeMounts:
            - name: certs
              mountPath: /var/certs
              readOnly: true
            - name: ldifs
              mountPath: /var/ldifs
              readOnly: true
            - name: additional-schema
              mountPath: /opt/bitnami/openldap/etc/schema/memberof.ldif
              subPath: memberof.ldif
              readOnly: true
      volumes:
        - name: certs
          secret:
            secretName: certs
        - name: ldifs
          secret:
            secretName: ldap-ldif-files
        - name: additional-schema
          secret:
            secretName: ldap-server-additional-schema-ldif-files
      tolerations:
        - key: kubernetes.io/arch
          effect: NoSchedule
          operator: Equal
          value: amd64 #! Allow running on amd64 nodes.
        - key: kubernetes.io/arch
          effect: NoSchedule
          operator: Equal
          value: arm64 #! Also allow running on arm64 nodes.
---
apiVersion: v1
kind: Service
metadata:
  name: ldap
  namespace: tools
  labels:
    app: ldap
spec:
  type: ClusterIP
  selector:
    app: ldap
  ports:
    - protocol: TCP
      port: 389
      targetPort: 1389
      name: ldap
    - protocol: TCP
      port: 636
      targetPort: 1636
      name: ldaps
---
apiVersion: v1
kind: Service
metadata:
  name: ldaps
  namespace: tools
  labels:
    app: ldap
spec:
  type: ClusterIP
  selector:
    app: ldap
  ports:
    - protocol: TCP
      port: 636
      targetPort: 1636
      name: ldaps
---
apiVersion: v1
kind: Service
metadata:
  name: ldapstarttls
  namespace: tools
  labels:
    app: ldap
spec:
  type: ClusterIP
  selector:
    app: ldap
  ports:
    - protocol: TCP
      port: 389
      targetPort: 1389
      name: ldap