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