Pair a controller and give it to a pod
This guide pairs a game controller with kubectl and hands it to one
pod. The example is a DualSense and a game in a namespace named
arcade, on a liken cluster with
the operator installed. Every step is a Kubernetes
API call, so RBAC controls who may do each one, and nobody needs a
shell on a node or in a pod.
1. Open a pairing window
Read the name of the adapter first. It is the radio’s address in lowercase with dashes:
kubectl get adapters
Then create a PairingRequest for it:
kubectl apply -f - <<'EOF'
apiVersion: bluetooth.liken.sh/v1alpha1
kind: PairingRequest
metadata:
name: new-gamepad
namespace: liken-system
spec:
adapter: 04-4a-69-66-92-27
windowSeconds: 180
EOF
The operator opens a window on that radio: it scans, and it stays
pairable and discoverable, for windowSeconds (180 by default, 15 to
900). Between windows the radio is neither, so nothing pairs with the
cluster while nobody asked.
2. Put the controller in pairing mode and read what the radio reports
On a DualSense, hold Create and PS until the light bar flashes. Then read the request:
kubectl get pairingrequest new-gamepad -n liken-system -o yaml
Every device the scan finds, and the cluster holds no bond with,
appears in status.seen with its address, its name, and when the
radio first observed it.
3. Approve the device you meant
Approval is a write to the request’s spec:
kubectl patch pairingrequest new-gamepad -n liken-system \
--type merge -p '{"spec":{"device":"A0:AB:51:33:B7:12"}}'
The operator pairs that device, trusts it so it reconnects on its
own, records the bond as a Pairing, and closes the window. The
request’s status.phase goes to Paired. A request nobody approves
only scans: an empty spec.device never pairs anything, the window
expires on its own, and the finished request is collected after
spec.ttlSecondsAfterFinished, a day by default.
To re-pair a device the cluster already records, set spec.device
when you create the request. An address set at creation is an
approval in advance.
4. See the published device
The bond is now a Pairing, its keys are in a Secret the Pairing
owns, and the controller is a device in this node’s ResourceSlice:
$ kubectl get resourceslice liken-1-bluetooth.liken.sh -o yaml
spec:
driver: bluetooth.liken.sh
nodeName: liken-1
devices:
- name: a0-ab-51-33-b7-12
attributes:
address: {string: "A0:AB:51:33:B7:12"}
connected: {bool: true}
name: {string: "DualSense Wireless Controller"}
From here on, PS alone reconnects the controller. The keys are
in the Secret, so they survive a pod restart, an upgrade, and a
reboot.
5. Claim the controller
If the Dynamic Resource Allocation
(DRA)
objects are new to you, read
How the pieces fit first. Then
create a
ResourceClaim
that selects the controller by its address:
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
name: player-one
namespace: arcade
spec:
devices:
requests:
- name: controller
exactly:
deviceClassName: bluetooth-input
selectors:
- cel:
expression: |
device.attributes["bluetooth.liken.sh"].address == "A0:AB:51:33:B7:12"
tolerations:
- key: bluetooth.liken.sh/disconnected
operator: Exists
effect: NoExecute
tolerationSeconds: 30
The toleration sets how long the radio may go silent before the
eviction controller ends the pod. Tolerate
bluetooth.liken.sh/disconnected and nothing else:
Devices explains why the other
taint must stay untolerated. Leave out the selector to claim any
paired controller.
6. Give the claim to a pod
apiVersion: v1
kind: Pod
metadata:
name: player
namespace: arcade
spec:
resourceClaims:
- name: controller
resourceClaimName: player-one
containers:
- name: game
image: ...
resources:
claims:
- name: controller
The container receives device nodes and nothing else:
/dev/input/event* for the one controller the claim allocated, which
on a DualSense is the gamepad and its motion sensors. No privilege,
no host mount, no environment variable. The container’s user must be
able to open the nodes.
If the controller is switched off, the pod parks Unschedulable and
starts when somebody turns it on. If the controller disconnects while
the pod runs, the eviction after tolerationSeconds ends the pod’s
session.
In a Deployment, claim through a ResourceClaimTemplate instead of
a standing ResourceClaim. A standing claim keeps its allocation
across an eviction, so the ReplicaSet’s replacement pods would
schedule onto a device that is gone and be evicted at once. A
template gives each replacement pod a fresh claim, and a fresh claim
needs a new allocation, which the taints block.
Unpair
Deleting the Pairing is the unpair:
kubectl delete pairing a0-ab-51-33-b7-12
The operator disconnects the controller, waits for any claim on it to
release, retires the device from the slice, and removes the bond. The
Secret with the keys is owned by the Pairing, so it is collected
with the object.