Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3f78ea9
fix(wake): bound requests and preserve HostPort endpoint handoffs
Baylem Oct 21, 2026
875b28c
fix(operator): clarify Hostport waits and localize their conditions
Baylem Oct 21, 2026
54ed9fb
merge: include shared web build prerequisite
Baylem Oct 21, 2026
640fbf3
ci: require and report every audit E2E bucket
Baylem Oct 21, 2026
b31a26b
test(sentinel): bind wake-budget network fixtures to context
Baylem Oct 21, 2026
ac16c1d
fix(ci): preserve audit coverage with upstream selection
Baylem Oct 23, 2026
5978bd1
test(ci): cover audit buckets in full selection fixtures
Baylem Oct 23, 2026
6a03b3f
test: synchronize closed RCON response fixtures
Baylem Oct 23, 2026
b4304e7
Merge branch 'master' into fix/audit-wake-reliability
ValgulNecron Oct 25, 2026
92b7ab0
fix: preserve HostPort status without healthy polling
Baylem Oct 25, 2026
db92821
Merge branch 'master' into fix/audit-wake-reliability
ValgulNecron Oct 26, 2026
644edc8
fix: clarify pending Hostport status and isolate CI fixtures
Baylem Oct 27, 2026
6137c24
chore: reconcile Hostport handoffs with upstream placement
Baylem Oct 27, 2026
b0de46f
merge: synchronize wake handoffs with upstream master
Baylem Nov 4, 2026
60ec371
fix(operator): fence Hostport workloads after node replacement
Baylem Nov 4, 2026
5ced4f9
fix(operator): fence stale Hostport sentinel mutations
Baylem Nov 5, 2026
802e1e8
merge: share pinned Trivy installation with wake reliability
Baylem Nov 5, 2026
25bea13
fix(operator): preserve sleeping Hostport listeners while nodes recover
Baylem Nov 5, 2026
018d6a8
fix(operator): lowercase sentinel endpoint error
Baylem Nov 6, 2026
e5c1f09
Merge shared compress advisory fix into wake reliability
Baylem Nov 6, 2026
1a168b3
Merge shared workspace checksum into wake reliability
Baylem Nov 6, 2026
22762f5
fix(wake): retry shared wake after caller cancellation
Baylem Nov 6, 2026
d86b7c7
fix(hostport): fence replacements before graceful endpoint shutdown
Baylem Nov 6, 2026
3d86dd9
fix(deps): carry x/net image security update
Baylem Nov 6, 2026
50c40c7
fix(deps): carry normalized source module checksums
Baylem Nov 6, 2026
424ddad
fix(deps): carry workspace module graph checksums
Baylem Nov 6, 2026
c986a69
fix(hostport): distinguish missing resources from invalid stop fences
Baylem Nov 6, 2026
9acf960
fix(hostport): recover invalid stop fences without blocking power intent
Baylem Nov 6, 2026
a8aa9d8
fix(hostport): retire orphaned stop scheduling gates
Baylem Nov 7, 2026
f5e659e
merge: integrate current master into HostPort wake fixes
Baylem Nov 7, 2026
2788629
merge: integrate monitoring and isolation into wake stack
Baylem Nov 7, 2026
ea13283
fix(operator): retain lifecycle ownership on HostPort stops
Baylem Nov 7, 2026
f3bcb97
test(operator): initialize lifecycle rollout fixture correctly
Baylem Nov 7, 2026
cf3e0af
Merge branch 'master' into stack/pr-761
Baylem Nov 8, 2026
4d1f433
merge: integrate current upstream into wake stack
Baylem Nov 9, 2026
c51bd33
fix(operator): reject stale HostPort rendering inputs
Baylem Nov 9, 2026
0d2908e
Merge branch 'master' into stack/pr-761
Baylem Nov 9, 2026
4693c9e
fix(ci): report only executed HostPort drills
Baylem Nov 9, 2026
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
31 changes: 18 additions & 13 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1835,9 +1835,9 @@ 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/settings except for the
# network-isolation, management-placement, backup-preparation,
# monitoring, and recovery drills, which run once on amd64.
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components, restricted-workloads, network-isolation, management-placement, backup-preparation, monitoring, recovery]
# wake-hostport, network-isolation, management-placement,
# backup-preparation, monitoring, and recovery drills, which run once on amd64.
bucket: [operator, api-auth, api-roles, api-rbac, api-agent, api-mods, telemetry, optional-components, restricted-workloads, wake-hostport, network-isolation, management-placement, backup-preparation, monitoring, recovery]
arch: [amd64]
include:
- bucket: monitoring
Expand All @@ -1852,10 +1852,14 @@ jobs:
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: backup-preparation
parallel: 1
test_timeout: 15m
Expand Down Expand Up @@ -1926,7 +1930,7 @@ jobs:
if: env.RUN_REGEX != '' || env.TAIL_REGEX != ''
env:
GAMEPLANE_E2E_CNI: ${{ (matrix.bucket == 'network-isolation' || matrix.bucket == 'monitoring') && 'cilium' || 'kindnet' }}
GAMEPLANE_E2E_PROFILE: ${{ (matrix.bucket == 'management-placement' || matrix.bucket == 'backup-preparation') && 'management' || 'default' }}
GAMEPLANE_E2E_PROFILE: ${{ matrix.bucket == 'wake-hostport' && 'two-node' || (matrix.bucket == 'management-placement' || matrix.bucket == 'backup-preparation') && 'management' || 'default' }}
run: ./deploy/kind/e2e.sh up "$CLUSTER" e2e "$GAMEPLANE_E2E_PROFILE"
- name: install real monitoring fixture
if: matrix.bucket == 'monitoring' && env.RUN_REGEX != ''
Expand Down Expand Up @@ -2767,7 +2771,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|restricted-workloads|network-isolation|management-placement|backup-preparation|monitoring|recovery) \//.test(n) && n.includes('/ amd64 '),
'e2e-go': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|restricted-workloads|wake-hostport|network-isolation|management-placement|backup-preparation|monitoring|recovery) \//.test(n) && n.includes('/ amd64 '),
'e2e-go-arm64': (n) => /^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|restricted-workloads) \//.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 @@ -2920,13 +2924,14 @@ 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|restricted-workloads|network-isolation|management-placement|backup-preparation|monitoring|recovery) \//);
if (m && m[1] !== 'recovery') bucketSet.add(m[1]);
// Partial selection can complete the recovery leg without
// running its drill. Count completed execution, including failure.
if (m && m[1] === 'recovery' && Array.isArray(j.steps)) {
const recovery = j.steps.find((s) => s.name === 'run go e2e (recovery)');
if (recovery && ['success', 'failure'].includes(recovery.conclusion)) bucketSet.add('recovery');
const m = n.match(/^e2e (operator|api-auth|api-roles|api-rbac|api-agent|api-mods|telemetry|optional-components|restricted-workloads|wake-hostport|network-isolation|management-placement|backup-preparation|monitoring|recovery) \//);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const executionGated = m && ['wake-hostport', 'recovery'].includes(m[1]);
if (m && !executionGated) bucketSet.add(m[1]);
// Partial selection can complete these legs without running
// their drills. Count completed execution, including failure.
if (executionGated && Array.isArray(j.steps)) {
const drill = j.steps.find((s) => s.name === `run go e2e (${m[1]})`);
if (drill && ['success', 'failure'].includes(drill.conclusion)) 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 @@ -33,9 +34,9 @@ kindnet | cilium) ;;
*) echo "unsupported E2E CNI: ${CNI}" >&2; exit 2 ;;
esac
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 @@ -201,7 +202,9 @@ up)
kind_wait=0s # Nodes become Ready after Cilium is installed below.
fi
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 "${kind_wait}" --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", "restricted-workloads", "network-isolation", "management-placement", "backup-preparation", "monitoring", "recovery"]
E2E_AMD64_ONLY = {"network-isolation", "management-placement", "backup-preparation", "monitoring", "recovery"}
"telemetry", "optional-components", "restricted-workloads", "wake-hostport", "network-isolation", "management-placement", "backup-preparation", "monitoring", "recovery"]
E2E_AMD64_ONLY = {"wake-hostport", "network-isolation", "management-placement", "backup-preparation", "monitoring", "recovery"}
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
48 changes: 47 additions & 1 deletion hack/test-check-ci-report-coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#
# Runs pass.yaml (needs: fully covered by NEEDS_ORDER + JOB_MATCHERS) and the
# fail-*.yaml fixtures (a needs: job missing from one or both structures)
# through the checker, and against the real .github/workflows/ci.yaml.
# through the checker, and against the real .github/workflows/ci.yaml. Also
# exercises that workflow's actual executed-bucket collector with synthetic jobs.

set -euo pipefail

Expand Down Expand Up @@ -34,6 +35,51 @@ check "fixture missing from NEEDS_ORDER and JOB_MATCHERS rejected" "$FIX/fail-mi
check "fixture missing only from JOB_MATCHERS rejected" "$FIX/fail-missing-matcher.yaml" 1
check "real ci.yaml is currently covered" ".github/workflows/ci.yaml" 0

if node <<'NODE'
const assert = require('node:assert/strict');
const fs = require('node:fs');
const workflow = fs.readFileSync('.github/workflows/ci.yaml', 'utf8');
const startMarker = 'const bucketSet = new Set();';
const endMarker = 'const buckets = [...bucketSet].sort();';
const start = workflow.indexOf(startMarker);
const end = workflow.indexOf(endMarker, start);
assert.ok(start !== -1 && end > start, 'workflow bucket collector must exist');
const collect = new Function('apiJobs', 'needs',
workflow.slice(start, end + endMarker.length) + '\nreturn buckets;');
const job = (bucket, steps) => ({ name: `e2e ${bucket} / amd64 (parallel 1)`, steps });
for (const bucket of ['wake-hostport', 'recovery']) {
const stepName = `run go e2e (${bucket})`;
const cases = [
['success', [{ name: stepName, conclusion: 'success' }], [bucket]],
['failure', [{ name: stepName, conclusion: 'failure' }], [bucket]],
['skipped', [{ name: stepName, conclusion: 'skipped' }], []],
['cancelled', [{ name: stepName, conclusion: 'cancelled' }], []],
['missing steps', undefined, []],
['missing step', [], []],
['wrong step', [{ name: 'run go e2e (operator)', conclusion: 'success' }], []],
];
for (const [label, steps, expected] of cases) {
assert.deepEqual(collect([job(bucket, steps)], {}), expected, `${bucket}: ${label}`);
}
}
assert.deepEqual(collect([
job('wake-hostport', [{ name: 'run go e2e (wake-hostport)', conclusion: 'success' }]),
job('wake-hostport', [{ name: 'run go e2e (wake-hostport)', conclusion: 'failure' }]),
job('recovery', [{ name: 'run go e2e (recovery)', conclusion: 'success' }]),
job('recovery', [{ name: 'run go e2e (recovery)', conclusion: 'failure' }]),
job('operator'),
job('api-auth', [{ name: 'run go e2e (api-auth)', conclusion: 'skipped' }]),
], {}), ['api-auth', 'operator', 'recovery', 'wake-hostport'],
'collector deduplicates completed drills and preserves ordinary bucket reporting');
NODE
then
echo "✓ executed-bucket collector reports completed drills only"
passed=$((passed + 1))
else
echo "✗ executed-bucket collector reported unexpected buckets"
failed=$((failed + 1))
fi

total=$((passed + failed))
if [[ $failed -eq 0 ]]; then
echo "✓ $total fixtures behaved as expected"
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_recovery_inputs_select_the_recovery_drill(self):
"""The recovery fixture and the services it exercises retain CI coverage."""
for path in ("operator/internal/controller/gameserver_controller.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 @@ -315,6 +315,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 @@ -475,10 +503,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 @@ -495,6 +539,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 @@ -515,6 +567,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
Loading
Loading