# The operator's identity in the cluster, and deliberately
# nothing more. It reads libraries and catalogs, writes their statuses,
# reads the storage behind each library, provisions the catalog claims,
# stands the catalog pod of each namespace that holds a Catalog with the
# catalog Service and EndpointSlice beside it, and creates the CronJob
# each library becomes and the Jobs a webhook and a departure ask for.
# It deletes an object to replace a stale one, to end a run, or to
# recover a screen the scheduler cannot place, which takes that
# screen's catalog claim with the pod. Every other teardown is the
# garbage collector's, through the ownerReferences the pods, the
# claims, the CronJob, and the catalog objects carry. It deletes no
# object a person created. Every grant is cluster-wide, because a
# Library can be in any namespace.
#
# The operator also holds a finalizer on every Library, so a delete
# waits on a cleanup Job that takes the departed library's rows out of
# the namespace's catalog. The finalizer is why libraries carry patch
# below.
#
# The webhook is on the operator itself, over one Service in the
# operator's own namespace, so this file grants nothing for it.
#
# it also reads the Players media-operator publishes, and stands one
# screen pod for each Player that names it as the idle controller. It writes
# no Player. A screen pod is deleted for a third reason beyond a stale
# template and a departure: the Player it stands for named another idle
# controller, or none.
#
# No pod this operator creates holds a credential of its own.
# The catalog pod reports over the bus, and the operator writes what it
# reports. A screen asks over the bus, and the operator creates the Play
# it asked for. So nothing in this file grants a pod anything.
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: library-operator
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: library-operator
rules:
  # The libraries, across every namespace, because a cluster keeps its
  # media in whatever namespace it likes and the operator is one
  # Deployment for the cluster. The watch is the wake signal; every
  # pass re-reads with a list. A person declares a Library and the
  # operator never edits one: the patch is for the finalizer list
  # alone, sent as a merge patch so a person's own fields on the
  # Library survive the write. The finalizer holds a deleted Library
  # open until its rows are out of every surviving agent's catalog.
  # There is no update verb, because nothing here rewrites a spec.
  - apiGroups: [library.liken.sh]
    resources: [libraries]
    verbs: [get, list, watch, patch]
  # Only the status half. The API server's subresource split is what
  # keeps this operator from ever rewriting a spec a person declared.
  - apiGroups: [library.liken.sh]
    resources: [libraries/status]
    verbs: [update]
  # The Catalogs, across every namespace, on the same terms as the
  # libraries: the watch is the wake signal, and every pass re-reads
  # with a list. Read-only, and only the status half is written.
  - apiGroups: [library.liken.sh]
    resources: [catalogs]
    verbs: [get, list, watch]
  - apiGroups: [library.liken.sh]
    resources: [catalogs/status]
    verbs: [update]
  # The operator reads the MetadataProviders of every namespace and
  # writes only their status: the verdict of the one reachability
  # check it runs per pass. A person declares a provider, and the
  # operator never edits one.
  - apiGroups: [library.liken.sh]
    resources: [metadataproviders]
    verbs: [get, list, watch]
  - apiGroups: [library.liken.sh]
    resources: [metadataproviders/status]
    verbs: [update]
  # The operator reads a Secret only to run that check. The key
  # reaches an enricher container through a secretKeyRef the kubelet
  # resolves, and the operator never writes it into a status, a log,
  # or the catalog.
  - apiGroups: [""]
    resources: [secrets]
    verbs: [get]
  # The Players, across every namespace, read-only. media-operator owns
  # them and writes them; this operator reads status.idle and writes no Player
  # at all. It stands a screen pod for a Player whose status.idle.controller
  # names it, and takes that pod down when the name changes. The watch is the
  # wake signal, and every pass re-reads with a list.
  - apiGroups: [media.liken.sh]
    resources: [players]
    verbs: [get, list, watch]
  # The household defaults, cluster-scoped and read-only, for one field:
  # the wall-clock zone every screen pod carries as TZ, so the browser's
  # clock shows the house's own hour. The watch is the wake signal, so a
  # zone the household sets rolls the screens on the next pass.
  - apiGroups: [media.liken.sh]
    resources: [mediapreferences]
    verbs: [get, list, watch]
  # The Plays this operator creates, one per request a screen publishes
  # on the bus. Create alone: the operator never reads, edits, or
  # deletes a Play, because media-operator owns its whole life once it
  # exists. The grant is cluster-wide because a Play is created in the
  # Player's namespace, and a Player can be in any namespace.
  - apiGroups: [media.liken.sh]
    resources: [plays]
    verbs: [create]
  # The media claim each Library names, read by name to learn whether
  # it is bound and which volume it is bound to. The catalog claims the
  # operator provisions: one per Library for its Jobs, one per Catalog for
  # the catalog pod, and one per screen, each owned by the object it belongs
  # to, so the garbage collector removes it with that owner. There is no
  # update, because a claim's spec is immutable once it binds. The delete
  # covers one claim: the catalog claim of a screen the scheduler has refused
  # for longer than the grace, and the operator sends it only when the
  # claim's name is the derived screen claim name, it carries the screen name
  # label and the player label, and its controller ownerReference names the
  # Player with the UID this pass read.
  - apiGroups: [""]
    resources: [persistentvolumeclaims]
    verbs: [get, create, delete]
  # The volume behind that claim, read by name for its source: an NFS
  # server and export, or the name of whatever other key serves it.
  # Playing a title from the library needs that, so the operator reads
  # it once per pass and reports it in the status. PersistentVolumes
  # are cluster-scoped, which is why this rule needs a ClusterRole.
  - apiGroups: [""]
    resources: [persistentvolumes]
    verbs: [get]
  # The pods this operator stands itself: the catalog pod of each
  # namespace that holds a Catalog, and one screen pod per delegated
  # Player. Both are created, read back on every pass, and deleted to
  # rebuild a stale template; a screen pod is deleted for a second
  # reason, that its Player named another idle controller. The list and
  # watch are cluster-scoped, because a Catalog and a Player live in any
  # namespace, and the watch wakes the loop when a pod becomes ready or
  # Kubernetes removes one. The pods of a Job are the Job controller's
  # and never this operator's.
  - apiGroups: [""]
    resources: [pods]
    verbs: [get, list, watch, create, delete]
  # The workers. One CronJob per Library runs its full walk on a
  # schedule, a Job runs one folder scan for a webhook, and a Job runs
  # the sweep of a departing Library. The list and watch are
  # cluster-scoped for the reason the pods are, and every pass re-reads
  # with the list. The delete verb covers three deletes: the schedule of
  # a Library that no longer stands one, a cleanup Job that failed and
  # is stood again on a backoff, and the retirement of a cleanup Job
  # when its library is released. A CronJob is also updated, because a
  # changed schedule or a new image rewrites it in place. A Job is
  # never updated: it runs once as it was created.
  - apiGroups: [batch]
    resources: [jobs]
    verbs: [get, list, watch, create, delete]
  - apiGroups: [batch]
    resources: [cronjobs]
    verbs: [get, list, watch, create, update, delete]
  # The headless catalog Service of each namespace that holds a
  # Catalog, read by name. The grant is cluster-wide because a Catalog
  # can be in any namespace, and the operator is one Deployment for the
  # cluster. There is no list and no watch: it is read by name, in the
  # namespaces the Catalog list already named. There is no delete verb:
  # the Service is owned by the Catalog, and the garbage collector
  # removes it with the Catalog.
  - apiGroups: [""]
    resources: [services]
    verbs: [get, create, update]
  # The EndpointSlice behind the catalog Service, on the same terms
  # and for the same reasons. There is one per namespace that holds a
  # Catalog, and the operator writes it because that Service names no
  # selector.
  - apiGroups: [discovery.k8s.io]
    resources: [endpointslices]
    verbs: [get, create, update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: library-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: library-operator
subjects:
  - kind: ServiceAccount
    name: library-operator
    namespace: liken-system
