Skip to content

Commit 7dfcba8

Browse files
committed
feat(service): LoadBalancer Service DNS with VPC hostname validation
Add Service LB DNS reconciliation using DNSRecordProvider, ExternalDNS hostname admission helpers, and gomock DNSRecordProvider stubs. Wire ServiceLbReconciler with shared DNS record service in cmd.
1 parent e3d6c05 commit 7dfcba8

11 files changed

Lines changed: 1217 additions & 196 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
)

pkg/controllers/service/service_lb_controller.go

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,32 @@ 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"
1213
apimachineryruntime "k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/apimachinery/pkg/types"
15+
"k8s.io/apimachinery/pkg/util/sets"
1316
"k8s.io/apimachinery/pkg/util/version"
1417
clientset "k8s.io/client-go/kubernetes"
1518
"k8s.io/client-go/rest"
1619
"k8s.io/client-go/tools/record"
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"
2025
"sigs.k8s.io/controller-runtime/pkg/webhook"
2126

27+
"github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1"
2228
"github.com/vmware-tanzu/nsx-operator/pkg/controllers/common"
2329
"github.com/vmware-tanzu/nsx-operator/pkg/logger"
2430
"github.com/vmware-tanzu/nsx-operator/pkg/metrics"
2531
_ "github.com/vmware-tanzu/nsx-operator/pkg/nsx/ratelimiter"
2632
servicecommon "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
33+
"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/dns"
2734
)
2835

2936
var (
@@ -38,6 +45,7 @@ type ServiceLbReconciler struct {
3845
Client client.Client
3946
Scheme *apimachineryruntime.Scheme
4047
Service *servicecommon.Service
48+
DNS dns.DNSRecordProvider
4149
Recorder record.EventRecorder
4250
}
4351

@@ -63,25 +71,42 @@ func (r *ServiceLbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
6371
if err := r.Client.Get(ctx, req.NamespacedName, service); err != nil {
6472
if apierrors.IsNotFound(err) {
6573
log.Info("Not found LB service", "req", req.NamespacedName)
66-
return ResultNormal, client.IgnoreNotFound(err)
74+
if _, delErr := r.DNS.DeleteRecordByOwnerNN(ctx, dns.ResourceKindService, req.Namespace, req.Name); delErr != nil {
75+
log.Error(delErr, "Failed to delete DNS records for Service", "Namespace", req.Namespace, "Name", req.Name)
76+
return common.ResultRequeueAfter10sec, delErr
77+
}
78+
return ResultNormal, nil
6779
}
6880
log.Error(err, "Failed to fetch LB service", "req", req.NamespacedName)
6981
return common.ResultRequeueAfter10sec, err
7082
}
7183

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-
}
84+
if service.Spec.Type != v1.ServiceTypeLoadBalancer || !service.ObjectMeta.DeletionTimestamp.IsZero() {
85+
// Try to delete DNS records for Service when it is not a LoadBalancer or is marked for deletion
86+
if _, err := r.DNS.DeleteRecordByOwnerNN(ctx, dns.ResourceKindService, service.Namespace, service.Name); err != nil {
87+
log.Error(err, "Failed to delete DNS records for Service", "Namespace", service.Namespace, "Name", service.Name)
88+
return common.ResultRequeueAfter10sec, err
89+
}
90+
if uerr := r.removeServiceDNSReadyCondition(ctx, req.NamespacedName); uerr != nil {
91+
log.Error(uerr, "Failed to clear Service DNS Ready condition", "Service", req.NamespacedName.String())
92+
return common.ResultRequeueAfter10sec, uerr
8493
}
94+
return ResultNormal, nil
95+
}
96+
97+
log.Info("Reconciling LB service", "LBService", req.NamespacedName)
98+
log.Debug("Reconciling LB Service", "name", service.Name, "version", service.ResourceVersion, "status", service.Status)
99+
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerSyncTotal, MetricResType)
100+
101+
if err := r.reconcileLoadBalancerServiceDNS(ctx, service); err != nil {
102+
log.Error(err, "Failed to reconcile DNS for LoadBalancer Service", "Name", service.Name, "Namespace", service.Namespace)
103+
return common.ResultRequeueAfter10sec, err
104+
}
105+
106+
metrics.CounterInc(r.Service.NSXConfig, metrics.ControllerUpdateTotal, MetricResType)
107+
if err := updateSuccess(r, ctx, service); err != nil {
108+
log.Error(err, "Failed to update LB service", "Name", service.Name, "Namespace", service.Namespace)
109+
return common.ResultRequeueAfter10sec, err
85110
}
86111

87112
return ResultNormal, nil
@@ -119,13 +144,18 @@ func (r *ServiceLbReconciler) setServiceLbStatus(ctx context.Context, lbService
119144
}
120145

121146
func (r *ServiceLbReconciler) setupWithManager(mgr ctrl.Manager) error {
122-
return ctrl.NewControllerManagedBy(mgr).
147+
b := ctrl.NewControllerManagedBy(mgr).
123148
For(&v1.Service{}).
149+
Watches(
150+
&v1alpha1.NetworkInfo{},
151+
handler.EnqueueRequestsFromMapFunc(r.enqueueLBServiceRequestsFromNetworkInfo),
152+
builder.WithPredicates(predicateNetworkInfoAllowedDNSDomainsChanged()),
153+
).
124154
WithOptions(
125155
controller.Options{
126156
MaxConcurrentReconciles: common.NumReconcile(),
127-
}).
128-
Complete(r)
157+
})
158+
return b.Complete(r)
129159
}
130160

131161
// Start setup manager
@@ -172,18 +202,73 @@ func (r *ServiceLbReconciler) StartController(mgr ctrl.Manager, _ webhook.Server
172202
log.Error(err, "Failed to create controller", "controller", "ServiceLb")
173203
return err
174204
}
205+
go common.GenericGarbageCollector(make(chan bool), servicecommon.GCInterval, r.CollectGarbage)
175206
return nil
176207
}
177208

209+
// listLoadBalancerServicesWithDNSAnnotation returns Service NNs that should retain DNS rows (LB, not terminating, hostname annotation).
210+
func listLoadBalancerServicesWithDNSAnnotation(ctx context.Context, c client.Client) (sets.Set[types.NamespacedName], error) {
211+
svcList := &v1.ServiceList{}
212+
if err := c.List(ctx, svcList); err != nil {
213+
return nil, err
214+
}
215+
nnSet := sets.New[types.NamespacedName]()
216+
for i := range svcList.Items {
217+
svc := &svcList.Items[i]
218+
if svc.Spec.Type != v1.ServiceTypeLoadBalancer || !svc.ObjectMeta.DeletionTimestamp.IsZero() {
219+
continue
220+
}
221+
if len(parseDNSHostnamesFromServiceAnnotation(svc.GetAnnotations())) == 0 {
222+
continue
223+
}
224+
nnSet.Insert(types.NamespacedName{Namespace: svc.Namespace, Name: svc.Name})
225+
}
226+
return nnSet, nil
227+
}
228+
178229
func (r *ServiceLbReconciler) CollectGarbage(ctx context.Context) error {
230+
if r.DNS == nil {
231+
return nil
232+
}
233+
apiSet, err := listLoadBalancerServicesWithDNSAnnotation(ctx, r.Client)
234+
if err != nil {
235+
log.Error(err, "Service LB GC: failed to list Services")
236+
return err
237+
}
238+
ownersByKind := r.DNS.ListRecordOwnerResource()
239+
cachedServices := ownersByKind[dns.ResourceKindService]
240+
var errs []error
241+
for nn := range cachedServices {
242+
if apiSet.Has(nn) {
243+
continue
244+
}
245+
if _, err := r.DNS.DeleteRecordByOwnerNN(ctx, dns.ResourceKindService, nn.Namespace, nn.Name); err != nil {
246+
log.Error(err, "Service LB GC: failed to delete DNS records for Service owner missing from API or no longer eligible",
247+
"Namespace", nn.Namespace, "Name", nn.Name)
248+
errs = append(errs, err)
249+
continue
250+
}
251+
if err := r.removeServiceDNSReadyCondition(ctx, nn); err != nil {
252+
log.Error(err, "Service LB GC: failed to clear Service DNS Ready condition", "Namespace", nn.Namespace, "Name", nn.Name)
253+
errs = append(errs, err)
254+
}
255+
}
256+
if len(errs) > 0 {
257+
return fmt.Errorf("service LB garbage collection encountered %d error(s): %v", len(errs), errs)
258+
}
179259
return nil
180260
}
181261

182-
func NewServiceLbReconciler(mgr ctrl.Manager, commonService servicecommon.Service) *ServiceLbReconciler {
262+
func NewServiceLbReconciler(mgr ctrl.Manager, commonService servicecommon.Service, dnsRecordService *dns.DNSRecordService) *ServiceLbReconciler {
183263
if isServiceLbStatusIpModeSupported(mgr.GetConfig()) {
264+
var dnsProv dns.DNSRecordProvider
265+
if dnsRecordService != nil {
266+
dnsProv = dnsRecordService
267+
}
184268
serviceLbReconciler := &ServiceLbReconciler{
185269
Client: mgr.GetClient(),
186270
Scheme: mgr.GetScheme(),
271+
DNS: dnsProv,
187272
Recorder: mgr.GetEventRecorderFor("serviceLb-controller"),
188273
}
189274
serviceLbReconciler.Service = &commonService

0 commit comments

Comments
 (0)