agones.dev/agones@v1.54.0/pkg/cloudproduct/gke/gke_test.go (about)

     1  // Copyright 2022 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package gke
    15  
    16  import (
    17  	"fmt"
    18  	"testing"
    19  
    20  	"agones.dev/agones/pkg/apis"
    21  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    22  	"agones.dev/agones/pkg/util/runtime"
    23  	"github.com/stretchr/testify/assert"
    24  	"github.com/stretchr/testify/require"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/validation/field"
    28  )
    29  
    30  func TestSyncPodPortsToGameServer(t *testing.T) {
    31  	assignmentAnnotation := map[string]string{hostPortAssignmentAnnotation: `{"min":7000,"max":8000,"portsAssigned":{"7001":7737,"7002":7738}}`}
    32  	badAnnotation := map[string]string{hostPortAssignmentAnnotation: `good luck parsing this as JSON`}
    33  	for name, tc := range map[string]struct {
    34  		gs      *agonesv1.GameServer
    35  		pod     *corev1.Pod
    36  		wantGS  *agonesv1.GameServer
    37  		wantErr bool
    38  	}{
    39  		"no ports => no change": {
    40  			gs:     &agonesv1.GameServer{},
    41  			pod:    testPod(nil),
    42  			wantGS: &agonesv1.GameServer{},
    43  		},
    44  		"no annotation => no change": {
    45  			gs:     testGameServer([]int32{7777}, nil),
    46  			pod:    testPod(nil),
    47  			wantGS: testGameServer([]int32{7777}, nil),
    48  		},
    49  		"annotation => ports mapped": {
    50  			gs:     testGameServer([]int32{7002, 7001, 7002}, nil),
    51  			pod:    testPod(assignmentAnnotation),
    52  			wantGS: testGameServer([]int32{7738, 7737, 7738}, nil),
    53  		},
    54  		"annotation, but ports already assigned => ports mapped": {
    55  			gs:     testGameServer([]int32{7001, 7002}, []int32{7001, 7002}),
    56  			pod:    testPod(assignmentAnnotation),
    57  			wantGS: testGameServer([]int32{7001, 7002}, []int32{7001, 7002}),
    58  		},
    59  		"bad annotation": {
    60  			gs:      testGameServer([]int32{7002, 7001, 7002}, nil),
    61  			pod:     testPod(badAnnotation),
    62  			wantErr: true,
    63  		},
    64  	} {
    65  		t.Run(name, func(t *testing.T) {
    66  			oldPod := tc.pod.DeepCopy()
    67  			err := (&gkeAutopilot{}).SyncPodPortsToGameServer(tc.gs, tc.pod)
    68  			if tc.wantErr {
    69  				assert.NotNil(t, err)
    70  				return
    71  			}
    72  			if assert.NoError(t, err) {
    73  				require.Equal(t, tc.wantGS, tc.gs)
    74  				require.Equal(t, oldPod, tc.pod)
    75  			}
    76  		})
    77  	}
    78  }
    79  
    80  func TestValidateGameServer(t *testing.T) {
    81  	for name, tc := range map[string]struct {
    82  		edPods             bool
    83  		ports              []agonesv1.GameServerPort
    84  		scheduling         apis.SchedulingStrategy
    85  		safeToEvict        agonesv1.EvictionSafe
    86  		want               field.ErrorList
    87  		portPolicyNoneFlag string
    88  	}{
    89  		"no ports => validated": {scheduling: apis.Packed},
    90  		"good ports => validated": {
    91  			ports: []agonesv1.GameServerPort{
    92  				{
    93  					Name:          "some-tcpudp",
    94  					PortPolicy:    agonesv1.Dynamic,
    95  					Range:         agonesv1.DefaultPortRange,
    96  					ContainerPort: 4321,
    97  					Protocol:      agonesv1.ProtocolTCPUDP,
    98  				},
    99  				{
   100  					Name:          "awesome-udp",
   101  					PortPolicy:    agonesv1.Dynamic,
   102  					Range:         agonesv1.DefaultPortRange,
   103  					ContainerPort: 1234,
   104  					Protocol:      corev1.ProtocolUDP,
   105  				},
   106  				{
   107  					Name:          "awesome-tcp",
   108  					PortPolicy:    agonesv1.Dynamic,
   109  					Range:         agonesv1.DefaultPortRange,
   110  					ContainerPort: 1234,
   111  					Protocol:      corev1.ProtocolTCP,
   112  				},
   113  				{
   114  					Name:          "none-udp",
   115  					PortPolicy:    agonesv1.None,
   116  					ContainerPort: 1234,
   117  					Protocol:      corev1.ProtocolUDP,
   118  				},
   119  				{
   120  					Name:          "passthrough-udp",
   121  					PortPolicy:    agonesv1.Passthrough,
   122  					Range:         agonesv1.DefaultPortRange,
   123  					ContainerPort: 1234,
   124  					Protocol:      corev1.ProtocolUDP,
   125  				},
   126  				{
   127  					Name:          "passthrough-tcp",
   128  					PortPolicy:    agonesv1.Passthrough,
   129  					Range:         agonesv1.DefaultPortRange,
   130  					ContainerPort: 1234,
   131  					Protocol:      corev1.ProtocolTCP,
   132  				},
   133  			},
   134  			safeToEvict: agonesv1.EvictionSafeAlways,
   135  			scheduling:  apis.Packed,
   136  		},
   137  		"bad port range => fails validation": {
   138  			ports: []agonesv1.GameServerPort{
   139  				{
   140  					Name:          "best-tcpudp",
   141  					PortPolicy:    agonesv1.Dynamic,
   142  					Range:         agonesv1.DefaultPortRange,
   143  					ContainerPort: 4321,
   144  					Protocol:      agonesv1.ProtocolTCPUDP,
   145  				},
   146  				{
   147  					Name:          "bad-range",
   148  					PortPolicy:    agonesv1.Dynamic,
   149  					Range:         "game",
   150  					ContainerPort: 1234,
   151  					Protocol:      corev1.ProtocolUDP,
   152  				},
   153  				{
   154  					Name:          "another-bad-range",
   155  					PortPolicy:    agonesv1.Dynamic,
   156  					Range:         "game",
   157  					ContainerPort: 1234,
   158  					Protocol:      corev1.ProtocolUDP,
   159  				},
   160  				{
   161  					Name:          "passthrough-udp-bad-range",
   162  					PortPolicy:    agonesv1.Passthrough,
   163  					Range:         "passthrough",
   164  					ContainerPort: 1234,
   165  					Protocol:      corev1.ProtocolUDP,
   166  				},
   167  				{
   168  					Name:          "passthrough-tcp-bad-range",
   169  					PortPolicy:    agonesv1.Passthrough,
   170  					Range:         "games",
   171  					ContainerPort: 1234,
   172  					Protocol:      corev1.ProtocolTCP,
   173  				},
   174  			},
   175  			safeToEvict: agonesv1.EvictionSafeAlways,
   176  			scheduling:  apis.Packed,
   177  			want: field.ErrorList{
   178  				field.Invalid(field.NewPath("spec", "ports").Index(1).Child("range"), "game", "range must not be used on GKE Autopilot"),
   179  				field.Invalid(field.NewPath("spec", "ports").Index(2).Child("range"), "game", "range must not be used on GKE Autopilot"),
   180  				field.Invalid(field.NewPath("spec", "ports").Index(3).Child("range"), "passthrough", "range must not be used on GKE Autopilot"),
   181  				field.Invalid(field.NewPath("spec", "ports").Index(4).Child("range"), "games", "range must not be used on GKE Autopilot"),
   182  			},
   183  		},
   184  		"bad policy (no feature gates) => fails validation": {
   185  			ports: []agonesv1.GameServerPort{
   186  				{
   187  					Name:          "best-tcpudp",
   188  					PortPolicy:    agonesv1.Dynamic,
   189  					Range:         agonesv1.DefaultPortRange,
   190  					ContainerPort: 4321,
   191  					Protocol:      agonesv1.ProtocolTCPUDP,
   192  				},
   193  				{
   194  					Name:          "bad-udp",
   195  					PortPolicy:    agonesv1.Static,
   196  					Range:         agonesv1.DefaultPortRange,
   197  					ContainerPort: 1234,
   198  					Protocol:      corev1.ProtocolUDP,
   199  				},
   200  				{
   201  					Name:          "another-bad-udp",
   202  					PortPolicy:    agonesv1.Static,
   203  					Range:         agonesv1.DefaultPortRange,
   204  					ContainerPort: 1234,
   205  					Protocol:      corev1.ProtocolUDP,
   206  				},
   207  			},
   208  			safeToEvict: agonesv1.EvictionSafeOnUpgrade,
   209  			scheduling:  apis.Distributed,
   210  			want: field.ErrorList{
   211  				field.Invalid(field.NewPath("spec", "scheduling"), "Distributed", "scheduling strategy must be Packed on GKE Autopilot"),
   212  				field.Invalid(field.NewPath("spec", "ports").Index(1).Child("portPolicy"), agonesv1.Static, "portPolicy must be Dynamic, Passthrough, or None on GKE Autopilot"),
   213  				field.Invalid(field.NewPath("spec", "ports").Index(2).Child("portPolicy"), agonesv1.Static, "portPolicy must be Dynamic, Passthrough, or None on GKE Autopilot"),
   214  				field.Invalid(field.NewPath("spec", "eviction", "safe"), "OnUpgrade", "eviction.safe OnUpgrade not supported on GKE Autopilot"),
   215  			},
   216  		},
   217  		"bad policy (GKEAutopilotExtendedDurationPods enabled) => fails validation but OnUpgrade works": {
   218  			edPods: true,
   219  			ports: []agonesv1.GameServerPort{
   220  				{
   221  					Name:          "best-tcpudp",
   222  					PortPolicy:    agonesv1.Dynamic,
   223  					Range:         agonesv1.DefaultPortRange,
   224  					ContainerPort: 4321,
   225  					Protocol:      agonesv1.ProtocolTCPUDP,
   226  				},
   227  				{
   228  					Name:          "bad-udp",
   229  					PortPolicy:    agonesv1.Static,
   230  					Range:         agonesv1.DefaultPortRange,
   231  					ContainerPort: 1234,
   232  					Protocol:      corev1.ProtocolUDP,
   233  				},
   234  				{
   235  					Name:          "another-bad-udp",
   236  					PortPolicy:    agonesv1.Static,
   237  					Range:         agonesv1.DefaultPortRange,
   238  					ContainerPort: 1234,
   239  					Protocol:      corev1.ProtocolUDP,
   240  				},
   241  				{
   242  					Name:          "passthrough-udp",
   243  					PortPolicy:    agonesv1.Passthrough,
   244  					Range:         agonesv1.DefaultPortRange,
   245  					ContainerPort: 1234,
   246  					Protocol:      corev1.ProtocolUDP,
   247  				},
   248  			},
   249  			safeToEvict: agonesv1.EvictionSafeOnUpgrade,
   250  			scheduling:  apis.Distributed,
   251  			want: field.ErrorList{
   252  				field.Invalid(field.NewPath("spec", "scheduling"), "Distributed", "scheduling strategy must be Packed on GKE Autopilot"),
   253  				field.Invalid(field.NewPath("spec", "ports").Index(1).Child("portPolicy"), agonesv1.Static, "portPolicy must be Dynamic, Passthrough, or None on GKE Autopilot"),
   254  				field.Invalid(field.NewPath("spec", "ports").Index(2).Child("portPolicy"), agonesv1.Static, "portPolicy must be Dynamic, Passthrough, or None on GKE Autopilot"),
   255  			},
   256  		},
   257  		"port policy none with feature disabled => fails validation": {
   258  			portPolicyNoneFlag: "false",
   259  			ports: []agonesv1.GameServerPort{
   260  				{
   261  					Name:          "none-gate-turned-off",
   262  					PortPolicy:    agonesv1.None,
   263  					Range:         agonesv1.DefaultPortRange,
   264  					ContainerPort: 1234,
   265  					Protocol:      corev1.ProtocolUDP,
   266  				},
   267  			},
   268  			scheduling: apis.Packed,
   269  			want: field.ErrorList{
   270  				field.Invalid(field.NewPath("spec", "ports").Index(0).Child("portPolicy"), agonesv1.None, "PortPolicy 'None' is not enabled"),
   271  			},
   272  		},
   273  	} {
   274  		t.Run(name, func(t *testing.T) {
   275  			// PortPolicy None is behind a feature flag
   276  			runtime.FeatureTestMutex.Lock()
   277  			defer runtime.FeatureTestMutex.Unlock()
   278  			if tc.portPolicyNoneFlag == "" {
   279  				tc.portPolicyNoneFlag = "true"
   280  			}
   281  			require.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=%s", runtime.FeaturePortPolicyNone, tc.portPolicyNoneFlag)))
   282  			causes := (&gkeAutopilot{useExtendedDurationPods: tc.edPods}).ValidateGameServerSpec(&agonesv1.GameServerSpec{
   283  				Ports:      tc.ports,
   284  				Scheduling: tc.scheduling,
   285  				Eviction:   &agonesv1.Eviction{Safe: tc.safeToEvict},
   286  			}, field.NewPath("spec"))
   287  			require.Equal(t, tc.want, causes)
   288  		})
   289  	}
   290  }
   291  
   292  func TestPodSeccompUnconfined(t *testing.T) {
   293  	for name, tc := range map[string]struct {
   294  		podSpec     *corev1.PodSpec
   295  		wantPodSpec *corev1.PodSpec
   296  	}{
   297  		"no context defined": {
   298  			podSpec: &corev1.PodSpec{},
   299  			wantPodSpec: &corev1.PodSpec{
   300  				SecurityContext: &corev1.PodSecurityContext{
   301  					SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined},
   302  				},
   303  			},
   304  		},
   305  		"security context set, no seccomp set": {
   306  			podSpec: &corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{}},
   307  			wantPodSpec: &corev1.PodSpec{
   308  				SecurityContext: &corev1.PodSecurityContext{
   309  					SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined},
   310  				},
   311  			},
   312  		},
   313  		"seccomp already set": {
   314  			podSpec: &corev1.PodSpec{
   315  				SecurityContext: &corev1.PodSecurityContext{
   316  					SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault},
   317  				},
   318  			},
   319  			wantPodSpec: &corev1.PodSpec{
   320  				SecurityContext: &corev1.PodSecurityContext{
   321  					SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault},
   322  				},
   323  			},
   324  		},
   325  	} {
   326  		t.Run(name, func(t *testing.T) {
   327  			podSpec := tc.podSpec.DeepCopy()
   328  			podSpecSeccompUnconfined(podSpec)
   329  			assert.Equal(t, tc.wantPodSpec, podSpec)
   330  		})
   331  	}
   332  }
   333  
   334  func TestSetPassthroughLabel(t *testing.T) {
   335  	for name, tc := range map[string]struct {
   336  		pod      *corev1.Pod
   337  		wantPod  *corev1.Pod
   338  		ports    []agonesv1.GameServerPort
   339  		features string
   340  	}{
   341  		"gameserver with with Passthrough port policy adds label to pod": {
   342  			features: "",
   343  
   344  			pod: &corev1.Pod{
   345  				ObjectMeta: metav1.ObjectMeta{
   346  					Annotations: map[string]string{},
   347  					Labels:      map[string]string{},
   348  				},
   349  			},
   350  			ports: []agonesv1.GameServerPort{
   351  				{
   352  					Name:          "awesome-udp",
   353  					PortPolicy:    agonesv1.Passthrough,
   354  					ContainerPort: 1234,
   355  					Protocol:      corev1.ProtocolUDP,
   356  				},
   357  			},
   358  			wantPod: &corev1.Pod{
   359  				ObjectMeta: metav1.ObjectMeta{
   360  					Annotations: map[string]string{},
   361  					Labels: map[string]string{
   362  						agonesv1.GameServerPortPolicyPodLabel: "autopilot-passthrough",
   363  					},
   364  				},
   365  			},
   366  		},
   367  		"gameserver with  Static port policy does not add label to pod": {
   368  			features: "",
   369  
   370  			pod: &corev1.Pod{
   371  				ObjectMeta: metav1.ObjectMeta{
   372  					Annotations: map[string]string{},
   373  					Labels:      map[string]string{},
   374  				},
   375  			},
   376  			ports: []agonesv1.GameServerPort{
   377  				{
   378  					Name:          "awesome-udp",
   379  					PortPolicy:    agonesv1.Static,
   380  					ContainerPort: 1234,
   381  					Protocol:      corev1.ProtocolUDP,
   382  				},
   383  			},
   384  			wantPod: &corev1.Pod{
   385  				ObjectMeta: metav1.ObjectMeta{
   386  					Annotations: map[string]string{},
   387  					Labels:      map[string]string{},
   388  				},
   389  			},
   390  		},
   391  	} {
   392  		t.Run(name, func(t *testing.T) {
   393  			runtime.FeatureTestMutex.Lock()
   394  			defer runtime.FeatureTestMutex.Unlock()
   395  			require.NoError(t, runtime.ParseFeatures(tc.features))
   396  			gs := (&autopilotPortAllocator{minPort: 7000, maxPort: 8000}).Allocate(&agonesv1.GameServer{Spec: agonesv1.GameServerSpec{Ports: tc.ports}})
   397  			pod := tc.pod.DeepCopy()
   398  			setPassthroughLabel(&gs.Spec, pod)
   399  			assert.Equal(t, tc.wantPod, pod)
   400  		})
   401  	}
   402  }
   403  
   404  func TestSetEvictionNoExtended(t *testing.T) {
   405  	emptyPodAnd := func(f func(*corev1.Pod)) *corev1.Pod {
   406  		pod := &corev1.Pod{
   407  			ObjectMeta: metav1.ObjectMeta{
   408  				Annotations: map[string]string{},
   409  				Labels:      map[string]string{},
   410  			},
   411  		}
   412  		f(pod)
   413  		return pod
   414  	}
   415  	for desc, tc := range map[string]struct {
   416  		eviction *agonesv1.Eviction
   417  		pod      *corev1.Pod
   418  		wantPod  *corev1.Pod
   419  		wantErr  bool
   420  	}{
   421  		"eviction: safe: Always, no incoming labels/annotations": {
   422  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeAlways},
   423  			pod:      emptyPodAnd(func(*corev1.Pod) {}),
   424  			wantPod: emptyPodAnd(func(pod *corev1.Pod) {
   425  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = agonesv1.True
   426  			}),
   427  		},
   428  		"eviction: safe: Never, no incoming labels/annotations": {
   429  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeNever},
   430  			pod:      emptyPodAnd(func(*corev1.Pod) {}),
   431  			wantPod: emptyPodAnd(func(pod *corev1.Pod) {
   432  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = agonesv1.False
   433  			}),
   434  		},
   435  		"eviction: safe: OnUpgrade => error": {
   436  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeOnUpgrade},
   437  			pod:      emptyPodAnd(func(*corev1.Pod) {}),
   438  			wantErr:  true,
   439  		},
   440  		"eviction: safe: Always, incoming labels/annotations": {
   441  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeAlways},
   442  			pod: emptyPodAnd(func(pod *corev1.Pod) {
   443  				pod.ObjectMeta.Annotations[agonesv1.PodSafeToEvictAnnotation] = "just don't touch, ok?"
   444  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = "seriously, leave it"
   445  			}),
   446  			wantPod: emptyPodAnd(func(pod *corev1.Pod) {
   447  				pod.ObjectMeta.Annotations[agonesv1.PodSafeToEvictAnnotation] = "just don't touch, ok?"
   448  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = "seriously, leave it"
   449  			}),
   450  		},
   451  		"eviction: safe: Never, incoming labels/annotations": {
   452  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeNever},
   453  			pod: emptyPodAnd(func(pod *corev1.Pod) {
   454  				pod.ObjectMeta.Annotations[agonesv1.PodSafeToEvictAnnotation] = "a passthrough"
   455  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = "or is it passthru?"
   456  			}),
   457  			wantPod: emptyPodAnd(func(pod *corev1.Pod) {
   458  				pod.ObjectMeta.Annotations[agonesv1.PodSafeToEvictAnnotation] = "a passthrough"
   459  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = "or is it passthru?"
   460  			}),
   461  		},
   462  		"eviction: safe: Never, but safe-to-evict pod annotation set to false": {
   463  			eviction: &agonesv1.Eviction{Safe: agonesv1.EvictionSafeNever},
   464  			pod: emptyPodAnd(func(pod *corev1.Pod) {
   465  				pod.ObjectMeta.Annotations[agonesv1.PodSafeToEvictAnnotation] = agonesv1.False
   466  			}),
   467  			wantPod: emptyPodAnd(func(pod *corev1.Pod) {
   468  				pod.ObjectMeta.Labels[agonesv1.SafeToEvictLabel] = agonesv1.False
   469  			}),
   470  		},
   471  	} {
   472  		t.Run(desc, func(t *testing.T) {
   473  			err := setEvictionNoExtended(tc.eviction, tc.pod)
   474  			if tc.wantErr {
   475  				assert.Error(t, err)
   476  				return
   477  			}
   478  			assert.NoError(t, err)
   479  			assert.Equal(t, tc.wantPod, tc.pod)
   480  		})
   481  	}
   482  }
   483  
   484  func TestAutopilotPortAllocator(t *testing.T) {
   485  	for name, tc := range map[string]struct {
   486  		ports           []agonesv1.GameServerPort
   487  		wantPorts       []agonesv1.GameServerPort
   488  		passthroughFlag string
   489  		wantAnnotation  bool
   490  	}{
   491  		"no ports => no change": {passthroughFlag: "false"},
   492  		"ports => assigned and annotated": {
   493  			passthroughFlag: "true",
   494  			ports: []agonesv1.GameServerPort{
   495  				{
   496  					Name:          "some-tcpudp",
   497  					PortPolicy:    agonesv1.Dynamic,
   498  					ContainerPort: 4321,
   499  					Protocol:      agonesv1.ProtocolTCPUDP,
   500  				},
   501  				{
   502  					Name:          "awesome-udp",
   503  					PortPolicy:    agonesv1.Dynamic,
   504  					ContainerPort: 1234,
   505  					Protocol:      corev1.ProtocolUDP,
   506  				},
   507  				{
   508  					Name:          "awesome-tcp",
   509  					PortPolicy:    agonesv1.Dynamic,
   510  					ContainerPort: 1234,
   511  					Protocol:      corev1.ProtocolTCP,
   512  				},
   513  				{
   514  					Name:          "another-tcpudp",
   515  					PortPolicy:    agonesv1.Dynamic,
   516  					ContainerPort: 5678,
   517  					Protocol:      agonesv1.ProtocolTCPUDP,
   518  				},
   519  				{
   520  					Name:          "passthrough-tcp",
   521  					PortPolicy:    agonesv1.Passthrough,
   522  					Range:         agonesv1.DefaultPortRange,
   523  					ContainerPort: 1234,
   524  					Protocol:      corev1.ProtocolTCP,
   525  				},
   526  				{
   527  					Name:          "passthrough-tcpudp",
   528  					PortPolicy:    agonesv1.Passthrough,
   529  					ContainerPort: 5678,
   530  					Protocol:      agonesv1.ProtocolTCPUDP,
   531  				},
   532  			},
   533  			wantPorts: []agonesv1.GameServerPort{
   534  				{
   535  					Name:          "some-tcpudp-tcp",
   536  					PortPolicy:    agonesv1.Dynamic,
   537  					ContainerPort: 4321,
   538  					HostPort:      1,
   539  					Protocol:      corev1.ProtocolTCP,
   540  				},
   541  				{
   542  					Name:          "some-tcpudp-udp",
   543  					PortPolicy:    agonesv1.Dynamic,
   544  					ContainerPort: 4321,
   545  					HostPort:      1,
   546  					Protocol:      corev1.ProtocolUDP,
   547  				},
   548  				{
   549  					Name:          "awesome-udp",
   550  					PortPolicy:    agonesv1.Dynamic,
   551  					ContainerPort: 1234,
   552  					HostPort:      2,
   553  					Protocol:      corev1.ProtocolUDP,
   554  				},
   555  				{
   556  					Name:          "awesome-tcp",
   557  					PortPolicy:    agonesv1.Dynamic,
   558  					ContainerPort: 1234,
   559  					HostPort:      3,
   560  					Protocol:      corev1.ProtocolTCP,
   561  				},
   562  				{
   563  					Name:          "another-tcpudp-tcp",
   564  					PortPolicy:    agonesv1.Dynamic,
   565  					ContainerPort: 5678,
   566  					HostPort:      4,
   567  					Protocol:      corev1.ProtocolTCP,
   568  				},
   569  				{
   570  					Name:          "another-tcpudp-udp",
   571  					PortPolicy:    agonesv1.Dynamic,
   572  					ContainerPort: 5678,
   573  					HostPort:      4,
   574  					Protocol:      corev1.ProtocolUDP,
   575  				},
   576  				{
   577  					Name:          "passthrough-tcp",
   578  					PortPolicy:    agonesv1.Passthrough,
   579  					Range:         agonesv1.DefaultPortRange,
   580  					ContainerPort: 1234,
   581  					HostPort:      5,
   582  					Protocol:      corev1.ProtocolTCP,
   583  				},
   584  				{
   585  					Name:          "passthrough-tcpudp-tcp",
   586  					PortPolicy:    agonesv1.Passthrough,
   587  					ContainerPort: 5678,
   588  					HostPort:      6,
   589  					Protocol:      corev1.ProtocolTCP,
   590  				},
   591  				{
   592  					Name:          "passthrough-tcpudp-udp",
   593  					PortPolicy:    agonesv1.Passthrough,
   594  					ContainerPort: 5678,
   595  					HostPort:      6,
   596  					Protocol:      corev1.ProtocolUDP,
   597  				},
   598  			},
   599  			wantAnnotation: true,
   600  		},
   601  		"bad policy => no change (should be rejected by webhooks previously)": {
   602  			passthroughFlag: "false",
   603  			ports: []agonesv1.GameServerPort{
   604  				{
   605  					Name:          "awesome-udp",
   606  					PortPolicy:    agonesv1.Static,
   607  					ContainerPort: 1234,
   608  					Protocol:      corev1.ProtocolUDP,
   609  				},
   610  				{
   611  					Name:          "awesome-none-udp",
   612  					PortPolicy:    agonesv1.None,
   613  					ContainerPort: 1234,
   614  					Protocol:      corev1.ProtocolUDP,
   615  				},
   616  				{
   617  					Name:          "passthrough-tcp",
   618  					PortPolicy:    agonesv1.Passthrough,
   619  					ContainerPort: 1234,
   620  					Protocol:      corev1.ProtocolTCP,
   621  				},
   622  			},
   623  			wantPorts: []agonesv1.GameServerPort{
   624  				{
   625  					Name:          "awesome-udp",
   626  					PortPolicy:    agonesv1.Static,
   627  					ContainerPort: 1234,
   628  					Protocol:      corev1.ProtocolUDP,
   629  				},
   630  				{
   631  					Name:          "awesome-none-udp",
   632  					PortPolicy:    agonesv1.None,
   633  					ContainerPort: 1234,
   634  					Protocol:      corev1.ProtocolUDP,
   635  				},
   636  				{
   637  					Name:          "passthrough-tcp",
   638  					PortPolicy:    agonesv1.Passthrough,
   639  					ContainerPort: 1234,
   640  					Protocol:      corev1.ProtocolTCP,
   641  				},
   642  			},
   643  		},
   644  	} {
   645  		t.Run(name, func(t *testing.T) {
   646  			// PortPolicy None is behind a feature flag
   647  			runtime.FeatureTestMutex.Lock()
   648  			defer runtime.FeatureTestMutex.Unlock()
   649  			gs := (&autopilotPortAllocator{minPort: 8000, maxPort: 9000}).Allocate(&agonesv1.GameServer{Spec: agonesv1.GameServerSpec{Ports: tc.ports}})
   650  			wantGS := &agonesv1.GameServer{Spec: agonesv1.GameServerSpec{Ports: tc.wantPorts}}
   651  			if tc.wantAnnotation {
   652  				wantGS.Spec.Template = corev1.PodTemplateSpec{
   653  					ObjectMeta: metav1.ObjectMeta{
   654  						Annotations: map[string]string{"autopilot.gke.io/host-port-assignment": `{"min":8000,"max":9000}`},
   655  					},
   656  				}
   657  			}
   658  			require.Equal(t, wantGS, gs)
   659  		})
   660  	}
   661  }
   662  
   663  func testPod(annotations map[string]string) *corev1.Pod {
   664  	return &corev1.Pod{
   665  		ObjectMeta: metav1.ObjectMeta{
   666  			Name:        "best-game-server",
   667  			Namespace:   "best-game",
   668  			Annotations: annotations,
   669  		},
   670  		TypeMeta: metav1.TypeMeta{Kind: "Pod"},
   671  	}
   672  }
   673  
   674  func testGameServer(portSpecIn []int32, portStatusIn []int32) *agonesv1.GameServer {
   675  	var portSpec []agonesv1.GameServerPort
   676  	for _, port := range portSpecIn {
   677  		portSpec = append(portSpec, agonesv1.GameServerPort{HostPort: port})
   678  	}
   679  	var portStatus []agonesv1.GameServerStatusPort
   680  	for _, port := range portStatusIn {
   681  		portStatus = append(portStatus, agonesv1.GameServerStatusPort{Port: port})
   682  	}
   683  	return &agonesv1.GameServer{
   684  		ObjectMeta: metav1.ObjectMeta{
   685  			Name:      "best-game-server",
   686  			Namespace: "best-game",
   687  		},
   688  		Spec: agonesv1.GameServerSpec{
   689  			Ports: portSpec,
   690  		},
   691  		Status: agonesv1.GameServerStatus{
   692  			Ports: portStatus,
   693  		},
   694  	}
   695  }