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:

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 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.