Skip to content

Webhooks

Continuous integration on easy mode. Webhooks allow for a ton of functionality, but we are going to use it to kick off a kubernetes job. Effectivitely automating reloading content on a static website.

Background

This docs website is a static site that is hosted inside an nginx container. The storage for these redundant pods is a longhorn rwx pvc that gets stood up. To initialize the storage a kubernetes job is run. This job does the following:

  • git clones the rskntroot/rskio repo containing the artifacts required to render the site
  • executes the mkdocs command to render the static site

So what if when we push to github, we setup a webhook that tells kubernetes to kick off that job? Well, we achieve some form of automation. So how do we do this?

Setup

RBAC

apiVersion: v1
kind: ServiceAccount
metadata:
  name: webhook-job-trigger
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: job-creator
  namespace: dev
rules:
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: job-creator-binding
  namespace: dev
subjects:
  - kind: ServiceAccount
    name: webhook-job-trigger
    namespace: default
roleRef:
  kind: Role
  name: job-creator
  apiGroup: rbac.authorization.k8s.io
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: job-creator
  namespace: prod
rules:
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: job-creator-binding
  namespace: prod
subjects:
  - kind: ServiceAccount
    name: webhook-job-trigger
    namespace: default
roleRef:
  kind: Role
  name: job-creator
  apiGroup: rbac.authorization.k8s.io

ConfigMap

We will create a config map from a directory including the following files.

ConvertJob

We are going to be using curl to call the kubernetes API directly, so we need to convert our job from yaml to json.

Convert the job to JSON and save to etc/mkdocs-dev.json

apiVersion: batch/v1
kind: Job
metadata:
  generateName: mkdocs-builder-
  namespace: dev
spec:
  ttlSecondsAfterFinished: 600
  template:
    spec:
      containers:
        - name: mkdocs
          image: squidfunk/mkdocs-material
          command: ["/bin/sh", "-c"]
          args:
            - |
              git clone --single-branch -b dev https://github.com/rskntroot/rskio.git --depth 1 /docs
              cd /docs/mkdocs
              mkdocs build --site-dir /output
          volumeMounts:
            - name: mkdocs-storage
              mountPath: /output
      restartPolicy: Never
      volumes:
        - name: mkdocs-storage
          persistentVolumeClaim:
            claimName: mkdocs-pvc
mkdir etc
cat job.yml | yq -e -j | jq > etc/mkdocs-dev.json

The following docs we will assume that you also created etc/mkdocs-main.json.

Hooks

create etc/hooks.yaml

- id: rskio-mkdocs
  execute-command: /etc/webhook/reload.sh
  command-working-directory: /etc/webhook
  response-message: payload received
  response-headers:
    - name: Access-Control-Allow-Origin
      value: "*"
  pass-arguments-to-command:
    - source: payload
      name: ref
    - source: payload
      name: repository.full_name
  trigger-rule:
    and:
      - match:
          type: value
          value: push
          parameter:
            source: header
            name: X-GitHub-Event
      - match:
          type: value
          value: rskntroot/rskio
          parameter:
            source: payload
            name: repository.full_name

after testing come back to implement secrets

trigger-rule:
  and:
  - match:
      type: payload-hmac-sha1
      secret: mysecret
      parameter:
        source: header
        name: X-Hub-Signature

apply the configmap and rollout restart the webhook deployment

Command

etc/reload.sh
#!/bin/sh

REF=$1
REPO=$2

dispatch() {
    NS=$1
    JOB_JSON=$2
    SA_PATH="/var/run/secrets/kubernetes.io/serviceaccount"
    curl https://kubernetes.default.svc/apis/batch/v1/namespaces/${NS}/jobs \
        -X POST \
        -H "Authorization: Bearer $(cat ${SA_PATH}/token)" \
        -H "Content-Type: application/json" \
        --cacert "${SA_PATH}/ca.crt" \
        -d "@${JOB_JSON}"
}

docs(){
    case ${REF} in
        refs/heads/dev)
            dispatch dev "/etc/webhook/mkdocs-dev.json"
            ;;
        refs/heads/main)
            dispatch prod "/etc/webhook/mkdocs-main.json"
            ;;
        *)
            echo "skipping push to unsupported ref ${REF}"
            exit 0
            ;;
    esac
}

case ${REPO} in
    rskntroot/rskio)
        docs
        ;;
    *)
        echo "skipping push to unsupported repo ${REPO}"
        ;;
esac

Create

once all resources in etc are created run the following command:

kubectl create configmap webhook-etc --from-file=etc

if you need to update anything run the following:

kubectl delete configmap webhook-etc
kubectl create configmap webhook-etc --from-file=etc

Resources

The following resources will complete the work

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-docs
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webhook-docs
  template:
    metadata:
      labels:
        app: webhook-docs
    spec:
      serviceAccountName: webhook-job-trigger
      containers:
        - name: webhook-docs
          image: ghcr.io/linuxserver-labs/webhook:latest
          command: ["/app/webhook"]
          args:
            - -hooks=/etc/webhook/hooks.yaml
            - -hotreload
            - -verbose
          volumeMounts:
            - name: webhook-etc
              mountPath: /etc/webhook
      volumes:
        - name: webhook-etc
          configMap:
            name: webhook-etc
            defaultMode: 493 # 0755
apiVersion: v1
kind: Service
metadata:
  name: webhook
spec:
  selector:
    app: webhook-docs
  ports:
    - protocol: TCP
      port: 9000
      targetPort: 9000
  type: ClusterIP
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: io-rsk-dev-hooks-tls
spec:
  secretName: io-rsk-dev-hooks-tls
  issuerRef:
    name: dev-step-issuer
    kind: ClusterIssuer
  commonName: hooks.dev.rsk.io
  dnsNames:
    - hooks.dev.rsk.io
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
  usages:
    - server auth
    - client auth
  duration: 2160h # 90 days
  renewBefore: 360h # 15 days
  secretTemplate:
    annotations:
      kubeseal-secret: "true"
    labels:
      domain: hooks-dev-rsk-io
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: webhook
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`hooks.dev.rsk.io`)
      kind: Rule
      services:
        - name: webhook
          port: 9000
      middlewares:
        - name: ratelimit
  tls:
    secretName: io-rsk-hooks-tls

Testing

curl -X POST https://hooks.dev.rsk.io/hooks/rskio-mkdocs \
  -H 'X-Github-Event: push' \
  -H 'Content-type: application-json' \
  -d '{"ref": "refs/heads/dev","repository": {"full_name":"rskntroot/rskio"}}'

Github needs access to a public domain for this to work.