Skip to content

Where game pods land, and who decides

A group can ask for node selectors, tolerations and affinity, but only as far as its Network lets it. This is the part of the operator that is not ordinary Kubernetes: the namespace is the boundary a group author is held to, and scheduling reaches past it, onto a control-plane node, a cordoned one, or beside somebody else's workload. So the Network's owner has to widen that boundary first.

That makes the permission two-sided, and both halves have to exist before anything works. Here they are together: a network that allows one taint key and one node label, and a group that uses both to keep off the database nodes. Save this as scheduling.yaml:

apiVersion: spawnery.cloud/v1alpha1
kind: Network
metadata:
  name: production
  namespace: minecraft
spec:
  # The Secret the sample and the tutorial both create; this half widens an
  # existing network rather than replacing it.
  forwardingSecretRef:
    name: velocity-forwarding-secret
  scheduling:
    allowedTolerationKeys:
      - workload
    allowedNodeSelectorKeys:
      - node-role.example.com/games
---
apiVersion: spawnery.cloud/v1alpha1
kind: ServerGroup
metadata:
  name: lobby
  namespace: minecraft
spec:
  networkRef:
    name: production
  type: Ephemeral
  image: ghcr.io/spawnery/purpur:26.3-0.34.0
  maxPlayers: 50
  scaling:
    minReplicas: 1
    maxReplicas: 5
    spareSlots: 20
  scheduling:
    nodeSelector:
      node-role.example.com/games: "true"
    tolerations:
      - key: workload
        operator: Equal
        value: games
        effect: NoSchedule
kubectl apply -f scheduling.yaml
kubectl get servergroup lobby -n minecraft

Apply only the group half and it is refused, which is the single most common way to meet this feature.

Reading the refusal

A group whose scheduling the network does not allow gets Accepted: False with reason SchedulingNotAllowed, a Warning event carrying the same text, and no servers at all:

kubectl get servergroup lobby -n minecraft \
  -o jsonpath='{.status.conditions[?(@.type=="Accepted")]}'

The message names the key and the Network field that would allow it, because the person reading it is usually a group author who can write neither the taint nor the Network, and "not allowed" on its own sends them to look through the wrong object. The operator assembles the message at the moment it refuses, so the wording below is exactly what it writes, with this page's own network and key names substituted in:

network "production" allows no scheduling; its spec.scheduling names what a group may ask of the scheduler, and it is unset
toleration key "workload" is not in network "production"'s spec.scheduling.allowedTolerationKeys
nodeSelector key "node-role.example.com/games" is not in network "production"'s spec.scheduling.allowedNodeSelectorKeys

Node affinity is checked against the same list as nodeSelector, and says so with node affinity key in place of nodeSelector key.

What the network can allow

Each list is a plain allowlist. An absent or empty list allows nothing; there is no "unset means anything" here, and a Network with no spec.scheduling at all refuses every group that asks for scheduling.

Field Governs
allowedTolerationKeys which taint keys a group may tolerate
allowedNodeSelectorKeys which node label keys it may select on, in nodeSelector and in node-affinity terms alike
allowedAffinityNamespaces which namespaces besides its own a pod-affinity term may name
hostPortRange which ports a HostPort proxy group may bind

Two things no network can allow, at any setting:

  • A toleration with no key. It tolerates every taint, including the ones that exist to keep workloads off a node. The refusal says so in those words: a toleration with no key tolerates every taint, which no network allows.
  • A pod-affinity term with a namespaceSelector. It can name any namespace, so allowlisting namespaces would mean nothing beside it.

Both are refused outright rather than made configurable, because an allowlist with a wildcard in it is not an allowlist.

HostPort is part of this, which is easy to miss

A proxy group exposed by HostPort binds a port on the node, so it is a scheduling question too and it goes through the same gate. Without spec.scheduling.hostPortRange on the Network, no HostPort group is accepted. The same two shapes, with the names filled in:

network "production" allows no host port; its spec.scheduling.hostPortRange is unset
host port 25565 is outside network "production"'s spec.scheduling.hostPortRange 30000-32767
spec:
  scheduling:
    hostPortRange:
      min: 25565
      max: 25599

The other costs of HostPort (the node cap on replicas, and Pod Security refusing it outright in a baseline or restricted namespace) are in How players reach the proxies.

Inheriting it instead of repeating it

Scheduling can also come from Network.spec.defaults.scheduling, which every group inherits and any group may override. The allowlist is applied to the effective scheduling either way, so a default the Network sets for its own groups still has to be something the Network's policy permits; the two halves do not shortcut each other.

A note for upgrades

This gate arrived in 0.2.33. A group that set spec.scheduling before that version and kept working can stop being accepted on upgrade, if its Network never gained a matching policy. Release notes carries what that upgrade costs and what to do about it; this page is about the model itself.

Every field named here is in the generated custom resource reference.