Install the operator
This guide installs observatory-operator on a
liken
cluster, and then runs a whole
observatory of INDI simulators: a dome, a weather station, two
telescopes, cameras, a filter wheel, a focuser, and a guider. At the
end, one telescope is reserved, and KStars on your desktop drives it.
No hardware is involved, so you can see every part of the operator
work before you describe your own equipment.
You need:
- A
likencluster. The simulators need no device, so any node can run them. kubectlwith cluster-admin access, because the install creates 20 CRDs.- KStars on your desktop, for the last part. Any INDI client works the same way.
Apply the manifests
The operator has no release yet, so you install a development build.
Every push to main that changes the operator publishes one. Its
version is the most recent release tag of the repository, plus a
suffix: 2026.10.04-004-dev-124-93f85ef4 is 124 commits past
release 2026.10.04-004, at commit 93f85ef4. The summary of the CI
run for that commit gives the version, and the
package page
lists every version that exists.
A development build has no git tag, so the manifests pin to the
commit’s full sha, and the image pins to the version. Write this
kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/liken-sh/liken//observatory-operator/deploy?ref=<full 40-character sha>
images:
- name: ghcr.io/liken-sh/observatory-operator
newTag: <version>
A git fetch by sha needs all forty characters. The eight in the version are not enough. Then apply it:
kubectl apply -k .
The base creates:
- the CRDs of the
observatory.liken.shgroup - a
ServiceAccount, aClusterRole, and aClusterRoleBinding - a
Roleand aRoleBindingthat let the operator hold itsLease,observatory-operatorinliken-system - the operator, a
Deploymentwith one replica inliken-system, the namespace of everylikenoperator
The operator watches every namespace. You choose the namespace your
observatory lives in, and the operator runs the observatory’s pods,
Services, and Jobs in that namespace, beside its resources. So
your ResourceQuota, NetworkPolicy, Pod Security level, and RBAC
grants in that namespace cover the observatory. Every resource of one
observatory goes in one namespace, because the resources name each
other. This guide uses observatory:
kubectl create namespace observatory
This site also serves the manifests as raw YAML, if you want to read them first or write your own.
Check that the operator runs:
kubectl -n liken-system rollout status deployment/observatory-operator
kubectl -n liken-system logs deployment/observatory-operator
Run the example observatory
The example declares a site named lab with two telescopes, east
and west, and a device of every kind the operator supports. Every
driver is an INDI simulator. Apply it:
kubectl apply -n observatory -f https://liken.sh/observatory/examples/simulators.yaml
Every kind is in the category astro, so one command lists the whole
observatory:
kubectl get astro -n observatory
The devices of east and west are Idle. Describing equipment
starts nothing. The operator starts pods only for a telescope that a
Reservation holds. The spare focuser is Inventory, because it is
on the shelf: it names no optical train.
The last resource in the example is the Reservation east-tonight.
It has no start time, so it activates the east telescope at once.
Watch it work through its steps:
kubectl get rsv -n observatory -w
Each line shows the phase, the step that runs, and what that step waits for. The first activation pulls the INDI images, so it takes longer than the next one. The camera’s activation cools the sensor to -10 °C and waits for it, which takes a few minutes on the simulator. To wait for the end in a script:
kubectl wait --for=condition=Ready reservation/east-tonight -n observatory --timeout=15m
When the reservation is Ready, the telescope’s pods run, every
device is connected, the dome is open, the mount is unparked, and
PHD2 is connected to the guide camera and the mount:
kubectl get pods -n observatory
kubectl get mnt,cam,dome,guider -n observatory
How a reservation runs explains each step.
Connect KStars
The reservation’s status names the telescope’s INDI server:
kubectl get rsv east-tonight -n observatory -o jsonpath='{.status.endpoint.host}:{.status.endpoint.port}'
That address, east-telescope.observatory.svc:7624, works only inside
the cluster. From your desktop, forward the port:
kubectl port-forward -n observatory svc/east-telescope 7624
In KStars, open Ekos, create a profile, and set its mode to remote
with host localhost and port 7624. Leave the device list empty:
Ekos takes every device the server offers. Start the profile, and the
INDI control panel lists the simulators by their INDI names, such as
Telescope Simulator and CCD Simulator.
For guiding, forward PHD2’s event server too, in a second terminal:
kubectl port-forward -n observatory svc/east-guider 4400
In the Ekos guide module, choose PHD2 as the guider, with host
localhost and port 4400. Guide with PHD2
covers the guider in full.
A port forward is the simplest path, and it is enough for one person at one desktop. To reach the telescope another way, see Reach the telescope from outside the cluster .
End the reservation
Delete the reservation to end the night:
kubectl delete reservation east-tonight -n observatory
The delete waits while the operator runs the deactivation steps: it stops guiding, closes the dust cap, warms the camera, parks the mount, parks the dome, and stops the pods. That takes a few minutes, mostly for the camera’s warm-up. When the command returns, the equipment is safe to power off.
Rollouts and failover
Only one copy of the operator acts at a time: the copy that holds the
Lease observatory-operator in liken-system. Two copies that
acted at once would both run the same reservation’s steps on the same
devices. Every other copy only reads the Lease. It opens no watch,
creates no pod, and sends nothing to a device. To see which pod leads:
kubectl -n liken-system get lease observatory-operator
A rollout starts the new pod beside the old one, and the new pod logs
that it waits for the Lease. When the old pod stops, it stops its
reservations’ steps and closes its INDI connections. It waits until
its last write to the API server returns, the Events it posted
included, and then it releases the Lease. The new pod takes the
Lease within about 11 seconds. When an Event write does not return
within 5 seconds, the old pod exits without the release, and the new
pod takes the Lease when it expires, 30 to 41 seconds later. It reads which
reservation holds each telescope from the reservations’ status, and
continues each reservation from the step that its status records. The
INDI servers and the device pods run through the rollout, so a
connected device stays connected.
When the leading pod cannot renew the Lease, for example because
its node lost the network, it stops writing 10 seconds after its last
renewal, and exits within 25 seconds of it. A waiting copy takes the Lease 30 to 41
seconds after the last renewal, so the two copies never act at the
same time. The base runs one replica, so the waiting copy is the
replacement pod that Kubernetes starts on another node, or the same
pod after the kubelet restarts it.
Upgrade from a base that created observatory
The base of releases before 2026.10.08-002 created the namespace
observatory and ran the operator in it. The base now creates no
namespace. A GitOps tool that prunes, such as Flux with prune: true,
deletes what a new version of a source no longer lists, so it deletes
observatory and every resource in it on the upgrade. Before you
upgrade, take the namespace out of the base’s hands. Declare it in
your own manifests, or mark it so Flux keeps it:
kubectl annotate namespace observatory kustomize.toolkit.fluxcd.io/prune=disabled
Flux then deletes the old Deployment, ServiceAccount, Role,
and RoleBinding in observatory, and creates the operator in
liken-system. With kubectl apply -k, delete those four yourself
before you apply the new base, so two copies of the operator never
run at once. The old copy holds no Lease, so the election does not
keep it apart from the new one. The new
operator finds the pods of a running reservation where they are, in
observatory.
Remove the operator
Remove your resources first, while the operator still runs:
kubectl delete -n observatory -f https://liken.sh/observatory/examples/simulators.yaml
A resource that the operator is still running something for carries
the finalizer observatory.liken.sh/deactivate, and its delete waits
for the operator. If the operator is gone first, those deletes wait
forever. Deleting a running resource
explains the finalizer and how to remove it by hand.
Then delete the base:
kubectl delete -k .
This deletes the CRDs, and with them every resource of the
observatory.liken.sh group that is left.