Skip to content

Commit 50a8233

Browse files
committed
Preserve external controller fields
1 parent 5c78a60 commit 50a8233

File tree

12 files changed

+693
-8
lines changed

12 files changed

+693
-8
lines changed

apis/v1alpha2/nginxproxy_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,21 @@ type ServiceSpec struct {
758758
// +optional
759759
NodePorts []NodePort `json:"nodePorts,omitempty"`
760760

761+
// PreserveAnnotations specifies patterns of annotations that should be preserved during
762+
// service reconciliation. This allows external controllers (e.g., MetalLB, external-dns,
763+
// cloud provider load balancer controllers) to add operational annotations that NGF will
764+
// not remove. Supports both exact annotation keys (e.g., "metallb.universe.tf/loadBalancerIPs")
765+
// and glob patterns (e.g., "*.amazonaws.com/*", "external-dns.alpha.kubernetes.io/*").
766+
// NGF-managed annotations always take precedence and cannot be preserved by external controllers.
767+
// Each pattern must be a valid annotation key format or glob pattern.
768+
//
769+
// +optional
770+
// +kubebuilder:validation:MaxItems=32
771+
// +kubebuilder:validation:items:MinLength=1
772+
// +kubebuilder:validation:items:MaxLength=253
773+
// +listType=set
774+
PreserveAnnotations []string `json:"preserveAnnotations,omitempty"`
775+
761776
// Patches are custom patches to apply to the NGINX Service.
762777
//
763778
// +optional

apis/v1alpha2/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/nginx-gateway-fabric/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri
207207
| `certGenerator.ttlSecondsAfterFinished` | How long to wait after the cert generator job has finished before it is removed by the job controller. | int | `30` |
208208
| `clusterDomain` | The DNS cluster domain of your Kubernetes cluster. | string | `"cluster.local"` |
209209
| `gateways` | A list of Gateway objects. View https://gateway-api.sigs.k8s.io/reference/spec/#gateway for full Gateway reference. | list | `[]` |
210-
| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"autoscaling":{"enable":false},"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","nginxOneConsole":{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false},"patches":[],"plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","enforceInitialReport":true,"resolver":"","secretName":"nplus-license","skipVerify":false}}` |
210+
| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"autoscaling":{"enable":false},"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","nginxOneConsole":{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false},"patches":[],"plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"preserveAnnotations":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","enforceInitialReport":true,"resolver":"","secretName":"nplus-license","skipVerify":false}}` |
211211
| `nginx.autoscaling` | Autoscaling configuration for the NGINX data plane. | object | `{"enable":false}` |
212212
| `nginx.autoscaling.enable` | Enable or disable Horizontal Pod Autoscaler for the NGINX data plane. | bool | `false` |
213213
| `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 | `{}` |
@@ -230,13 +230,14 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri
230230
| `nginx.plus` | Is NGINX Plus image being used. | bool | `false` |
231231
| `nginx.pod` | The pod configuration for the NGINX data plane pod. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` |
232232
| `nginx.replicas` | The number of replicas of the NGINX Deployment. This value is ignored if autoscaling.enable is true. | int | `1` |
233-
| `nginx.service` | The service configuration for the NGINX data plane. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"type":"LoadBalancer"}` |
233+
| `nginx.service` | The service configuration for the NGINX data plane. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"preserveAnnotations":[],"type":"LoadBalancer"}` |
234234
| `nginx.service.externalTrafficPolicy` | The externalTrafficPolicy of the service. The value Local preserves the client source IP. | string | `"Local"` |
235235
| `nginx.service.loadBalancerClass` | LoadBalancerClass is the class of the load balancer implementation this Service belongs to. Requires nginx.service.type set to LoadBalancer. | string | `""` |
236236
| `nginx.service.loadBalancerIP` | The static IP address for the load balancer. Requires nginx.service.type set to LoadBalancer. | string | `""` |
237237
| `nginx.service.loadBalancerSourceRanges` | The IP ranges (CIDR) that are allowed to access the load balancer. Requires nginx.service.type set to LoadBalancer. | list | `[]` |
238238
| `nginx.service.nodePorts` | A list of NodePorts to expose on the NGINX data plane service. Each NodePort MUST map to a Gateway listener port, otherwise it will be ignored. The default NodePort range enforced by Kubernetes is 30000-32767. | list | `[]` |
239239
| `nginx.service.patches` | Custom patches to apply to the NGINX Service. | list | `[]` |
240+
| `nginx.service.preserveAnnotations` | Patterns of annotations that should be preserved during service reconciliation. This allows external controllers (e.g., MetalLB, external-dns, cloud provider load balancer controllers) to add operational annotations that NGF will not remove. Supports both exact annotation keys (e.g., "metallb.universe.tf/loadBalancerIPs") and glob patterns (e.g., "*.amazonaws.com/*", "external-dns.alpha.kubernetes.io/*"). NGF-managed annotations always take precedence and cannot be preserved by external controllers. | list | `[]` |
240241
| `nginx.service.type` | The type of service to create for the NGINX data plane. | string | `"LoadBalancer"` |
241242
| `nginx.usage.caSecretName` | The name of the Secret containing the NGINX Instance Manager CA certificate. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` |
242243
| `nginx.usage.clientSSLSecretName` | The name of the Secret containing the client certificate and key for authenticating with NGINX Instance Manager. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` |

charts/nginx-gateway-fabric/values.schema.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,19 @@
652652
"title": "patches",
653653
"type": "array"
654654
},
655+
"preserveAnnotations": {
656+
"description": "Patterns of annotations that should be preserved during service reconciliation.\nThis allows external controllers (e.g., MetalLB, external-dns, cloud provider load balancer controllers)\nto add operational annotations that NGF will not remove. Supports both exact annotation keys\n(e.g., \"metallb.universe.tf/loadBalancerIPs\") and glob patterns\n(e.g., \"*.amazonaws.com/*\", \"external-dns.alpha.kubernetes.io/*\").\nNGF-managed annotations always take precedence and cannot be preserved by external controllers.",
657+
"items": {
658+
"maxLength": 253,
659+
"minLength": 1,
660+
"required": [],
661+
"type": "string"
662+
},
663+
"maxItems": 32,
664+
"required": [],
665+
"title": "preserveAnnotations",
666+
"type": "array"
667+
},
655668
"type": {
656669
"default": "LoadBalancer",
657670
"description": "The type of service to create for the NGINX data plane.",

charts/nginx-gateway-fabric/values.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,23 @@ nginx:
645645
# - port: 30025
646646
# listenerPort: 80
647647

648+
# @schema
649+
# type: array
650+
# items:
651+
# type: string
652+
# minLength: 1
653+
# maxLength: 253
654+
# maxItems: 32
655+
# uniqueItems: true
656+
# @schema
657+
# -- Patterns of annotations that should be preserved during service reconciliation.
658+
# This allows external controllers (e.g., MetalLB, external-dns, cloud provider load balancer controllers)
659+
# to add operational annotations that NGF will not remove. Supports both exact annotation keys
660+
# (e.g., "metallb.universe.tf/loadBalancerIPs") and glob patterns
661+
# (e.g., "*.amazonaws.com/*", "external-dns.alpha.kubernetes.io/*").
662+
# NGF-managed annotations always take precedence and cannot be preserved by external controllers.
663+
preserveAnnotations: []
664+
648665
# -- Custom patches to apply to the NGINX Service.
649666
patches: []
650667
# -- Example:

config/crd/bases/gateway.nginx.org_nginxproxies.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7998,6 +7998,22 @@ spec:
79987998
x-kubernetes-preserve-unknown-fields: true
79997999
type: object
80008000
type: array
8001+
preserveAnnotations:
8002+
description: |-
8003+
PreserveAnnotations specifies patterns of annotations that should be preserved during
8004+
service reconciliation. This allows external controllers (e.g., MetalLB, external-dns,
8005+
cloud provider load balancer controllers) to add operational annotations that NGF will
8006+
not remove. Supports both exact annotation keys (e.g., "metallb.universe.tf/loadBalancerIPs")
8007+
and glob patterns (e.g., "*.amazonaws.com/*", "external-dns.alpha.kubernetes.io/*").
8008+
NGF-managed annotations always take precedence and cannot be preserved by external controllers.
8009+
Each pattern must be a valid annotation key format or glob pattern.
8010+
items:
8011+
maxLength: 253
8012+
minLength: 1
8013+
type: string
8014+
maxItems: 32
8015+
type: array
8016+
x-kubernetes-list-type: set
80018017
type:
80028018
default: LoadBalancer
80038019
description: ServiceType describes ingress method for the

deploy/crds.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8583,6 +8583,22 @@ spec:
85838583
x-kubernetes-preserve-unknown-fields: true
85848584
type: object
85858585
type: array
8586+
preserveAnnotations:
8587+
description: |-
8588+
PreserveAnnotations specifies patterns of annotations that should be preserved during
8589+
service reconciliation. This allows external controllers (e.g., MetalLB, external-dns,
8590+
cloud provider load balancer controllers) to add operational annotations that NGF will
8591+
not remove. Supports both exact annotation keys (e.g., "metallb.universe.tf/loadBalancerIPs")
8592+
and glob patterns (e.g., "*.amazonaws.com/*", "external-dns.alpha.kubernetes.io/*").
8593+
NGF-managed annotations always take precedence and cannot be preserved by external controllers.
8594+
Each pattern must be a valid annotation key format or glob pattern.
8595+
items:
8596+
maxLength: 253
8597+
minLength: 1
8598+
type: string
8599+
maxItems: 32
8600+
type: array
8601+
x-kubernetes-list-type: set
85868602
type:
85878603
default: LoadBalancer
85888604
description: ServiceType describes ingress method for the
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package provisioner
2+
3+
import (
4+
"encoding/json"
5+
"maps"
6+
"path/filepath"
7+
8+
"github.com/go-logr/logr"
9+
corev1 "k8s.io/api/core/v1"
10+
)
11+
12+
const (
13+
// internalPreservePatternsAnnotation is an internal annotation key used to temporarily
14+
// store preserve annotation patterns during object creation/update.
15+
// This annotation is removed during reconciliation after being used.
16+
internalPreservePatternsAnnotation = "gateway.nginx.org/internal-preserve-patterns"
17+
)
18+
19+
// preserveServiceAnnotations merges annotations from the existing service with new annotations,
20+
// preserving annotations that match the specified patterns.
21+
//
22+
// Priority (highest to lowest):
23+
// 1. Annotations in newAnnotations (NGF-managed)
24+
// 2. Annotations from existing service that match preservePatterns
25+
// 3. Annotations from existing service that don't match preservePatterns are removed.
26+
func preserveServiceAnnotations(
27+
existingService *corev1.Service,
28+
newAnnotations map[string]string,
29+
preservePatterns []string,
30+
logger logr.Logger,
31+
) map[string]string {
32+
if existingService == nil || len(existingService.Annotations) == 0 {
33+
return newAnnotations
34+
}
35+
36+
if len(preservePatterns) == 0 {
37+
return newAnnotations
38+
}
39+
40+
// Start with NGF-managed annotations
41+
result := make(map[string]string, len(newAnnotations))
42+
maps.Copy(result, newAnnotations)
43+
44+
// Preserve annotations from existing service that match patterns
45+
// but don't override NGF-managed annotations
46+
for key, value := range existingService.Annotations {
47+
// Skip if this annotation is already managed by NGF
48+
if _, ngfManaged := newAnnotations[key]; ngfManaged {
49+
continue
50+
}
51+
52+
// Check if annotation matches any preserve pattern
53+
if matchesAnyPattern(key, preservePatterns) {
54+
result[key] = value
55+
logger.V(1).Info("Preserving external annotation",
56+
"key", key,
57+
"value", value,
58+
"service", existingService.Name,
59+
"namespace", existingService.Namespace,
60+
)
61+
}
62+
}
63+
64+
return result
65+
}
66+
67+
// matchesAnyPattern checks if the annotation key matches any of the specified patterns.
68+
// Supports both exact matches and glob patterns (using filepath.Match).
69+
func matchesAnyPattern(annotationKey string, patterns []string) bool {
70+
for _, pattern := range patterns {
71+
// Try exact match first
72+
if annotationKey == pattern {
73+
return true
74+
}
75+
76+
// Try glob pattern match
77+
matched, err := filepath.Match(pattern, annotationKey)
78+
if err == nil && matched {
79+
return true
80+
}
81+
}
82+
83+
return false
84+
}
85+
86+
// storePreservePatterns stores preserve annotation patterns in the service's annotations
87+
// using an internal annotation key. This is used to pass the patterns to the setter function.
88+
func storePreservePatterns(service *corev1.Service, patterns []string) {
89+
if len(patterns) == 0 {
90+
return
91+
}
92+
93+
if service.Annotations == nil {
94+
service.Annotations = make(map[string]string)
95+
}
96+
97+
// Serialize patterns as JSON
98+
data, err := json.Marshal(patterns)
99+
if err != nil {
100+
// If marshaling fails, skip storing patterns
101+
return
102+
}
103+
104+
service.Annotations[internalPreservePatternsAnnotation] = string(data)
105+
}
106+
107+
// getPreservePatterns retrieves preserve annotation patterns from an annotation map.
108+
// Returns nil if no patterns are stored or if deserialization fails.
109+
func getPreservePatterns(annotations map[string]string) []string {
110+
if annotations == nil {
111+
return nil
112+
}
113+
114+
patternsJSON, exists := annotations[internalPreservePatternsAnnotation]
115+
if !exists {
116+
return nil
117+
}
118+
119+
// Deserialize patterns
120+
var patterns []string
121+
if err := json.Unmarshal([]byte(patternsJSON), &patterns); err != nil {
122+
return nil
123+
}
124+
125+
return patterns
126+
}

0 commit comments

Comments
 (0)