Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
f209797
fix(ci): install pinned Trivy from GitHub releases
Baylem Nov 5, 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
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
27 changes: 27 additions & 0 deletions .github/actions/install-trivy/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Install pinned Trivy
description: Install the checksum-verified Linux amd64 scanner from its GitHub release.
runs:
using: composite
steps:
- name: Download and verify Trivy
shell: bash
run: |
set -euo pipefail
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
echo "This installer requires a Linux amd64 runner" >&2
exit 1
fi
version=0.74.0
sha256=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
archive="trivy_${version}_Linux-64bit.tar.gz"
download_dir=$(mktemp -d "${RUNNER_TEMP:?}/trivy-download.XXXXXX")
trap 'rm -rf -- "$download_dir"' EXIT
# Bound DNS, connection and HTTP retries. Verify before extraction or execution.
curl --fail --show-error --location --retry 3 --retry-all-errors \
--retry-delay 2 --retry-max-time 180 --connect-timeout 15 --max-time 90 \
--output "$download_dir/$archive" \
"https://github.com/aquasecurity/trivy/releases/download/v${version}/${archive}"
printf '%s %s\n' "$sha256" "$download_dir/$archive" | sha256sum --check --status
tar -xzf "$download_dir/$archive" -C "$download_dir" trivy
sudo install -m 0755 "$download_dir/trivy" /usr/local/bin/trivy
trivy --version
18 changes: 10 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,7 @@ jobs:
path: ~/.cache/trivy
key: trivy-db-${{ steps.week.outputs.week }}
- name: install trivy
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
uses: ./.github/actions/install-trivy
- name: prove the gate rejects a vulnerable image
run: ./hack/check-image-scan_test.sh
- name: scan every e2e image
Expand Down Expand Up @@ -1824,17 +1822,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 +1894,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 @@ -2706,7 +2708,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 @@ -2859,7 +2861,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
5 changes: 2 additions & 3 deletions .github/workflows/publish-edge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ on:
- "**/Dockerfile"
- "**/Dockerfile.*"
- ".github/workflows/publish-edge.yaml"
- ".github/actions/install-trivy/**"
- "hack/ci_scope.py"
- ".dockerignore"
- "signing/cosign.pub"
Expand Down Expand Up @@ -149,9 +150,7 @@ jobs:
# Spec 013 G2: never sign an image that has a fixed HIGH/CRITICAL
# vulnerability. Same flags as the `vuln (images)` CI gate.
- name: install trivy
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
uses: ./.github/actions/install-trivy
- name: scan pushed image
env:
IMAGE: ${{ steps.image.outputs.name }}
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ jobs:
# Spec 013 G2: never sign an image that has a fixed HIGH/CRITICAL
# vulnerability. Same flags as the `vuln (images)` CI gate.
- name: install trivy
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
uses: ./.github/actions/install-trivy
- name: scan pushed image
env:
IMAGE: ${{ steps.image.outputs.name }}
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: "UnsupportedProtocol", english: "Tailscale forwards only advertised TCP and UDP ports. Remove unsupported protocols from the advertised ports.", description: "TunnelReady condition when a Tailscale template advertises a protocol other than TCP or UDP."},
{types: []string{"TunnelReady"}, reason: "InvalidTailscaleTags", english: "Correct the Tailscale tags: use an optional tag: prefix, a letter first, then only letters, digits or hyphens.", description: "TunnelReady condition when Tailscale access-control tags have invalid syntax."},
Expand Down
Loading
Loading