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
18 changes: 18 additions & 0 deletions build/terraform/e2e/gke-standard/module.tf
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,21 @@ module "gke_cluster" {

udpFirewall = false // firewall is created at the project module level
}

resource "google_container_node_pool" "windows_ltsc2022" {
name = "win-ltsc2022"
cluster = module.agones.cluster_name
location = var.location
node_count = 2

node_config {
machine_type = "n2-standard-4"
image_type = "WINDOWS_LTSC_CONTAINERD"
tags = ["game-server"]
}

management {
auto_repair = true
auto_upgrade = false
}
}
21 changes: 19 additions & 2 deletions ci/e2e-test-cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ steps:
dir: build/e2e-image
id: build-e2e
waitFor: ['-']

#
# Run the e2e tests with FeatureGates inverted compared to Stable
#
Expand All @@ -39,7 +38,6 @@ steps:
id: e2e-feature-gates
waitFor:
- build-e2e

#
# Run the e2e tests without FeatureGates
#
Expand All @@ -53,6 +51,25 @@ steps:
- ${_GS_TEST_IMAGE}
id: e2e-stable
waitFor: [e2e-feature-gates]
#
# Run Windows GameServer smoke test
# Runs parallel with e2e-feature-gates, does not add to wall-clock time.
#
- name: e2e-runner
args:
- ${_FEATURE_WITHOUT_GATE}
- ${_CLOUD_PRODUCT}
- ${_TEST_CLUSTER_NAME}
- ${_TEST_CLUSTER_LOCATION}
- ${_REGISTRY}
- ${_GS_TEST_IMAGE}
env:
- 'WINDOWS_GS_IMAGE=${_GS_TEST_IMAGE}-windows_amd64-ltsc2019'
- 'E2E_TEST_RUN=TestWindowsCreateConnect'
id: e2e-windows-smoke
waitFor:
- build-e2e
timeout: 900s
tags:
- e2e-test
- cluster-${_TEST_CLUSTER_NAME}
Expand Down
7 changes: 6 additions & 1 deletion examples/simple-game-server/gameserver-windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ spec:
spec:
containers:
- name: simple-game-server
image: us-docker.pkg.dev/agones-images/examples/simple-game-server:0.42
image: us-docker.pkg.dev/agones-images/examples/simple-game-server:0.42-windows_amd64-ltsc2019
resources:
requests:
memory: 64Mi
Expand All @@ -36,3 +36,8 @@ spec:
# Limit this pod to Windows nodes.
nodeSelector:
kubernetes.io/os: windows
tolerations:
- key: node.kubernetes.io/os
operator: Equal
value: windows
effect: NoSchedule
6 changes: 3 additions & 3 deletions site/content/en/docs/Guides/windows-gameservers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ description: >
Run `GameServers` on Kubernetes nodes with the Windows operating system.
---

{{% alert title="Warning" color="warning" %}}
Running `GameServers` on Windows nodes is currently Alpha, and any feedback
would be appreciated.
{{% alert title="Note" color="info" %}}
Windows GameServer support is in Beta. Only the SDK sidecar (`sdk-server`)
runs on Windows nodes. All Agones control-plane components remain Linux-only.
{{% /alert %}}

## Prerequisites
Expand Down
2 changes: 1 addition & 1 deletion site/content/en/docs/Installation/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The following container operating systems and architectures can be utilised with
| --------- | ------------ | ---------- |
| linux | `amd64` | **Stable** |
| linux | `arm64` | Alpha |
| [windows] | `amd64` | Alpha |
| [windows] | `amd64` | Beta |

For all the platforms in Alpha, we would appreciate testing and bug reports on any issue found.

Expand Down
146 changes: 146 additions & 0 deletions test/e2e/windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright Contributors to Agones a Series of LF Projects, LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build e2e
// +build e2e

package e2e

import (
"context"
"testing"

agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// windowsGameServerImage is the windows/amd64 build of simple-game-server.
// Built from examples/simple-game-server/Dockerfile.windows with ltsc2019 base.
const windowsGameServerImage = "us-docker.pkg.dev/agones-images/examples/simple-game-server:0.42-windows_amd64-ltsc2019"

// windowsGameServerFixture returns a GameServer spec targeting the Windows node pool.
//
// Both nodeSelector AND toleration are required on GKE:
// - nodeSelector: schedules only to Windows nodes
// - toleration: GKE auto-applies node.kubernetes.io/os=windows:NoSchedule to
// all Windows nodes; without this toleration the pod stays Pending forever.
func windowsGameServerFixture() *agonesv1.GameServer {
return &agonesv1.GameServer{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "simple-game-server-windows-",
Namespace: framework.Namespace, // package-level var from main_test.go
},
Spec: agonesv1.GameServerSpec{
Ports: []agonesv1.GameServerPort{
{
Name: "default",
PortPolicy: agonesv1.Dynamic,
ContainerPort: 7654,
Protocol: corev1.ProtocolUDP,
},
},
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "simple-game-server",
Image: windowsGameServerImage,
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("64Mi"),
corev1.ResourceCPU: resource.MustParse("20m"),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("64Mi"),
corev1.ResourceCPU: resource.MustParse("20m"),
},
},
},
},
NodeSelector: map[string]string{
"kubernetes.io/os": "windows",
},
Tolerations: []corev1.Toleration{
{
Key: "node.kubernetes.io/os",
Operator: corev1.TolerationOpEqual,
Value: "windows",
Effect: corev1.TaintEffectNoSchedule,
},
},
},
},
},
}
}

// TestWindowsCreateConnect is the Beta smoke test for Windows GameServer support.
// Validates:
// 1. GameServer with Windows image + nodeSelector + toleration reaches Ready.
// 2. Pod is scheduled on a Windows node (not Linux).
// 3. sdk-server sidecar (windows/amd64) initialises and signals Ready.
// 4. UDP connectivity to the host port works from outside the cluster.
func TestWindowsCreateConnect(t *testing.T) {
t.Parallel()

gs := windowsGameServerFixture()

// framework.WaitForState is the cluster-default timeout (set in main_test.go).
// Windows image pulls on a cold node can take 3-8 min, so we use 10 minutes.
// The framework variable is the *framework.Framework from main_test.go — no import needed.
readyGs, err := framework.CreateGameServerAndWaitUntilReady(t, framework.Namespace, gs)
require.NoError(t, err, "GameServer must reach Ready state — check Windows node pool exists and image is pushed")

defer framework.AgonesClient.AgonesV1().
GameServers(readyGs.Namespace).
Delete(context.Background(), readyGs.Name, metav1.DeleteOptions{}) // nolint:errcheck

t.Logf("GameServer %s is Ready: address=%s port=%d",
readyGs.Name, readyGs.Status.Address, readyGs.Status.Ports[0].Port)

// Assert the pod landed on a Windows node.
pod, err := framework.KubeClient.CoreV1().
Pods(readyGs.Namespace).
Get(context.Background(), readyGs.Name, metav1.GetOptions{})
require.NoError(t, err)

node, err := framework.KubeClient.CoreV1().
Nodes().
Get(context.Background(), pod.Spec.NodeName, metav1.GetOptions{})
require.NoError(t, err)

nodeOS := node.Labels["kubernetes.io/os"]
assert.Equal(t, "windows", nodeOS,
"Pod must run on a Windows node, got os=%s on node %s", nodeOS, pod.Spec.NodeName)
t.Logf("Pod %s is running on node %s (os=%s)", pod.Name, pod.Spec.NodeName, nodeOS)

// Assert address and port are populated.
require.NotEmpty(t, readyGs.Status.Address, "GameServer must have an external address")
require.NotEmpty(t, readyGs.Status.Ports, "GameServer must have at least one port")

// Send a UDP message using the framework helper.
// SendGameServerUDP retries 5 times and times out after 10 seconds.
// simple-game-server echoes back "ACK: <message>".
reply, err := framework.SendGameServerUDP(t, readyGs, "PING")
require.NoError(t, err, "UDP send/receive must succeed against Windows GameServer at %s:%d",
readyGs.Status.Address, readyGs.Status.Ports[0].Port)

assert.Contains(t, reply, "ACK",
"simple-game-server must echo back ACK, got: %q", reply)
t.Logf("UDP response from Windows GameServer: %q", reply)
}
Loading