diff --git a/apis/v1alpha2/nginxproxy_types.go b/apis/v1alpha2/nginxproxy_types.go index 43b509d06d..98f9d24a6d 100644 --- a/apis/v1alpha2/nginxproxy_types.go +++ b/apis/v1alpha2/nginxproxy_types.go @@ -479,6 +479,11 @@ type ContainerSpec struct { // +optional Lifecycle *corev1.Lifecycle `json:"lifecycle,omitempty"` + // HostPorts are the list of ports to expose on the host. + // + // +optional + HostPorts []HostPort `json:"hostPorts,omitempty"` + // VolumeMounts describe the mounting of Volumes within a container. // // +optional @@ -599,12 +604,25 @@ const ( // automatically if required. The default NodePort range enforced by Kubernetes is 30000-32767. type NodePort struct { // Port is the NodePort to expose. - // kubebuilder:validation:Minimum=1 - // kubebuilder:validation:Maximum=65535 + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 Port int32 `json:"port"` // ListenerPort is the Gateway listener port that this NodePort maps to. - // kubebuilder:validation:Minimum=1 - // kubebuilder:validation:Maximum=65535 + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 ListenerPort int32 `json:"listenerPort"` } + +// HostPort exposes an nginx container port on the host. +type HostPort struct { + // Port to expose on the host. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Port int32 `json:"port"` + + // ContainerPort is the port on the nginx container to map to the HostPort. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + ContainerPort int32 `json:"containerPort"` +} diff --git a/apis/v1alpha2/zz_generated.deepcopy.go b/apis/v1alpha2/zz_generated.deepcopy.go index 4b0f8bf9f8..d17e59b0b3 100644 --- a/apis/v1alpha2/zz_generated.deepcopy.go +++ b/apis/v1alpha2/zz_generated.deepcopy.go @@ -34,6 +34,11 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { *out = new(v1.Lifecycle) (*in).DeepCopyInto(*out) } + if in.HostPorts != nil { + in, out := &in.HostPorts, &out.HostPorts + *out = make([]HostPort, len(*in)) + copy(*out, *in) + } if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]v1.VolumeMount, len(*in)) @@ -92,6 +97,21 @@ func (in *DeploymentSpec) DeepCopy() *DeploymentSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPort) DeepCopyInto(out *HostPort) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPort. +func (in *HostPort) DeepCopy() *HostPort { + if in == nil { + return nil + } + out := new(HostPort) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Image) DeepCopyInto(out *Image) { *out = *in diff --git a/charts/nginx-gateway-fabric/README.md b/charts/nginx-gateway-fabric/README.md index 656ef9a82a..26e932f86a 100644 --- a/charts/nginx-gateway-fabric/README.md +++ b/charts/nginx-gateway-fabric/README.md @@ -264,9 +264,13 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `certGenerator.ttlSecondsAfterFinished` | How long to wait after the cert generator job has finished before it is removed by the job controller. | int | `30` | | `clusterDomain` | The DNS cluster domain of your Kubernetes cluster. | string | `"cluster.local"` | | `gateways` | A list of Gateway objects. View https://gateway-api.sigs.k8s.io/reference/spec/#gateway for full Gateway reference. | list | `[]` | -| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` | +| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{"hostPorts":[],"lifecycle":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` | | `nginx.config` | The configuration for the data plane that is contained in the NginxProxy resource. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` | -| `nginx.container` | The container configuration for the NGINX container. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` | +| `nginx.container` | The container configuration for the NGINX container. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{"hostPorts":[],"lifecycle":{},"resources":{},"volumeMounts":[]}` | +| `nginx.container.hostPorts` | A list of HostPorts to expose on the host. This configuration allows containers to bind to a specific port on the host node, enabling external network traffic to reach the container directly through the host's IP address and port. Use this option when you need to expose container ports on the host for direct access, such as for debugging, legacy integrations, or when NodePort/LoadBalancer services are not suitable. Note: Using hostPort may have security and scheduling implications, as it ties pods to specific nodes and ports. | list | `[]` | +| `nginx.container.lifecycle` | The lifecycle of the NGINX container. | object | `{}` | +| `nginx.container.resources` | The resource requirements of the NGINX container. | object | `{}` | +| `nginx.container.volumeMounts` | volumeMounts are the additional volume mounts for the NGINX container. | list | `[]` | | `nginx.debug` | Enable debugging for NGINX. Uses the nginx-debug binary. The NGINX error log level should be set to debug in the NginxProxy resource. | bool | `false` | | `nginx.image.repository` | The NGINX image to use. | string | `"ghcr.io/nginx/nginx-gateway-fabric/nginx"` | | `nginx.imagePullSecret` | The name of the secret containing docker registry credentials. Secret must exist in the same namespace as the helm release. The control plane will copy this secret into any namespace where NGINX is deployed. | string | `""` | diff --git a/charts/nginx-gateway-fabric/templates/_helpers.tpl b/charts/nginx-gateway-fabric/templates/_helpers.tpl index d0c7c50d84..a1d5b686f5 100644 --- a/charts/nginx-gateway-fabric/templates/_helpers.tpl +++ b/charts/nginx-gateway-fabric/templates/_helpers.tpl @@ -106,5 +106,7 @@ Filters out empty fields from a struct. {{- $result = merge $result (dict $key $value) }} {{- end }} {{- end }} -{{- $result | toYaml }} +{{- if $result -}} +{{- $result | toYaml -}} +{{- end -}} {{- end }} diff --git a/charts/nginx-gateway-fabric/templates/nginxproxy.yaml b/charts/nginx-gateway-fabric/templates/nginxproxy.yaml index b5e33292c8..0c4640c5b9 100644 --- a/charts/nginx-gateway-fabric/templates/nginxproxy.yaml +++ b/charts/nginx-gateway-fabric/templates/nginxproxy.yaml @@ -18,8 +18,11 @@ spec: {{- toYaml .Values.nginx.pod | nindent 8 }} {{- end }} container: - {{- if .Values.nginx.container }} - {{- toYaml .Values.nginx.container | nindent 8 }} + {{- with .Values.nginx.container }} + {{- $filtered := include "filterEmptyFields" . }} + {{- if $filtered }} + {{- $filtered | nindent 8 }} + {{- end }} {{- end }} image: {{- toYaml .Values.nginx.image | nindent 10 }} @@ -34,8 +37,11 @@ spec: {{- toYaml .Values.nginx.pod | nindent 8 }} {{- end }} container: - {{- if .Values.nginx.container }} - {{- toYaml .Values.nginx.container | nindent 8 }} + {{- with .Values.nginx.container }} + {{- $filtered := include "filterEmptyFields" . }} + {{- if $filtered }} + {{- $filtered | nindent 8 }} + {{- end }} {{- end }} image: {{- toYaml .Values.nginx.image | nindent 10 }} @@ -46,6 +52,9 @@ spec: {{- if .Values.nginx.service }} service: {{- with .Values.nginx.service }} - {{- include "filterEmptyFields" . | nindent 6 }} + {{- $filtered := include "filterEmptyFields" . }} + {{- if $filtered }} + {{- $filtered | nindent 6 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/nginx-gateway-fabric/values.schema.json b/charts/nginx-gateway-fabric/values.schema.json index 15582052e5..7b2034f743 100644 --- a/charts/nginx-gateway-fabric/values.schema.json +++ b/charts/nginx-gateway-fabric/values.schema.json @@ -313,6 +313,53 @@ }, "container": { "description": "The container configuration for the NGINX container. This is applied globally to all Gateways managed by this\ninstance of NGINX Gateway Fabric.", + "properties": { + "hostPorts": { + "description": "A list of HostPorts to expose on the host.\nThis configuration allows containers to bind to a specific port on the host node,\nenabling external network traffic to reach the container directly through the host's IP address and port.\nUse this option when you need to expose container ports on the host for direct access,\nsuch as for debugging, legacy integrations, or when NodePort/LoadBalancer services are not suitable.\nNote: Using hostPort may have security and scheduling implications, as it ties pods to specific nodes and ports.", + "items": { + "properties": { + "containerPort": { + "maximum": 65535, + "minimum": 1, + "required": [], + "type": "integer" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "required": [], + "type": "integer" + } + }, + "required": [], + "type": "object" + }, + "required": [], + "title": "hostPorts", + "type": "array" + }, + "lifecycle": { + "description": "The lifecycle of the NGINX container.", + "required": [], + "title": "lifecycle", + "type": "object" + }, + "resources": { + "description": "The resource requirements of the NGINX container.", + "required": [], + "title": "resources", + "type": "object" + }, + "volumeMounts": { + "description": "volumeMounts are the additional volume mounts for the NGINX container.", + "items": { + "required": [] + }, + "required": [], + "title": "volumeMounts", + "type": "array" + } + }, "required": [], "title": "container", "type": "object" diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index e49bcc20a3..8354e2b926 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -400,15 +400,41 @@ nginx: # -- The container configuration for the NGINX container. This is applied globally to all Gateways managed by this # instance of NGINX Gateway Fabric. - container: {} + container: + # @schema + # type: array + # items: + # type: object + # properties: + # port: + # type: integer + # required: true + # minimum: 1 + # maximum: 65535 + # containerPort: + # type: integer + # required: true + # minimum: 1 + # maximum: 65535 + # @schema + # -- A list of HostPorts to expose on the host. + # This configuration allows containers to bind to a specific port on the host node, + # enabling external network traffic to reach the container directly through the host's IP address and port. + # Use this option when you need to expose container ports on the host for direct access, + # such as for debugging, legacy integrations, or when NodePort/LoadBalancer services are not suitable. + # Note: Using hostPort may have security and scheduling implications, as it ties pods to specific nodes and ports. + hostPorts: [] + # - port: 80 + # containerPort: 80 + # -- The resource requirements of the NGINX container. - # resources: {} + resources: {} # -- The lifecycle of the NGINX container. - # lifecycle: {} + lifecycle: {} # -- volumeMounts are the additional volume mounts for the NGINX container. - # volumeMounts: [] + volumeMounts: [] # -- The service configuration for the NGINX data plane. This is applied globally to all Gateways managed by this # instance of NGINX Gateway Fabric. diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml index 9947e34eb7..ac2ab0e612 100644 --- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml +++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml @@ -81,6 +81,31 @@ spec: description: Debug enables debugging for NGINX by using the nginx-debug binary. type: boolean + hostPorts: + description: HostPorts are the list of ports to expose + on the host. + items: + description: HostPort exposes an nginx container port + on the host. + properties: + containerPort: + description: ContainerPort is the port on the nginx + container to map to the HostPort. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + port: + description: Port to expose on the host. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - containerPort + - port + type: object + type: array image: description: Image is the NGINX image to use. properties: @@ -3467,6 +3492,31 @@ spec: description: Debug enables debugging for NGINX by using the nginx-debug binary. type: boolean + hostPorts: + description: HostPorts are the list of ports to expose + on the host. + items: + description: HostPort exposes an nginx container port + on the host. + properties: + containerPort: + description: ContainerPort is the port on the nginx + container to map to the HostPort. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + port: + description: Port to expose on the host. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - containerPort + - port + type: object + type: array image: description: Image is the NGINX image to use. properties: @@ -6885,18 +6935,17 @@ spec: automatically if required. The default NodePort range enforced by Kubernetes is 30000-32767. properties: listenerPort: - description: |- - ListenerPort is the Gateway listener port that this NodePort maps to. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 + description: ListenerPort is the Gateway listener port + that this NodePort maps to. format: int32 + maximum: 65535 + minimum: 1 type: integer port: - description: |- - Port is the NodePort to expose. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 + description: Port is the NodePort to expose. format: int32 + maximum: 65535 + minimum: 1 type: integer required: - listenerPort diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 7517ce1c4a..b0322888bc 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -666,6 +666,31 @@ spec: description: Debug enables debugging for NGINX by using the nginx-debug binary. type: boolean + hostPorts: + description: HostPorts are the list of ports to expose + on the host. + items: + description: HostPort exposes an nginx container port + on the host. + properties: + containerPort: + description: ContainerPort is the port on the nginx + container to map to the HostPort. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + port: + description: Port to expose on the host. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - containerPort + - port + type: object + type: array image: description: Image is the NGINX image to use. properties: @@ -4052,6 +4077,31 @@ spec: description: Debug enables debugging for NGINX by using the nginx-debug binary. type: boolean + hostPorts: + description: HostPorts are the list of ports to expose + on the host. + items: + description: HostPort exposes an nginx container port + on the host. + properties: + containerPort: + description: ContainerPort is the port on the nginx + container to map to the HostPort. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + port: + description: Port to expose on the host. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - containerPort + - port + type: object + type: array image: description: Image is the NGINX image to use. properties: @@ -7470,18 +7520,17 @@ spec: automatically if required. The default NodePort range enforced by Kubernetes is 30000-32767. properties: listenerPort: - description: |- - ListenerPort is the Gateway listener port that this NodePort maps to. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 + description: ListenerPort is the Gateway listener port + that this NodePort maps to. format: int32 + maximum: 65535 + minimum: 1 type: integer port: - description: |- - Port is the NodePort to expose. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 + description: Port is the NodePort to expose. format: int32 + maximum: 65535 + minimum: 1 type: integer required: - listenerPort diff --git a/internal/controller/provisioner/objects.go b/internal/controller/provisioner/objects.go index 1fc2b43bcd..5c3b8501c6 100644 --- a/internal/controller/provisioner/objects.go +++ b/internal/controller/provisioner/objects.go @@ -788,6 +788,15 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec( container.Command = append(container.Command, "/agent/entrypoint.sh") container.Args = append(container.Args, "debug") } + + for _, hostPort := range containerSpec.HostPorts { + for i, port := range container.Ports { + if hostPort.ContainerPort == port.ContainerPort { + container.Ports[i].HostPort = hostPort.Port + } + } + } + spec.Spec.Containers[0] = container } } diff --git a/internal/controller/provisioner/objects_test.go b/internal/controller/provisioner/objects_test.go index 96710f8902..632b5c437c 100644 --- a/internal/controller/provisioner/objects_test.go +++ b/internal/controller/provisioner/objects_test.go @@ -257,6 +257,11 @@ func TestBuildNginxResourceObjects_NginxProxyConfig(t *testing.T) { Name: "gw", Namespace: "default", }, + Spec: gatewayv1.GatewaySpec{ + Listeners: []gatewayv1.Listener{ + {Name: "port-8443", Port: 8443, Protocol: "tcp"}, + }, + }, } resourceName := "gw-nginx" @@ -293,6 +298,7 @@ func TestBuildNginxResourceObjects_NginxProxyConfig(t *testing.T) { corev1.ResourceCPU: resource.Quantity{Format: "100m"}, }, }, + HostPorts: []ngfAPIv1alpha2.HostPort{{ContainerPort: int32(8443), Port: int32(8443)}}, }, }, }, @@ -344,6 +350,12 @@ func TestBuildNginxResourceObjects_NginxProxyConfig(t *testing.T) { g.Expect(container.ImagePullPolicy).To(Equal(corev1.PullAlways)) g.Expect(container.Resources.Limits).To(HaveKey(corev1.ResourceCPU)) g.Expect(container.Resources.Limits[corev1.ResourceCPU].Format).To(Equal(resource.Format("100m"))) + + g.Expect(container.Ports).To(ContainElement(corev1.ContainerPort{ + ContainerPort: 8443, + Name: "port-8443", + HostPort: 8443, + })) } func TestBuildNginxResourceObjects_Plus(t *testing.T) {