apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: registry-credential-rotator-cron
spec:
  # Initially run every minute to create credentials.
  # Every run sets the schedule to a longer interval.
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: credential-rotator
            image: alpine:3.10
            command: ["/bin/sh", "-c"]
            args:
            - |
                DATE_FMT=%Y-%m-%dT%H:%M:%SZ
                set -eo pipefail
                MIN_TIME_AGO="$(date +%s)"
                MIN_TIME_AGO="$(expr $MIN_TIME_AGO - $MIN_ROTATION_INTERVAL_MINUTES '*' 60)" || (echo Invalid MIN_ROTATION_INTERVAL_MINUTES env var >&2; false)
                apk add --update --no-cache apache2-utils curl
                TOKEN="$(cat /run/secrets/kubernetes.io/serviceaccount/token)"
                APISERVER=https://kubernetes.default.svc
                ln -s /run/secrets/kubernetes.io/serviceaccount/ca.crt /usr/local/share/ca-certificates/ca.cert
                update-ca-certificates

                # Patch cronjob schedule to run once per day
                ERR="$(curl -fsS -XPATCH \
                  -H "Authorization: Bearer $TOKEN" \
                  -H 'Content-Type: application/strategic-merge-patch+json' \
                  "$APISERVER/apis/batch/v1beta1/namespaces/$NAMESPACE/cronjobs/$CRON_NAME" \
                  -d '{"spec":{"schedule": "0 0 1 * *"}}' 2>&1)" \
                  || (echo -n "Cannot patch CronJob $CRON_NAME schedule: $ERR" >&2; sleep 15; false)

                # Check if the secret exists and hasn't been updated after the given threshold
                HTPASSWD_SECRET_NAME=${DEPLOYMENT_NAME}-htpasswd
                RES_VERSION=
                HTPASSWD=''
                if SEC="$(curl -fsS -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$HTPASSWD_SECRET_NAME")"; then
                  HTPASSWD="$(echo "$SEC" | grep -Eo '"htpasswd":\s*"[^"]+"' | sed -E 's/^"htpasswd":\s*"|"$//g' | base64 -d)"
                  RES_VERSION="$(echo "$SEC" | grep -Eo '"resourceVersion":\s*"[0-9]+"' | grep -Eo '[0-9]+')"
                  LAST_ROTATION_DATE="$(echo "$SEC" | grep -Eo '"lastRotationDate":\s*"[^"]+"' | sed -E 's/^"lastRotationDate":\s*"|"$//g' | base64 -d)"
                  [ "$HTPASSWD" ] && [ "$RES_VERSION" ] && [ "$LAST_ROTATION_DATE" ] || (echo Unexpected contents in secret $HTPASSWD_SECRET_NAME >&2; false)
                  if [ "$(date --date "$LAST_ROTATION_DATE" --utc -D $DATE_FMT +%s)" -gt "$MIN_TIME_AGO" ]; then
                    echo Skipping credential rotation since the secret $HTPASSWD_SECRET_NAME has been updated recently >&2
                    exit 0
                  fi
                  RES_VERSION='"resourceVersion": "'"$RES_VERSION"'",'
                fi

                # Write secrets
                writeSecret() {
                  SEC="$(cat -)"
                  OUT="$(echo "$SEC" | curl -fsS -XPUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$1" -d @-)" \
                    || OUT="$(echo "$SEC" | curl -fsS -XPOST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets" -d @-)" \
                    || (echo -n "Cannot write secret '$1'. $OUT" >&2; sleep 15; false)
                }
                base64enc() {
                  echo -n "$1" | base64 | xargs | sed -e 's/ //g'
                }
                generateDockerConfSecret() {
                  # see https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
                  REGISTRY_AUTH='{"auth":"'"$(base64enc "$2:$3")"'"}'
                  REGISTRY_AUTH='{
                    "auths": {
                      "https://'"$COMMON_NAME"'/v2/": '"$REGISTRY_AUTH"',
                      "https://'"${SERVICE_NAME}.${SERVICE_NAMESPACE}.svc"'/v2/": '"$REGISTRY_AUTH"',
                      "https://'"$SERVICE_NAME"'/v2/": '"$REGISTRY_AUTH"'
                    }
                  }'
                  echo '{
                    "metadata": {
                      "name": "'"$1"'"
                    },
                    "data": {
                      ".dockerconfigjson": "'"$(base64enc "$REGISTRY_AUTH")"'",
                      "config.json": "'"$(base64enc "$REGISTRY_AUTH")"'",
                      "username": "'"$(base64enc "$2")"'",
                      "password": "'"$(base64enc "$3")"'",
                      "ca.crt": "'"$(cat /cert/ca.crt | base64 | tr -d '\n')"'"
                    },
                    "type": "kubernetes.io/dockerconfigjson"
                  }'
                }
                randomPasswd() {
                  RPWD="$(cat /dev/urandom | tr -dc '%/._A-Z-a-z-0-9' | head -c32)" || [ "$RPWD" ]
                  echo "$RPWD"
                }
                USER_SUFFIX=$(date +%Y%m%d%H%M%S)
                PULL_USER=pull-$USER_SUFFIX
                PULL_PASSWORD="$(randomPasswd)"
                PUSH_USER=push-$USER_SUFFIX
                PUSH_PASSWORD="$(randomPasswd)"
                HTPASSWD="$(echo -n "$PULL_PASSWORD" | htpasswd -niB "$PULL_USER" && echo "$HTPASSWD")"
                HTPASSWD="$(echo -n "$PUSH_PASSWORD" | htpasswd -niB "$PUSH_USER" && echo "$HTPASSWD")"
                HTPASSWD="$(echo "$HTPASSWD" | sed -E '/^\s*$/d' | head -4)"
                echo "$HTPASSWD" > htpasswd
                echo -n "$PULL_PASSWORD" | htpasswd -vi htpasswd "$PULL_USER"
                echo -n "$PUSH_PASSWORD" | htpasswd -vi htpasswd "$PUSH_USER"
                rm htpasswd
                writeSecret $HTPASSWD_SECRET_NAME <<-EOF
                  {
                    "metadata": {
                      $RES_VERSION
                      "name": "$HTPASSWD_SECRET_NAME"
                    },
                    "data": {
                      "lastRotationDate": "$(base64enc $(date --utc +$DATE_FMT))",
                      "htpasswd": "$(base64enc "$HTPASSWD")"
                    }
                  }
                EOF
                sleep 30 # give pods time to update before publishing new credentials
                # TODO: generate new credentials every time but publish only last new generated credentials

                generateDockerConfSecret ${DEPLOYMENT_NAME}-pull-credentials "$PULL_USER" "$PULL_PASSWORD" |
                  writeSecret ${DEPLOYMENT_NAME}-pull-credentials
                generateDockerConfSecret ${DEPLOYMENT_NAME}-push-credentials "$PUSH_USER" "$PUSH_PASSWORD" |
                  writeSecret ${DEPLOYMENT_NAME}-push-credentials
                echo secrets successfully written
            env:
            - name: CRON_NAME
              value: $(CRON_NAME)
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: SERVICE_NAMESPACE
              value: $(SERVICE_NAMESPACE)
            - name: SERVICE_NAME
              value: $(SERVICE_NAME)
            - name: COMMON_NAME
              value: $(COMMON_NAME)
            - name: MIN_ROTATION_INTERVAL_MINUTES
              # in case a job is run more than once this avoids updating the secret more than once
              value: "1"
            - name: DEPLOYMENT_NAME
              value: $(DEPLOYMENT_NAME)
            resources:
              requests:
                cpu: "10m"
                memory: "8Mi"
              limits:
                cpu: "100m"
                memory: "32Mi"
            volumeMounts:
            - name: registry-cert
              mountPath: /cert
          volumes:
          - name: registry-cert
            secret:
              secretName: $(CERT_SECRET_NAME)
          restartPolicy: OnFailure
          terminationGracePeriodSeconds: 30
          serviceAccountName: registry-credential-rotator
          automountServiceAccountToken: true
      backoffLimit: 0
      activeDeadlineSeconds: 300
