Install the operator

This guide installs library-operator on a liken cluster. At the end, the operator runs, a namespace has its catalog, and a Library can be declared.

You need:

This operator publishes no devices and needs no DeviceClass. The screen it draws uses the Player’s existing display claim, which media-operator holds.

1. Create the storage

The media claim

A Library names an existing claim in spec.storage.claim. Any volume the cluster can mount works: an NFS export, a Longhorn volume, a local disk on a single-node cluster.

Two kinds of pod mount the claim, and they can land on different nodes. A screen mounts it read-only. The Library’s Job mounts it read-write, because its phases write the .nfo and art files beside the media, and its scan container mounts it read-only. So the claim has to allow more than one node at once, which on most clusters means ReadWriteMany:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: movies-pvc
  namespace: media
spec:
  accessModes: [ReadWriteMany]
  resources:
    requests:
      storage: 500Gi
  storageClassName: nfs

A ReadWriteOnce claim works only when every pod that mounts it is on the same node, which nothing in the schema enforces.

A franchises library needs a second claim for the art its scan downloads. Franchises covers it.

The catalog claims

The catalog is SQLite, and one agent writes each copy. So the operator provisions a catalog claim as ReadWriteOnce, and as ReadWriteMany on a per-node class, where every node holds a copy of its own. A Library’s claim on a per-node class is ReadWriteOncePod, so Kubernetes admits one pod of it in the cluster at a time. The namespace’s Catalog names the class of each kind of claim:

A class left empty binds to the cluster’s default class.

Keep the catalog of record and the progress store on classes that survive a lost node, such as block storage from a SAN. A SQLite file on NFS can corrupt when its node is lost, so do not use an NFS class. A Library’s claim is a working copy that a Job rebuilds from the catalog of record, and a screen pod is pinned to the machine that holds its display. A node-local class such as local-path fits both.

2. Apply the manifests

The install is the kustomize base in the operator’s deploy/ directory. Take it into a kustomization of your own and pin <tag> to the operator’s version, so the install is the same every time it is applied:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: liken-system

resources:
  - https://github.com/liken-sh/liken//library-operator/deploy?ref=<tag>

images:
  - name: ghcr.io/liken-sh/library-operator
    newTag: <tag>

The operator’s version is the release tag that last published it. The GitHub release for each tag lists every component and its version.

The base creates the three CustomResourceDefinitions, a ServiceAccount, a ClusterRole with its binding, a Role with its binding, one Deployment, and one Service. The Deployment runs one unprivileged replica with every capability dropped. The Service is the address every Library’s webhook is reached at.

Only one copy of the operator acts at a time. Each copy competes for the Lease named library-operator in the operator’s namespace, and only the copy that holds it watches, reconciles, and opens a bus session. The Role grants that one Lease. A rollout starts the new pod beside the old one. The old pod finishes its pass and releases the Lease when it stops, and the new pod takes it within about 11 seconds. A copy that waits for the Lease still takes webhooks, and it serves each one when it leads.

A copy that cannot renew the Lease, for example while the API server restarts, stops writing 10 seconds after its last renewal and exits. The kubelet restarts the container in the same pod. Because the earlier holder was a process of the same pod, the new process takes the Lease on its first read after the Lease expires, 30 seconds after the last renewal. A copy in another pod waits 30 seconds from the moment it first reads that Lease.

The ClusterRole is cluster-wide because a Library can be in any namespace. Its grants, by object:

This site serves the same files as raw YAML, so a clone is never needed: libraries-crd.yaml , catalogs-crd.yaml , metadataproviders-crd.yaml , rbac.yaml , and operator.yaml .

3. Declare a Catalog

A Library is namespaced, and each namespace that holds one has a catalog of its own. The namespace is a boundary: every Library in it writes into one catalog, and a screen shows that catalog. Declare exactly one Catalog in the namespace before the first Library. A Library in a namespace with no Catalog waits with the reason NoCatalog, and a second Catalog blocks both.

apiVersion: library.liken.sh/v1alpha1
kind: Catalog
metadata:
  name: media
  namespace: media
spec:
  storage: {}

An empty storage provisions a 1Gi claim on the default class. Catalog describes every field, and The catalog describes what the pod it creates does.

4. Confirm it runs

kubectl -n liken-system get pods
kubectl -n liken-system logs deploy/library-operator

The operator’s log reports its first pass and the bus it reports over:

library.liken.sh: operating 0 libraries over bus.liken-system.svc:1883

A missing LIBRARY_BUS_ADDRESS or OPERATOR_NAMESPACE is an error at startup, printed to the log, and the pod exits. The served operator.yaml sets both.

Now declare a library . Once it is Ready, the listing shows its counts and its phase:

$ kubectl -n media get libraries
NAME     KIND     TITLES   ITEMS   FILES   WAITING   SOURCES   STATUS   READY   AGE
movies   movies   0        0       0       0                   Idle     True    2m

Running a development build

A push to main that changes the operator publishes a development build of it. Its version is the most recent release tag plus a suffix: 2026.09.03-007-dev-003-abcdef01 is three commits past release 2026.09.03-007, at commit abcdef01. Every image of the operator has the same version, and :latest still names the most recent release.

A development build has no git tag, so the manifests pin to the commit’s full sha, and the image pins to the version:

resources:
  - https://github.com/liken-sh/liken//library-operator/deploy?ref=<full 40-character sha>
images:
  - name: ghcr.io/liken-sh/library-operator
    newTag: 2026.09.03-007-dev-003-abcdef01

A git fetch by sha needs all forty characters; the eight in the version are not enough. The summary of the CI run for that commit gives the version.

The same build publishes the manifests as the OCI artifact oci://ghcr.io/liken-sh/library-operator-deploy:<version>, with each image of the operator set to that version. A Flux OCIRepository can pull that artifact by the version, with no sha.

Remove the operator

Delete every Library first, and wait for each one to go. A Library has a finalizer that the operator releases after a cleanup Job removes its rows from the namespace’s catalog. If the operator’s Deployment is gone, nothing runs that Job, and the Library stays Terminating until a person patches the finalizer off.

kubectl -n media delete library movies
kubectl -n media delete catalog media
kubectl delete -k https://github.com/liken-sh/liken//library-operator/deploy?ref=<tag>

Deleting the Catalog deletes the catalog pod and the claim the operator provisioned for it. Deleting the base deletes the CustomResourceDefinitions, and with them every MetadataProvider. The media claim and what is on it stay.