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:
- A
likencluster. - The
media-operator, installed first, inliken-system. It owns the players, the plays, and the remotes, and its bus atbus.liken-system.svc:1883is where every catalog reports. ALibraryin a cluster with no bus reportsOfflineand never reachesReady. - A volume that holds the media, as a
PersistentVolumeClaimin the namespace where theLibrarywill be. The operator never creates this claim. See The media claim below. - A
StorageClassfor the catalogs. The operator provisions those claims itself, one per catalog pod, one per scanJob, and one per screen. See The catalog claims. kubectlwith cluster-admin access. The base creates aClusterRoleand threeCustomResourceDefinitions.
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.