Give many applications one repository

One repository can hold the configuration of many applications, one directory per application. Each application gets its own writeable volume on the same repository and the same ref, and mounts only its own directory. No application reads or writes another one’s files.

Every volume here is a writeable volume, which Give an application a repository to write describes. The example repository is git@code.example.com:home/configuration.git. It holds assistant/configuration/ for a home automation service and maps/site/ for a static site.

A volume for each application

Each application gets its own PersistentVolume. The URL and the ref are the same on both. The volumeHandle is not: it names the volume on the node, so each volume needs its own.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: assistant-configuration
spec:
  capacity: {storage: 1Gi}
  accessModes: [ReadWriteOncePod]
  persistentVolumeReclaimPolicy: Retain
  volumeAttributesClassName: assistant-configuration
  csi:
    driver: git.liken.sh
    volumeHandle: assistant-configuration
    volumeAttributes:
      url: git@code.example.com:home/configuration.git
      ref: main
    nodeStageSecretRef:
      name: assistant-deploy-key
      namespace: home
    nodePublishSecretRef:
      name: assistant-deploy-key
      namespace: home
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: maps-site
spec:
  capacity: {storage: 1Gi}
  accessModes: [ReadWriteOncePod]
  persistentVolumeReclaimPolicy: Retain
  volumeAttributesClassName: maps-site
  csi:
    driver: git.liken.sh
    volumeHandle: maps-site
    volumeAttributes:
      url: git@code.example.com:home/configuration.git
      ref: main
    nodeStageSecretRef:
      name: maps-deploy-key
      namespace: home
    nodePublishSecretRef:
      name: maps-deploy-key
      namespace: home

Each volume names its own deploy key here, in both references. Two volumes can name one Secret instead, because they push to one repository. The credential says why a volume names its Secret twice.

A claim for each application

Each claim binds one volume and names a class. The binder pairs a claim and a static volume only when both name the same class, so the PersistentVolume above has the same volumeAttributesClassName.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: assistant-configuration
  namespace: home
spec:
  volumeName: assistant-configuration
  accessModes: [ReadWriteOncePod]
  resources: {requests: {storage: 1Gi}}
  volumeAttributesClassName: assistant-configuration
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: maps-site
  namespace: home
spec:
  volumeName: maps-site
  accessModes: [ReadWriteOncePod]
  resources: {requests: {storage: 1Gi}}
  volumeAttributesClassName: maps-site

A class for each application

The classes can differ. The home automation service writes a database beside its configuration, so its class ignores those files. The static site changes in bursts, so its class waits longer for the tree to go quiet.

apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
  name: assistant-configuration
driverName: git.liken.sh
parameters:
  push.quiesce: 30s
  commit.author: Assistant <assistant@home.example>
  ignore: "*.db*,*.log"
---
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
  name: maps-site
driverName: git.liken.sh
parameters:
  push.quiesce: 5m
  commit.author: Maps <maps@home.example>

The class reference lists every parameter, its values, and its default.

One directory in each pod

subPath on the volume mount publishes one directory of the tree into the container. The container gets that directory and nothing else of the repository. Use strategy: Recreate on a Deployment, because a rolling update would wait forever for a second pod that ReadWriteOncePod never lets start.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: assistant
  namespace: home
spec:
  replicas: 1
  strategy: {type: Recreate}
  selector:
    matchLabels: {app: assistant}
  template:
    metadata:
      labels: {app: assistant}
    spec:
      containers:
        - name: assistant
          image: registry.example.com/assistant:1
          volumeMounts:
            - name: configuration
              mountPath: /config
              subPath: assistant/configuration
      volumes:
        - name: configuration
          persistentVolumeClaim: {claimName: assistant-configuration}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: maps
  namespace: home
spec:
  replicas: 1
  strategy: {type: Recreate}
  selector:
    matchLabels: {app: maps}
  template:
    metadata:
      labels: {app: maps}
    spec:
      containers:
        - name: maps
          image: registry.example.com/maps:1
          volumeMounts:
            - name: site
              mountPath: /site
              subPath: maps/site
      volumes:
        - name: site
          persistentVolumeClaim: {claimName: maps-site}

When both applications push

Each volume commits and pushes on its own. The first of the two pushes succeeds. The second runs against a ref that moved, so the forge rejects it. The driver then fetches, rebases the volume’s commits onto upstream beside the pod’s tree, and pushes again, three times at most. The pod’s tree takes the result in one step that rewrites only the files upstream changed. Those files are the other application’s, and subPath keeps them out of its pod. The claim’s events include GitVolumeRebased, with the count of commits and the upstream commit.

When both applications write one file

The rebase does not settle a file that the application and upstream both changed. That volume moves to the branch <ref>.<volumeHandle>, and its events include GitVolumeDiverged. Every push goes there until a person merges it into the ref on the forge, and commits continue, so no work stops. At the volume’s next push after the merge, it is back on the ref and the side branch is deleted. The writeable guide’s When upstream moves gives the full rule.

The metadata record

The modes, owners, and empty directories of the tree are recorded on one ref, refs/git-csi/metadata, as the writeable guide says. Two writers share it. After a rebase, a volume takes the other application’s modes for the files the rebase brought in. The driver rebuilds a record the forge rejects on the record the forge holds, so neither application’s record overwrites the other’s. Leave the metadata parameter on.

A read-only claim beside the writers

A read-only claim on the same repository takes no part in any of this. The driver ignores a VolumeAttributesClass on such a claim, because a read-only volume commits nothing and pushes nothing. Mount a repository read-only gives its form.