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.