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 claims the display through the Player’s own standing 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.

Three kinds of pod mount the claim, and they can land on different nodes. Every scan Job mounts it read-only. The enrich Job mounts it read-write, because it writes the .nfo and art files beside the media. 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 every catalog claim as ReadWriteOnce, and the namespace’s Catalog names the class: spec.storage.storageClassName for the durable catalog, and spec.screens.storageClassName for the copy each screen holds. Either one left empty binds to the cluster’s default class.

A SQLite file on NFS can corrupt when its node is lost, so give the durable catalog a class that binds node-local storage. A screen pod is already pinned to the machine that holds its display, so a node-local class such as local-path fits the screens too.

2. Apply the manifests

The install is the kustomize base in the repository’s deploy/ directory. Take it into a kustomization of your own and pin <tag> to a release, 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/library-operator//deploy?ref=<tag>

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

The base creates the three CustomResourceDefinitions, a ServiceAccount, a ClusterRole 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.

The ClusterRole is cluster-wide because a Library can be in any namespace. Its grants are read and status writes on this operator’s own resources, read on media-operator’s Player and MediaPreferences, create on Play, and create and delete on the claims, pods, Jobs, CronJobs, and Services it owns.

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: catalog
  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   STATUS   READY   AGE
movies   movies   0        0       0       0         Idle     True    2m

Running a development build

Every push to the operator’s main branch publishes a development build. Its version is the most recent release plus a suffix: 2026.09.03-007-dev-003-abcdef01 is three commits past release 2026.09.03-007, at commit abcdef01. Every image the repository builds carries 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/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 CI run for that commit prints both lines in its summary.

Remove the operator

Delete every Library first, and wait for each one to go. A Library carries 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 catalog
kubectl delete -k https://github.com/liken-sh/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.