Skip to content

Commit 1c640e4

Browse files
wenyingdcursoragent
andcommitted
feat(service): reconcile DNS records for LoadBalancer Services
- Reconcile DNS records based on the `nsx.vmware.com/hostname` annotation on LoadBalancer Services using VPCNetworkConfiguration allowed DNS zones. - Support multiple comma-separated FQDNs in the annotation. - Report DNSRecordReady condition for DNS zone validation errors and generic DNS build errors. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ab46578 commit 1c640e4

15 files changed

Lines changed: 1479 additions & 210 deletions

File tree

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) {
247247
subnetport.NewSubnetPortReconciler(mgr, subnetPortService, subnetService, vpcService, ipAddressAllocationService),
248248
pod.NewPodReconciler(mgr, subnetPortService, subnetService, vpcService, nodeService),
249249
networkpolicycontroller.NewNetworkPolicyReconciler(mgr, commonService, vpcService),
250-
service.NewServiceLbReconciler(mgr, commonService),
250+
service.NewServiceLbReconciler(mgr, commonService, dnsRecordService),
251251
subnetbindingcontroller.NewReconciler(mgr, subnetService, subnetBindingService),
252252
subnetipreservationcontroller.NewReconciler(mgr, subnetIPReservationService, subnetService),
253253
)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ require (
5656
require (
5757
github.com/gofrs/uuid v4.4.0+incompatible
5858
go.uber.org/mock v0.6.0
59+
sigs.k8s.io/gateway-api v1.5.1
5960
)
6061

6162
require (
@@ -113,7 +114,6 @@ require (
113114
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
114115
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
115116
github.com/onsi/ginkgo/v2 v2.28.1 // indirect
116-
github.com/onsi/gomega v1.39.1 // indirect
117117
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
118118
github.com/prometheus/client_model v0.6.2 // indirect
119119
github.com/prometheus/common v0.67.5 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUo
420420
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
421421
sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
422422
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
423+
sigs.k8s.io/gateway-api v1.5.1 h1:RqVRIlkhLhUO8wOHKTLnTJA6o/1un4po4/6M1nRzdd0=
424+
sigs.k8s.io/gateway-api v1.5.1/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o=
423425
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
424426
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
425427
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=

pkg/controllers/service/service_lb_controller.go

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,33 @@ package service
55

66
import (
77
"context"
8+
"fmt"
89
"time"
910

1011
v1 "k8s.io/api/core/v1"
1112
apierrors "k8s.io/apimachinery/pkg/api/errors"
13+
"k8s.io/apimachinery/pkg/types"
1214
apimachineryruntime "k8s.io/apimachinery/pkg/runtime"
1315
"k8s.io/apimachinery/pkg/util/version"
1416
clientset "k8s.io/client-go/kubernetes"
1517
"k8s.io/client-go/rest"
1618
"k8s.io/client-go/tools/record"
19+
"k8s.io/client-go/util/retry"
1720
ctrl "sigs.k8s.io/controller-runtime"
21+
"sigs.k8s.io/controller-runtime/pkg/builder"
1822
"sigs.k8s.io/controller-runtime/pkg/client"
1923
"sigs.k8s.io/controller-runtime/pkg/controller"
24+
"sigs.k8s.io/controller-runtime/pkg/handler"
25+
"sigs.k8s.io/controller-runtime/pkg/manager"
2026
"sigs.k8s.io/controller-runtime/pkg/webhook"
2127

28+
"github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1"
2229
"github.com/vmware-tanzu/nsx-operator/pkg/controllers/common"
2330
"github.com/vmware-tanzu/nsx-operator/pkg/logger"
2431
"github.com/vmware-tanzu/nsx-operator/pkg/metrics"
2532
_ "github.com/vmware-tanzu/nsx-operator/pkg/nsx/ratelimiter"
2633
servicecommon "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
34+
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/dns"
2735
)
2836

2937
var (
@@ -38,6 +46,7 @@ type ServiceLbReconciler struct {
3846
Client client.Client
3947
Scheme *apimachineryruntime.Scheme
4048
Service *servicecommon.Service
49+
DNS dns.DNSRecordProvider
4150
Recorder record.EventRecorder
4251
}
4352

@@ -53,6 +62,14 @@ func updateSuccess(r *ServiceLbReconciler, c context.Context, lbService *v1.Serv
5362
return err
5463
}
5564

65+
func (r *ServiceLbReconciler) deleteDNSForService(ctx context.Context, namespace, name string, op string) error {
66+
if _, err := r.DNS.DeleteRecordByOwnerNN(ctx, dns.ResourceKindService, namespace, name); err != nil {
67+
log.Error(err, "Failed to delete DNS records for Service", "Namespace", namespace, "Name", name, "Operation", op)
68+
return fmt.Errorf("deleting DNS records for %s: %w", op, err)
69+
}
70+
return nil
71+
}
72+
5673
func (r *ServiceLbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
5774
service := &v1.Service{}
5875
startTime := time.Now()
@@ -63,33 +80,43 @@ func (r *ServiceLbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
6380
if err := r.Client.Get(ctx, req.NamespacedName, service); err != nil {
6481
if apierrors.IsNotFound(err) {
6582
log.Info("Not found LB service", "req", req.NamespacedName)
66-
return ResultNormal, client.IgnoreNotFound(err)
83+
if err := r.deleteDNSForService(ctx, req.Namespace, req.Name, "deleted Service"); err != nil {
84+
return common.ResultRequeueAfter10sec, err
85+
}
86+
return ResultNormal, nil
6787
}
6888
log.Error(err, "Failed to fetch LB service", "req", req.NamespacedName)
6989
return common.ResultRequeueAfter10sec, err
7090
}
7191

72-
if service.Spec.Type == v1.ServiceTypeLoadBalancer {
73-
log.Info("Reconciling LB service", "LBService", req.NamespacedName)
74-
log.Debug("Reconciling LB Service", "name", service.Name, "version", service.ResourceVersion, "status", service.Status)
75-
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerSyncTotal, MetricResType)
76-
77-
if service.ObjectMeta.DeletionTimestamp.IsZero() {
78-
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerUpdateTotal, MetricResType)
79-
err := updateSuccess(r, ctx, service)
80-
if err != nil {
81-
log.Error(err, "Failed to update LB service", "Name", service.Name, "Namespace", service.Namespace)
82-
return common.ResultRequeueAfter10sec, err
83-
}
92+
if service.Spec.Type != v1.ServiceTypeLoadBalancer || !service.ObjectMeta.DeletionTimestamp.IsZero() {
93+
// Try to delete DNS records for Service when it is not a LoadBalancer or is marked for deletion
94+
if err := r.clearDNSAndConditionForService(ctx, req.NamespacedName, "non-LB or terminating Service"); err != nil {
95+
return common.ResultRequeueAfter10sec, err
8496
}
97+
return ResultNormal, nil
98+
}
99+
100+
log.Info("Reconciling LB service", "LBService", req.NamespacedName)
101+
log.Debug("Reconciling LB Service", "name", service.Name, "version", service.ResourceVersion, "status", service.Status)
102+
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerSyncTotal, MetricResType)
103+
104+
if err := r.reconcileLoadBalancerServiceDNS(ctx, service); err != nil {
105+
log.Error(err, "Failed to reconcile DNS for LoadBalancer Service", "Name", service.Name, "Namespace", service.Namespace)
106+
return common.ResultRequeueAfter10sec, fmt.Errorf("reconciling DNS: %w", err)
107+
}
108+
109+
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerUpdateTotal, MetricResType)
110+
if err := updateSuccess(r, ctx, service); err != nil {
111+
log.Error(err, "Failed to update LB service", "Name", service.Name, "Namespace", service.Namespace)
112+
return common.ResultRequeueAfter10sec, fmt.Errorf("updating LB service: %w", err)
85113
}
86114

87115
return ResultNormal, nil
88116
}
89117

90118
func (r *ServiceLbReconciler) setServiceLbStatus(ctx context.Context, lbService *v1.Service) error {
91119
ipMode := v1.LoadBalancerIPModeProxy
92-
statusUpdated := false
93120
// If nsx.vmware.com/ingress-ip-mode label with values proxy or vip,
94121
// the LoadBalancer service ipMode status would be set to whatever the label is set to,
95122
// Otherwise, it's set to Proxy by default when unset or other invalid values.
@@ -98,34 +125,48 @@ func (r *ServiceLbReconciler) setServiceLbStatus(ctx context.Context, lbService
98125
ipMode = v1.LoadBalancerIPModeVIP
99126
}
100127
}
101-
for i, ing := range lbService.Status.LoadBalancer.Ingress {
102-
if ing.IP != "" {
103-
if ing.IPMode == nil || *(ing.IPMode) != ipMode {
104-
lbService.Status.LoadBalancer.Ingress[i].IPMode = &ipMode
105-
statusUpdated = true
128+
129+
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
130+
svc := &v1.Service{}
131+
if err := r.Client.Get(ctx, types.NamespacedName{Name: lbService.Name, Namespace: lbService.Namespace}, svc); err != nil {
132+
return client.IgnoreNotFound(err)
133+
}
134+
135+
statusUpdated := false
136+
for i, ing := range svc.Status.LoadBalancer.Ingress {
137+
if ing.IP != "" {
138+
if ing.IPMode == nil || *(ing.IPMode) != ipMode {
139+
svc.Status.LoadBalancer.Ingress[i].IPMode = &ipMode
140+
statusUpdated = true
141+
}
106142
}
107143
}
108-
}
109144

110-
if statusUpdated {
111-
err := r.Client.Status().Update(ctx, lbService)
112-
if err != nil {
113-
log.Error(err, "Failed to update LB service status ipMode", "Name", lbService.Name, "Namespace", lbService.Namespace, "ipMode", ipMode)
114-
return err
145+
if statusUpdated {
146+
err := r.Client.Status().Update(ctx, svc)
147+
if err != nil {
148+
log.Error(err, "Failed to update LB service status ipMode", "Name", svc.Name, "Namespace", svc.Namespace, "ipMode", ipMode)
149+
return err
150+
}
151+
log.Info("Updated LB service status ipMode", "Name", svc.Name, "Namespace", svc.Namespace, "ipMode", ipMode)
115152
}
116-
log.Info("Updated LB service status ipMode", "Name", lbService.Name, "Namespace", lbService.Namespace, "ipMode", ipMode)
117-
}
118-
return nil
153+
return nil
154+
})
119155
}
120156

121157
func (r *ServiceLbReconciler) setupWithManager(mgr ctrl.Manager) error {
122-
return ctrl.NewControllerManagedBy(mgr).
158+
b := ctrl.NewControllerManagedBy(mgr).
123159
For(&v1.Service{}).
160+
Watches(
161+
&v1alpha1.NetworkInfo{},
162+
handler.EnqueueRequestsFromMapFunc(r.enqueueLBServiceRequestsFromNetworkInfo),
163+
builder.WithPredicates(predicateNetworkInfoAllowedDNSDomainsChanged()),
164+
).
124165
WithOptions(
125166
controller.Options{
126167
MaxConcurrentReconciles: common.NumReconcile(),
127-
}).
128-
Complete(r)
168+
})
169+
return b.Complete(r)
129170
}
130171

131172
// Start setup manager
@@ -172,18 +213,36 @@ func (r *ServiceLbReconciler) StartController(mgr ctrl.Manager, _ webhook.Server
172213
log.Error(err, "Failed to create controller", "controller", "ServiceLb")
173214
return err
174215
}
216+
err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
217+
stop := make(chan bool)
218+
go func() {
219+
<-ctx.Done()
220+
close(stop)
221+
}()
222+
common.GenericGarbageCollector(stop, servicecommon.GCInterval, r.CollectGarbage)
223+
return nil
224+
}))
225+
if err != nil {
226+
log.Error(err, "Failed to add LB GC to manager")
227+
return err
228+
}
175229
return nil
176230
}
177231

178232
func (r *ServiceLbReconciler) CollectGarbage(ctx context.Context) error {
179-
return nil
233+
return r.collectDNSGarbage(ctx)
180234
}
181235

182-
func NewServiceLbReconciler(mgr ctrl.Manager, commonService servicecommon.Service) *ServiceLbReconciler {
236+
func NewServiceLbReconciler(mgr ctrl.Manager, commonService servicecommon.Service, dnsRecordService *dns.DNSRecordService) *ServiceLbReconciler {
183237
if isServiceLbStatusIpModeSupported(mgr.GetConfig()) {
238+
var dnsProv dns.DNSRecordProvider
239+
if dnsRecordService != nil {
240+
dnsProv = dnsRecordService
241+
}
184242
serviceLbReconciler := &ServiceLbReconciler{
185243
Client: mgr.GetClient(),
186244
Scheme: mgr.GetScheme(),
245+
DNS: dnsProv,
187246
Recorder: mgr.GetEventRecorderFor("serviceLb-controller"), //nolint:staticcheck // record.EventRecorder; StatusUpdater not on events.EventRecorder yet
188247
}
189248
serviceLbReconciler.Service = &commonService

0 commit comments

Comments
 (0)