diff --git a/install/helm/agones/templates/extensions.yaml b/install/helm/agones/templates/extensions.yaml index 597dd180fe..ff6f62b7e9 100644 --- a/install/helm/agones/templates/extensions.yaml +++ b/install/helm/agones/templates/extensions.yaml @@ -169,6 +169,54 @@ webhooks: {{- end }} {{- if not (default .Values.agones.controller.disableSecret .Values.agones.extensions.disableSecret) }} --- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: zzz-agones-mutation-webhook +{{- $annotations := default .Values.agones.controller.mutatingWebhook.annotations .Values.agones.extensions.mutatingWebhook.annotations }} +{{- if $annotations }} + annotations: +{{- toYaml $annotations | nindent 4 }} +{{- end }} + labels: + component: controller + app: {{ template "agones.name" . }} + chart: {{ template "agones.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +webhooks: + - name: mutations.agones.dev + admissionReviewVersions: + - v1 + sideEffects: None + failurePolicy: Fail + clientConfig: + service: + name: agones-controller-service + namespace: {{ .Release.Namespace }} + path: /mutate +{{- if not .Values.agones.controller.mutatingWebhook.disableCaBundle }} +{{- if .Values.agones.controller.generateTLS }} + caBundle: {{ b64enc $ca.Cert }} +{{- else }} + caBundle: {{ default (.Files.Get "certs/server.crt") .Values.agones.controller.tlsCert | b64enc }} +{{- end }} +{{- end }} + objectSelector: + matchLabels: + agones.dev/port: "autopilot-passthrough" + rules: + - apiGroups: + - "" + resources: + - "pods" + apiVersions: + - "v1" + operations: + - CREATE +{{- end }} +{{- if not .Values.agones.controller.disableSecret }} +--- apiVersion: v1 kind: Secret metadata: diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index cfa540ac6a..2f1c963cbb 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -17599,6 +17599,42 @@ webhooks: - CREATE - UPDATE --- +# Source: agones/templates/extensions.yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: zzz-agones-mutation-webhook + labels: + component: controller + app: agones + chart: agones-1.41.0-dev + release: agones-manual + heritage: Helm +webhooks: + - name: mutations.agones.dev + admissionReviewVersions: + - v1 + sideEffects: None + failurePolicy: Fail + clientConfig: + service: + name: agones-controller-service + namespace: agones-system + path: /mutate + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVjVENDQTFtZ0F3SUJBZ0lVRm5DOUsxT1kzRnFNaWhqN3RWbXh5R3hwUVdzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dhb3hDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwVGIyMWxMVk4wWVhSbE1ROHdEUVlEVlFRSwpEQVpCWjI5dVpYTXhEekFOQmdOVkJBc01Ca0ZuYjI1bGN6RTBNRElHQTFVRUF3d3JZV2R2Ym1WekxXTnZiblJ5CmIyeHNaWEl0YzJWeWRtbGpaUzVoWjI5dVpYTXRjM2x6ZEdWdExuTjJZekV1TUN3R0NTcUdTSWIzRFFFSkFSWWYKWVdkdmJtVnpMV1JwYzJOMWMzTkFaMjl2WjJ4bFozSnZkWEJ6TG1OdmJUQWVGdzB5TVRBMk16QXhPVFUyTWpGYQpGdzB6TVRBMk1qZ3hPVFUyTWpGYU1JR3FNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1UyOXRaUzFUCmRHRjBaVEVQTUEwR0ExVUVDZ3dHUVdkdmJtVnpNUTh3RFFZRFZRUUxEQVpCWjI5dVpYTXhOREF5QmdOVkJBTU0KSzJGbmIyNWxjeTFqYjI1MGNtOXNiR1Z5TFhObGNuWnBZMlV1WVdkdmJtVnpMWE41YzNSbGJTNXpkbU14TGpBcwpCZ2txaGtpRzl3MEJDUUVXSDJGbmIyNWxjeTFrYVhOamRYTnpRR2R2YjJkc1pXZHliM1Z3Y3k1amIyMHdnZ0VpCk1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ2dka0xPS0NINThLSkJpdEJqeVlyTDArRTkKdEl0TFhGVGdxQU9TMGdBQitSVXNZMGhicmVWRHd0SExKYXBnMG55Ni9UYTcvMEc1Wm9kaGR4RlFtS2JWMUxmWQpmZGR0Qm4vOGd4Wi9JQ2dRblU3N3RqY1pLV3JxaW4vZ3h3ZUJua3hjWEtrT3Z1MldoRHdZZVFLN3ZHNEljOGhzClZHb1hTZWo4US94d2M4a0FCRG04YVRSU1RUYmsyWi9kem9mUmswU2xrc1BrVWV5b0NwRGVGbERqY0tTcDAzWnUKV2dBUTNpVy83c1AxVFV5WEtnblZ5M2ZpWm1RQUZreEtOQkxVV0gvVEJJeWtMdUVCMmRYYUd0L0VpZzQ4SWpVOQpMYUxyM3JWSW1Dcmt6dlB5V3VEZTd6MmVKdDE3WEhoTFVHcnE4YTFUSFp3d1NSWUZRc29tQ09ORVNBSTdBZ01CCkFBR2pnWXd3Z1lrd0hRWURWUjBPQkJZRUZMa3FUUWNMQloyMUlWc3BGbkNiaS9TbGtUbzlNQjhHQTFVZEl3UVkKTUJhQUZMa3FUUWNMQloyMUlWc3BGbkNiaS9TbGtUbzlNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdOZ1lEVlIwUgpCQzh3TFlJcllXZHZibVZ6TFdOdmJuUnliMnhzWlhJdGMyVnlkbWxqWlM1aFoyOXVaWE10YzNsemRHVnRMbk4yCll6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFWQTUxU3dNcEhZY20zUnRuc2I5MkgwUTZYT1ZndEJzSWRaY1QKbFBuSmFBSGdybEt2SnhiMU0rdTdQYllDZkZOTWlUTStyWGZ5cWtJRXY3VU1aN0dWeS9CYm9zTk1sb2M0UHJjaAo3RnVlai9zVnArcW1GT1c0VzlPVTFwcytqWm5vcHJ4Z3R1OVgzbmpBZjZiWWVqQWMzaVo0Q0xpem8vMDd2Qk94CnA5L3J4R0FjSVVjQW04Y3hXa01kaEduNnZOYkNFcXJoVTRJdnZSYlMwVnlrckhPY3RGM25raC9GbnRHQU80RDEKUEgrUThSQXBNK2xBeGtXcFIvNXlHTXdLM05WcS9kc2JaclQ5RHhId0hUU2tqL3JXZVRrWmxIN042MHpZL3JqbwpNUjBJNEtOWHl3WElTcGdNbE93dkxPdGY2aUNYeHJDNyt1RjdyQmxCei9tSUNxYnR0dz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + objectSelector: + matchLabels: + agones.dev/port: "autopilot-passthrough" + rules: + - apiGroups: + - "" + resources: + - "pods" + apiVersions: + - "v1" + operations: + - CREATE +--- # Source: agones/templates/priority-class.yaml apiVersion: scheduling.k8s.io/v1 kind: PriorityClass diff --git a/pkg/cloudproduct/gke/gke.go b/pkg/cloudproduct/gke/gke.go index 42b18afc88..144c131295 100644 --- a/pkg/cloudproduct/gke/gke.go +++ b/pkg/cloudproduct/gke/gke.go @@ -134,10 +134,18 @@ func (*gkeAutopilot) NewPortAllocator(portRanges map[string]portallocator.PortRa func (*gkeAutopilot) WaitOnFreePorts() bool { return true } +func checkPassthroughPortPolicy(portPolicy agonesv1.PortPolicy) bool { + // if feature is not enabled and port is Passthrough return true because that should be an invalid port + // if feature is not enabled and port is not Passthrough you can return false because there's no error but check for None port + // if feature is enabled and port is passthrough return false because there is no error + // if feature is enabled and port is not passthrough return false because there is no error but check for None port + return (!runtime.FeatureEnabled(runtime.FeatureAutopilotPassthroughPort) && portPolicy == agonesv1.Passthrough) || portPolicy == agonesv1.Static +} + func (g *gkeAutopilot) ValidateGameServerSpec(gss *agonesv1.GameServerSpec, fldPath *field.Path) field.ErrorList { allErrs := g.ValidateScheduling(gss.Scheduling, fldPath.Child("scheduling")) for i, p := range gss.Ports { - if p.PortPolicy != agonesv1.Dynamic && (p.PortPolicy != agonesv1.None || !runtime.FeatureEnabled(runtime.FeaturePortPolicyNone)) { + if p.PortPolicy != agonesv1.Dynamic && (p.PortPolicy != agonesv1.None || !runtime.FeatureEnabled(runtime.FeaturePortPolicyNone)) && checkPassthroughPortPolicy(p.PortPolicy) { allErrs = append(allErrs, field.Invalid(fldPath.Child("ports").Index(i).Child("portPolicy"), string(p.PortPolicy), errPortPolicyMustBeDynamicOrNone)) } if p.Range != agonesv1.DefaultPortRange && (p.PortPolicy != agonesv1.None || !runtime.FeatureEnabled(runtime.FeaturePortPolicyNone)) { @@ -256,6 +264,15 @@ type autopilotPortAllocator struct { func (*autopilotPortAllocator) Run(_ context.Context) error { return nil } func (*autopilotPortAllocator) DeAllocate(gs *agonesv1.GameServer) {} +func checkPassthroughPortPolicyForAutopilot(portPolicy agonesv1.PortPolicy) bool { + // Autopilot can have Dynamic or Passthrough + // if feature is not enabled and port is Passthrough -> true + // if feature is not enabled and port is not Passthrough -> true + // if feature is enabled and port is Passthrough -> false + // if feature is enabled and port is not Passthrough -> true + return !(runtime.FeatureEnabled(runtime.FeatureAutopilotPassthroughPort) && portPolicy == agonesv1.Passthrough) +} + func (apa *autopilotPortAllocator) Allocate(gs *agonesv1.GameServer) *agonesv1.GameServer { if len(gs.Spec.Ports) == 0 { return gs // Nothing to do. @@ -263,7 +280,7 @@ func (apa *autopilotPortAllocator) Allocate(gs *agonesv1.GameServer) *agonesv1.G var ports []agonesv1.GameServerPort for i, p := range gs.Spec.Ports { - if p.PortPolicy != agonesv1.Dynamic { + if p.PortPolicy != agonesv1.Dynamic && checkPassthroughPortPolicyForAutopilot(p.PortPolicy) { logger.WithField("gs", gs.Name).WithField("portPolicy", p.PortPolicy).Error( "GameServer has invalid PortPolicy for Autopilot - this should have been rejected by webhooks. Refusing to assign ports.") return gs diff --git a/pkg/cloudproduct/gke/gke_test.go b/pkg/cloudproduct/gke/gke_test.go index abc4548b81..152764100d 100644 --- a/pkg/cloudproduct/gke/gke_test.go +++ b/pkg/cloudproduct/gke/gke_test.go @@ -79,14 +79,16 @@ func TestSyncPodPortsToGameServer(t *testing.T) { func TestValidateGameServer(t *testing.T) { for name, tc := range map[string]struct { - edPods bool - ports []agonesv1.GameServerPort - scheduling apis.SchedulingStrategy - safeToEvict agonesv1.EvictionSafe - want field.ErrorList + edPods bool + ports []agonesv1.GameServerPort + scheduling apis.SchedulingStrategy + safeToEvict agonesv1.EvictionSafe + want field.ErrorList + passthroughFlag string }{ - "no ports => validated": {scheduling: apis.Packed}, + "no ports => validated": {passthroughFlag: "false", scheduling: apis.Packed}, "good ports => validated": { + passthroughFlag: "true", ports: []agonesv1.GameServerPort{ { Name: "some-tcpudp", @@ -115,11 +117,26 @@ func TestValidateGameServer(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "passthrough-udp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, }, safeToEvict: agonesv1.EvictionSafeAlways, scheduling: apis.Packed, }, "bad port range => fails validation": { + passthroughFlag: "true", ports: []agonesv1.GameServerPort{ { Name: "best-tcpudp", @@ -142,15 +159,32 @@ func TestValidateGameServer(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "passthrough-udp-bad-range", + PortPolicy: agonesv1.Passthrough, + Range: "passthrough", + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, + { + Name: "passthrough-tcp-bad-range", + PortPolicy: agonesv1.Passthrough, + Range: "games", + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, }, safeToEvict: agonesv1.EvictionSafeAlways, scheduling: apis.Packed, want: field.ErrorList{ field.Invalid(field.NewPath("spec", "ports").Index(1).Child("range"), "game", "range must not be used on GKE Autopilot"), field.Invalid(field.NewPath("spec", "ports").Index(2).Child("range"), "game", "range must not be used on GKE Autopilot"), + field.Invalid(field.NewPath("spec", "ports").Index(3).Child("range"), "passthrough", "range must not be used on GKE Autopilot"), + field.Invalid(field.NewPath("spec", "ports").Index(4).Child("range"), "games", "range must not be used on GKE Autopilot"), }, }, "bad policy (no feature gates) => fails validation": { + passthroughFlag: "false", ports: []agonesv1.GameServerPort{ { Name: "best-tcpudp", @@ -173,6 +207,20 @@ func TestValidateGameServer(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "passthrough-udp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, }, safeToEvict: agonesv1.EvictionSafeOnUpgrade, scheduling: apis.Distributed, @@ -180,11 +228,14 @@ func TestValidateGameServer(t *testing.T) { field.Invalid(field.NewPath("spec", "scheduling"), "Distributed", "scheduling strategy must be Packed on GKE Autopilot"), field.Invalid(field.NewPath("spec", "ports").Index(1).Child("portPolicy"), "Static", "portPolicy must be Dynamic or None on GKE Autopilot"), field.Invalid(field.NewPath("spec", "ports").Index(2).Child("portPolicy"), "Static", "portPolicy must be Dynamic or None on GKE Autopilot"), + field.Invalid(field.NewPath("spec", "ports").Index(3).Child("portPolicy"), "Passthrough", "portPolicy must be Dynamic or None on GKE Autopilot"), + field.Invalid(field.NewPath("spec", "ports").Index(4).Child("portPolicy"), "Passthrough", "portPolicy must be Dynamic or None on GKE Autopilot"), field.Invalid(field.NewPath("spec", "eviction", "safe"), "OnUpgrade", "eviction.safe OnUpgrade not supported on GKE Autopilot"), }, }, "bad policy (GKEAutopilotExtendedDurationPods enabled) => fails validation but OnUpgrade works": { - edPods: true, + edPods: true, + passthroughFlag: "false", ports: []agonesv1.GameServerPort{ { Name: "best-tcpudp", @@ -207,6 +258,13 @@ func TestValidateGameServer(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "passthrough-udp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, }, safeToEvict: agonesv1.EvictionSafeOnUpgrade, scheduling: apis.Distributed, @@ -214,6 +272,7 @@ func TestValidateGameServer(t *testing.T) { field.Invalid(field.NewPath("spec", "scheduling"), "Distributed", "scheduling strategy must be Packed on GKE Autopilot"), field.Invalid(field.NewPath("spec", "ports").Index(1).Child("portPolicy"), "Static", "portPolicy must be Dynamic or None on GKE Autopilot"), field.Invalid(field.NewPath("spec", "ports").Index(2).Child("portPolicy"), "Static", "portPolicy must be Dynamic or None on GKE Autopilot"), + field.Invalid(field.NewPath("spec", "ports").Index(3).Child("portPolicy"), "Passthrough", "portPolicy must be Dynamic or None on GKE Autopilot"), }, }, } { @@ -221,7 +280,7 @@ func TestValidateGameServer(t *testing.T) { // PortPolicy None is behind a feature flag runtime.FeatureTestMutex.Lock() defer runtime.FeatureTestMutex.Unlock() - require.NoError(t, runtime.ParseFeatures(string(runtime.FeaturePortPolicyNone)+"=true")) + require.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s="+tc.passthroughFlag, runtime.FeaturePortPolicyNone, runtime.FeatureAutopilotPassthroughPort))) causes := (&gkeAutopilot{useExtendedDurationPods: tc.edPods}).ValidateGameServerSpec(&agonesv1.GameServerSpec{ Ports: tc.ports, @@ -451,12 +510,14 @@ func TestSetEvictionNoExtended(t *testing.T) { func TestAutopilotPortAllocator(t *testing.T) { for name, tc := range map[string]struct { - ports []agonesv1.GameServerPort - wantPorts []agonesv1.GameServerPort - wantAnnotation bool + ports []agonesv1.GameServerPort + wantPorts []agonesv1.GameServerPort + passthroughFlag string + wantAnnotation bool }{ - "no ports => no change": {}, + "no ports => no change": {passthroughFlag: "false"}, "ports => assigned and annotated": { + passthroughFlag: "true", ports: []agonesv1.GameServerPort{ { Name: "some-tcpudp", @@ -482,6 +543,19 @@ func TestAutopilotPortAllocator(t *testing.T) { ContainerPort: 5678, Protocol: agonesv1.ProtocolTCPUDP, }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "passthrough-tcpudp", + PortPolicy: agonesv1.Passthrough, + ContainerPort: 5678, + Protocol: agonesv1.ProtocolTCPUDP, + }, }, wantPorts: []agonesv1.GameServerPort{ { @@ -526,10 +600,33 @@ func TestAutopilotPortAllocator(t *testing.T) { HostPort: 4, Protocol: corev1.ProtocolUDP, }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + Range: agonesv1.DefaultPortRange, + ContainerPort: 1234, + HostPort: 5, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "passthrough-tcpudp-tcp", + PortPolicy: agonesv1.Passthrough, + ContainerPort: 5678, + HostPort: 6, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "passthrough-tcpudp-udp", + PortPolicy: agonesv1.Passthrough, + ContainerPort: 5678, + HostPort: 6, + Protocol: corev1.ProtocolUDP, + }, }, wantAnnotation: true, }, "bad policy => no change (should be rejected by webhooks previously)": { + passthroughFlag: "false", ports: []agonesv1.GameServerPort{ { Name: "awesome-udp", @@ -537,6 +634,18 @@ func TestAutopilotPortAllocator(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "awesome-none-udp", + PortPolicy: agonesv1.None, + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, }, wantPorts: []agonesv1.GameServerPort{ { @@ -545,10 +654,26 @@ func TestAutopilotPortAllocator(t *testing.T) { ContainerPort: 1234, Protocol: corev1.ProtocolUDP, }, + { + Name: "awesome-none-udp", + PortPolicy: agonesv1.None, + ContainerPort: 1234, + Protocol: corev1.ProtocolUDP, + }, + { + Name: "passthrough-tcp", + PortPolicy: agonesv1.Passthrough, + ContainerPort: 1234, + Protocol: corev1.ProtocolTCP, + }, }, }, } { t.Run(name, func(t *testing.T) { + // PortPolicy None is behind a feature flag + runtime.FeatureTestMutex.Lock() + defer runtime.FeatureTestMutex.Unlock() + require.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s="+tc.passthroughFlag, runtime.FeatureAutopilotPassthroughPort))) gs := (&autopilotPortAllocator{minPort: 8000, maxPort: 9000}).Allocate(&agonesv1.GameServer{Spec: agonesv1.GameServerSpec{Ports: tc.ports}}) wantGS := &agonesv1.GameServer{Spec: agonesv1.GameServerSpec{Ports: tc.wantPorts}} if tc.wantAnnotation { diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index d44cce42a3..4121b5404d 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -59,9 +59,10 @@ import ( ) const ( - sdkserverSidecarName = "agones-gameserver-sidecar" - grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" - httpPortEnvVar = "AGONES_SDK_HTTP_PORT" + sdkserverSidecarName = "agones-gameserver-sidecar" + grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" + httpPortEnvVar = "AGONES_SDK_HTTP_PORT" + passthroughPortEnvVar = "PASSTHROUGH" ) // Extensions struct contains what is needed to bind webhook handlers @@ -211,6 +212,9 @@ func NewExtensions(apiHooks agonesv1.APIHooks, wh *webhooks.WebHook) *Extensions wh.AddHandler("/mutate", agonesv1.Kind("GameServer"), admissionv1.Create, ext.creationMutationHandler) wh.AddHandler("/validate", agonesv1.Kind("GameServer"), admissionv1.Create, ext.creationValidationHandler) + if runtime.FeatureEnabled(runtime.FeatureAutopilotPassthroughPort) { + wh.AddHandler("/mutate", corev1.SchemeGroupVersion.WithKind("Pod").GroupKind(), admissionv1.Create, ext.creationMutationHandlerPod) + } return ext } @@ -316,6 +320,45 @@ func (ext *Extensions) creationValidationHandler(review admissionv1.AdmissionRev return review, nil } +// creationMutationHandlerPod that mutates a GameServer pod when it is created +// Should only be called on gameserver pod create operations. +func (ext *Extensions) creationMutationHandlerPod(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) { + obj := review.Request.Object + pod := &corev1.Pod{} + err := json.Unmarshal(obj.Raw, pod) + if err != nil { + // If the JSON is invalid during mutation, fall through to validation. This allows OpenAPI schema validation + // to proceed, resulting in a more user friendly error message. + return review, nil + } + + ext.baseLogger.WithField("pod.Name", pod.Name).Debug("creationMutationHandlerPod") + + // TODO: We need to deal with case of multiple and mixed type ports before enabling the feature gate. + pod.Spec.Containers[1].Ports[0].ContainerPort = pod.Spec.Containers[1].Ports[0].HostPort + + newPod, err := json.Marshal(pod) + if err != nil { + return review, errors.Wrapf(err, "error marshalling changes applied Pod %s to json", pod.ObjectMeta.Name) + } + + patch, err := jsonpatch.CreatePatch(obj.Raw, newPod) + if err != nil { + return review, errors.Wrapf(err, "error creating patch for Pod %s", pod.ObjectMeta.Name) + } + + jsonPatch, err := json.Marshal(patch) + if err != nil { + return review, errors.Wrapf(err, "error creating json for patch for Pod %s", pod.ObjectMeta.Name) + } + + pt := admissionv1.PatchTypeJSONPatch + review.Response.PatchType = &pt + review.Response.Patch = jsonPatch + + return review, nil +} + // Run the GameServer controller. Will block until stop is closed. // Runs threadiness number workers to process the rate limited queue func (c *Controller) Run(ctx context.Context, workers int) error { diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index 7d40f5ee9f..ed37442aea 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -57,6 +57,7 @@ const ( ) var GameServerKind = metav1.GroupVersionKind(agonesv1.SchemeGroupVersion.WithKind("GameServer")) +var PodKind = corev1.SchemeGroupVersion.WithKind("Pod") func TestControllerSyncGameServer(t *testing.T) { t.Parallel() @@ -580,6 +581,54 @@ func TestControllerCreationValidationHandler(t *testing.T) { }) } +func TestControllerCreationMutationHandlerPod(t *testing.T) { + t.Parallel() + ext := newFakeExtensions() + + type expected struct { + patches []jsonpatch.JsonPatchOperation + } + + t.Run("valid pod mutation for Passthrough portPolicy, containerPort should be the same as hostPort", func(t *testing.T) { + gameServerHostPort := float64(newPassthroughPortSingleContainerSpec().Containers[1].Ports[0].HostPort) + fixture := &corev1.Pod{Spec: newPassthroughPortSingleContainerSpec()} + raw, err := json.Marshal(fixture) + require.NoError(t, err) + review := admissionv1.AdmissionReview{ + Request: &admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind(PodKind), + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: raw, + }, + }, + Response: &admissionv1.AdmissionResponse{Allowed: true}, + } + expected := expected{ + patches: []jsonpatch.JsonPatchOperation{ + {Operation: "replace", Path: "/spec/containers/1/ports/0/containerPort", Value: gameServerHostPort}}, + } + + result, err := ext.creationMutationHandlerPod(review) + assert.NoError(t, err) + patch := &jsonpatch.ByPath{} + err = json.Unmarshal(result.Response.Patch, patch) + found := false + + for _, expected := range expected.patches { + for _, p := range *patch { + if assert.ObjectsAreEqual(p, expected) { + found = true + } + } + assert.True(t, found, "Could not find operation %#v in patch %v", expected, *patch) + } + + require.NoError(t, err) + + }) +} + func TestControllerSyncGameServerDeletionTimestamp(t *testing.T) { t.Parallel() @@ -2225,3 +2274,16 @@ func newSingleContainerSpec() agonesv1.GameServerSpec { }, } } + +func newPassthroughPortSingleContainerSpec() corev1.PodSpec { + return corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agones-gameserver-sidecar", + Image: "container/image", + Env: []corev1.EnvVar{{Name: passthroughPortEnvVar, Value: "TRUE"}}}, + {Name: "simple-game-server", + Image: "container2/image", + Ports: []corev1.ContainerPort{{HostPort: 7777, ContainerPort: 555}}, + Env: []corev1.EnvVar{{Name: passthroughPortEnvVar, Value: "TRUE"}}}}, + } +} diff --git a/pkg/util/webhooks/webhooks.go b/pkg/util/webhooks/webhooks.go index a0c2898a50..fa654e17af 100644 --- a/pkg/util/webhooks/webhooks.go +++ b/pkg/util/webhooks/webhooks.go @@ -86,6 +86,7 @@ func (wh *WebHook) handle(path string, w http.ResponseWriter, r *http.Request) e review.Response = &admissionv1.AdmissionResponse{Allowed: true} } review.Response.UID = review.Request.UID + wh.logger.WithField("name", review.Request.Name).WithField("path", path).WithField("kind", review.Request.Kind.Kind).WithField("group", review.Request.Kind.Group).Debug("handling webhook request") for _, oh := range wh.handlers[path] { if oh.operation == review.Request.Operation &&