Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1824,17 +1824,21 @@ jobs:
# (operator does zero logins and is wait-dominated, so it runs
# wide; the api buckets stay within the login limiter's budget).
# The arm64 job below uses the same steps and per-bucket settings.
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components, management-placement]
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components, wake-hostport, management-placement]
arch: [amd64]
include:
- bucket: operator
parallel: 8
test_timeout: 20m
job_timeout: 60 # Boots a kind cluster, installs the chart, runs the bucket's suite.
- bucket: wake-hostport
parallel: 1
test_timeout: 15m
job_timeout: 60 # Boots two-node kind, installs the chart and exercises repeated Hostport sleep/wake handoffs.
- bucket: management-placement
parallel: 1
test_timeout: 15m
job_timeout: 60
job_timeout: 60 # Boots management kind, installs the chart and checks placement/disruption behavior.
- bucket: api-auth
parallel: 4
test_timeout: 15m
Expand Down Expand Up @@ -1892,7 +1896,7 @@ jobs:
- name: bring up cluster + helm install
if: env.RUN_REGEX != '' || env.TAIL_REGEX != ''
env:
GAMEPLANE_E2E_PROFILE: ${{ matrix.bucket == 'management-placement' && 'management' || 'default' }}
GAMEPLANE_E2E_PROFILE: ${{ matrix.bucket == 'wake-hostport' && 'two-node' || matrix.bucket == 'management-placement' && 'management' || 'default' }}
run: ./deploy/kind/e2e.sh up "$CLUSTER" e2e "$GAMEPLANE_E2E_PROFILE"
- name: run go e2e (${{ matrix.bucket }})
if: env.RUN_REGEX != ''
Expand Down Expand Up @@ -2697,7 +2701,7 @@ jobs:
'go-e2e-unit': (n) => n === 'go (test/e2e)',
'api-postgres': (n) => n === 'api (postgres)',
'e2e-buckets': (n) => n === 'e2e bucket coverage',
'e2e-go': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|management-placement) \//.test(n) && n.includes('/ amd64 '),
'e2e-go': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|wake-hostport|management-placement) \//.test(n) && n.includes('/ amd64 '),
'e2e-go-arm64': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components) \//.test(n) && n.includes('/ arm64 '),
'e2e-multicluster': (n) => n.startsWith('e2e multicluster /') && n.includes('/ amd64 '),
'e2e-multicluster-arm64': (n) => n.startsWith('e2e multicluster /') && n.includes('/ arm64 '),
Expand Down Expand Up @@ -2850,7 +2854,7 @@ jobs:
const bucketSet = new Set();
for (const j of apiJobs) {
const n = j.name || '';
const m = n.match(/^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|management-placement) \//);
const m = n.match(/^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|wake-hostport|management-placement) \//);
if (m) bucketSet.add(m[1]);
if (/^e2e multicluster \//.test(n)) bucketSet.add('multicluster');
if (/^e2e upgrade \//.test(n)) bucketSet.add('upgrade');
Expand Down
4 changes: 4 additions & 0 deletions charts/gameplane/templates/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ metadata:
name: {{ .Release.Name }}-operator-watch
labels: {{- include "gameplane.labels" . | nindent 4 }}
rules:
# Direct node identity/readiness checks for durable Hostport wake endpoints.
- apiGroups: [""]
resources: [nodes]
verbs: [get]
- apiGroups: ["gameplane.local"]
resources: [gameservers, gametemplates, backups, backupschedules, restores, modules, modulesources, networkcaptures]
verbs: [get, list, watch]
Expand Down
11 changes: 7 additions & 4 deletions deploy/kind/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
# deploy/kind/e2e.sh down [cluster-name]
#
# Diffs from dev-up (deploy/kind/up.sh):
# - Single-node by default; management profile adds two scheduling workers.
# - Single-node by default; two-node adds one worker for Hostport handoff,
# and management adds two workers for scheduling and disruption checks.
# - Skips ingress-nginx (the dashboard isn't exercised here).
# - Loads pre-built gameplane-test/{operator,api,agent,sentinel,capture-sidecar,telemetry-receiver,fakeoidc}:<tag>
# images (and gameplane-test/gameprobe:<tag> when present).
Expand All @@ -28,9 +29,9 @@ CLUSTER="${2:-gameplane-e2e}"
TAG="${3:-${GAMEPLANE_E2E_TAG:-e2e}}"
PROFILE="${4:-default}"
case "${PROFILE}" in
default | postgres | management) ;;
default | postgres | two-node | management) ;;
*)
echo "unknown profile '${PROFILE}' (want default, postgres or management)" >&2
echo "unknown profile '${PROFILE}' (want default, postgres, two-node or management)" >&2
exit 2
;;
esac
Expand Down Expand Up @@ -190,7 +191,9 @@ up)
fi
echo "creating kind cluster ${CLUSTER} (${PROFILE})"
extra_nodes=""
if [[ "${PROFILE}" == "management" ]]; then
if [[ "${PROFILE}" == "two-node" ]]; then
extra_nodes=" - role: worker"
elif [[ "${PROFILE}" == "management" ]]; then
extra_nodes=$' - role: worker\n - role: worker'
fi
kind create cluster --name "${CLUSTER}" --wait 90s --config - <<EOF
Expand Down
4 changes: 2 additions & 2 deletions hack/ci_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
FORCE_ALL_PREFIXES = (".github/actions/",)
# Mirrors the e2e-go matrix buckets in .github/workflows/ci.yaml.
E2E_BUCKETS = ["operator", "api-auth", "api-roles", "api-rbac", "api-agent", "api-mods",
"telemetry", "optional-components", "management-placement"]
E2E_AMD64_ONLY = {"management-placement"}
"telemetry", "optional-components", "wake-hostport", "management-placement"]
E2E_AMD64_ONLY = {"wake-hostport", "management-placement"}
SUITES_FILE = "test/e2e/suite-components.json"
# Paths no Go e2e test reads. Their own jobs keep their paths-filter gates.
NO_E2E_FILES = {
Expand Down
11 changes: 11 additions & 0 deletions hack/test_ci_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,17 @@ def select(self, *paths):
"""Return the e2e outputs for a partial-scope change to paths."""
return e2e_outputs(ROOT, list(paths), None)

def test_audit_wake_hostport_has_a_run_pattern(self):
"""Affected components and the owning test file select the audit bucket."""
for path in ("operator/internal/controller/gameserver_sentinel.go",
"sentinel/main.go",
"gameproto/minecraft.go",
"test/e2e/hostport_wake_e2e_test.go"):
with self.subTest(path=path):
values = self.select(path)
self.assertIn("TestGameServer_HostportWakeStableEndpoint",
values["e2e-run"].get("wake-hostport", ""))

def test_audit_management_placement_has_a_run_pattern(self):
"""Affected components and the owning test file select the audit bucket."""
for path in ("operator/cmd/main.go",
Expand Down
56 changes: 56 additions & 0 deletions i18n/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,34 @@
"defaultMessage": "The state of the server is not known yet.",
"description": "Healthy condition before the operator has observed the server."
},
"condition.hostportwakeready.endpointnodeunavailable": {
"defaultMessage": "The Hostport endpoint node is missing, replaced or not Ready; restore that node or explicitly migrate the endpoint.",
"description": "Hostport endpoint handoff status: EndpointNodeUnavailable."
},
"condition.hostportwakeready.endpointnodeunknown": {
"defaultMessage": "No game endpoint node has been recorded; start the game before enabling sleep.",
"description": "Hostport endpoint handoff status: EndpointNodeUnknown."
},
"condition.hostportwakeready.endpointunschedulable": {
"defaultMessage": "The Hostport listener cannot schedule on its endpoint node. Inspect the Pod scheduling events for details.",
"description": "Hostport endpoint handoff status: EndpointUnschedulable."
},
"condition.hostportwakeready.gamedraining": {
"defaultMessage": "Waiting for the game Pod to disappear before acquiring its Hostport.",
"description": "Hostport endpoint handoff status: GameDraining."
},
"condition.hostportwakeready.listenerpending": {
"defaultMessage": "Waiting for a Ready listener on the Hostport endpoint node.",
"description": "Hostport endpoint handoff status: ListenerPending."
},
"condition.hostportwakeready.listenerready": {
"defaultMessage": "The listener is Ready on the Hostport endpoint node.",
"description": "Hostport endpoint handoff status: ListenerReady."
},
"condition.hostportwakeready.sentineldraining": {
"defaultMessage": "Waiting for the sentinel Deployment and Pods to disappear before scheduling the game on its endpoint node.",
"description": "Hostport endpoint handoff status: SentinelDraining."
},
"condition.idlescheduleinvalid.wakewindowunparseable": {
"defaultMessage": "A wake window in the idle schedule cannot be parsed: {details}",
"description": "Condition when spec.idle has an invalid wake window. details is the English parse error."
Expand Down Expand Up @@ -423,10 +451,26 @@
"defaultMessage": "The module content does not match its pinned digest.",
"description": "Module condition when the digest differs from the pin."
},
"condition.ready.endpointnodeunavailable": {
"defaultMessage": "The Hostport endpoint node is missing, replaced or not Ready; restore that node or explicitly migrate the endpoint.",
"description": "Hostport endpoint handoff status: EndpointNodeUnavailable."
},
"condition.ready.endpointnodeunknown": {
"defaultMessage": "No game endpoint node has been recorded; start the game before enabling sleep.",
"description": "Hostport endpoint handoff status: EndpointNodeUnknown."
},
"condition.ready.endpointunschedulable": {
"defaultMessage": "The Hostport listener cannot schedule on its endpoint node. Inspect the Pod scheduling events for details.",
"description": "Hostport endpoint handoff status: EndpointUnschedulable."
},
"condition.ready.failed": {
"defaultMessage": "{detail}",
"description": "Ready condition when the server cannot start. detail is the English reason from the operator, for example a missing template or an invalid configuration."
},
"condition.ready.gamedraining": {
"defaultMessage": "Waiting for the game Pod to disappear before acquiring its Hostport.",
"description": "Hostport endpoint handoff status: GameDraining."
},
"condition.ready.idleasleep": {
"defaultMessage": "The server is asleep because no players are online.",
"description": "Ready condition for a server put to sleep by idle auto-sleep."
Expand All @@ -443,6 +487,14 @@
"defaultMessage": "The module cannot be removed while game servers use its template.",
"description": "Ready condition when deletion is blocked."
},
"condition.ready.listenerpending": {
"defaultMessage": "Waiting for a Ready listener on the Hostport endpoint node.",
"description": "Hostport endpoint handoff status: ListenerPending."
},
"condition.ready.listenerready": {
"defaultMessage": "The listener is Ready on the Hostport endpoint node.",
"description": "Hostport endpoint handoff status: ListenerReady."
},
"condition.ready.noversionavailable": {
"defaultMessage": "The module source has no available versions.",
"description": "Module condition when no version can be chosen."
Expand All @@ -463,6 +515,10 @@
"defaultMessage": "The server is running and the agent is reporting heartbeats.",
"description": "Ready condition while the game server is up."
},
"condition.ready.sentineldraining": {
"defaultMessage": "Waiting for the sentinel Deployment and Pods to disappear before scheduling the game on its endpoint node.",
"description": "Hostport endpoint handoff status: SentinelDraining."
},
"condition.ready.servingstalecatalog": {
"defaultMessage": "Serving the cached catalog. The last index attempt failed.",
"description": "Ready condition while an older catalog stays available."
Expand Down
6 changes: 6 additions & 0 deletions operator/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ rules:
- list
- patch
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
Expand Down
11 changes: 11 additions & 0 deletions operator/hack/condition-ids/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type row struct {
// guardCalls maps a helper that takes a condition reason as a string literal
// to the index of that argument.
var guardCalls = map[string]int{
"hostportCondition": 1,
"hostportPending": 0,
"markUnhealthy": 2,
"markFailed": 2,
"markPending": 2,
Expand Down Expand Up @@ -120,6 +122,15 @@ var rows = []row{
{types: []string{"AddressAssignment"}, reason: "AssignmentPending", english: "Requested $REQ: the address manager has not assigned an address yet.", description: "AddressAssignment condition while the address manager works."},
{types: []string{"AddressAssignment"}, reason: "Assigned", english: "{assignedFrom, select, pool {Address {address} assigned from pool ''{pool}''.} other {Address {address} assigned.}}", description: "AddressAssignment condition once an address is assigned. assignedFrom is pool or default."},

// Hostport endpoint handoff; Ready can mirror the same reason.
{types: []string{"HostportWakeReady", "Ready"}, reason: "EndpointNodeUnknown", english: "No game endpoint node has been recorded; start the game before enabling sleep.", description: "Hostport endpoint handoff status: EndpointNodeUnknown."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "EndpointNodeUnavailable", english: "The Hostport endpoint node is missing, replaced or not Ready; restore that node or explicitly migrate the endpoint.", description: "Hostport endpoint handoff status: EndpointNodeUnavailable."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "GameDraining", english: "Waiting for the game Pod to disappear before acquiring its Hostport.", description: "Hostport endpoint handoff status: GameDraining."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "SentinelDraining", english: "Waiting for the sentinel Deployment and Pods to disappear before scheduling the game on its endpoint node.", description: "Hostport endpoint handoff status: SentinelDraining."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "EndpointUnschedulable", english: "The Hostport listener cannot schedule on its endpoint node. Inspect the Pod scheduling events for details.", description: "Hostport endpoint handoff status: EndpointUnschedulable."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "ListenerReady", english: "The listener is Ready on the Hostport endpoint node.", description: "Hostport endpoint handoff status: ListenerReady."},
{types: []string{"HostportWakeReady", "Ready"}, reason: "ListenerPending", english: "Waiting for a Ready listener on the Hostport endpoint node.", description: "Hostport endpoint handoff status: ListenerPending."},

// GameServer: tunnel and other
{types: []string{"TunnelReady"}, reason: "InvalidConfig", english: "{provider, select, none {The tunnel is enabled but the server has no tunnel configuration.} other {The ''{provider}'' tunnel provider is selected but its configuration is missing.}}", description: "TunnelReady condition for an incomplete tunnel setup. provider is the selected provider, or none."},
{types: []string{"TunnelReady"}, reason: "DeploymentNotReady", english: "Waiting for the tunnel deployment to become ready.", description: "TunnelReady condition while the tunnel pod is not ready."},
Expand Down
24 changes: 21 additions & 3 deletions operator/internal/controller/gameserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ func (r *GameServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
logger.Error(err, "compute desired replicas")
return ctrl.Result{}, err
}
if plan.blockGame {
replicas = 0
}
if err := r.reconcileStatefulSet(ctx, &gs, &tmpl, ver, mc, replicas, &wipeState); err != nil {
if apierrors.IsConflict(err) {
return ctrl.Result{Requeue: true}, nil
Expand Down Expand Up @@ -450,7 +453,7 @@ func (r *GameServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
}

requeue, err := r.reconcileStatus(ctx, &gs, idle, idleStatus, idleScheduleErr, tunnelPlan, &tmpl, svcEvents, conflictingServer)
requeue, err := r.reconcileStatus(ctx, &gs, idle, idleStatus, idleScheduleErr, tunnelPlan, &tmpl, svcEvents, conflictingServer, plan.hostportCondition)
if err != nil {
return ctrl.Result{}, err
}
Expand All @@ -460,7 +463,7 @@ func (r *GameServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
// or nothing fires the transition until an unrelated event happens to
// wake the reconciler.
hints := []time.Duration{stopRequeue, idleRequeue}
if plan.wantSentinel {
if sentinelBackstopPending(plan) {
// Bounded backstop for planSentinel's wait on the sentinel or the
// game pod. Both are owned (Owns(&appsv1.Deployment{}),
// Owns(&appsv1.StatefulSet{})) so a readiness transition normally
Expand Down Expand Up @@ -1600,7 +1603,22 @@ func (r *GameServerReconciler) reconcileStatefulSet(
// a removed scheduling pin active on the StatefulSet.
ss.Spec.Template.Spec.NodeSelector = gs.Spec.NodeSelector
ss.Spec.Template.Spec.Tolerations = gs.Spec.Tolerations
ss.Spec.Template.Spec.Affinity = gs.Spec.Affinity
// Do not change a live Pod's template merely because its node was
// recorded while a graceful stop is still saving the game. Apply the
// endpoint pin at zero replicas, then retain it on every future wake.
if actualReplicas == 0 || ss.ResourceVersion == "" || ss.Spec.Template.Annotations[hostportNodeAnnotation] != "" {
ss.Spec.Template.Spec.Affinity = hostportAffinity(gs)
if gs.Spec.Networking.Expose == "Hostport" && gs.Annotations[hostportNodeAnnotation] != "" {
if ss.Spec.Template.Annotations == nil {
ss.Spec.Template.Annotations = map[string]string{}
}
ss.Spec.Template.Annotations[hostportNodeAnnotation] = gs.Annotations[hostportNodeAnnotation]
} else {
delete(ss.Spec.Template.Annotations, hostportNodeAnnotation)
}
} else {
ss.Spec.Template.Spec.Affinity = gs.Spec.Affinity.DeepCopy()
}
// Same unconditional-assign reasoning: removing template.spec.security
// must clear a previously-set pod-level FSGroup, not leave it pinned.
ss.Spec.Template.Spec.SecurityContext = gamePodSecurityContext(tmpl)
Expand Down
Loading
Loading