Skip to content

Commit 4e58b66

Browse files
committed
fix: namespace filtering
1 parent d62610d commit 4e58b66

File tree

3 files changed

+200
-20
lines changed

3 files changed

+200
-20
lines changed

internal/provider/provider.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
log "github.com/sirupsen/logrus"
88
dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1"
9-
"sigs.k8s.io/controller-runtime/pkg/client"
109

1110
"sigs.k8s.io/external-dns/endpoint"
1211
"sigs.k8s.io/external-dns/plan"
@@ -49,7 +48,8 @@ func (p *Provider) Start(ctx context.Context) error {
4948
}
5049

5150
// Records returns the current DNS records from DNSRecordSet resources across
52-
// all zone sources.
51+
// all zone sources. Only records in namespaces with discovered zones are
52+
// returned, ensuring namespace-level filtering is respected.
5353
func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
5454
p.logger.Debug("Fetching DNS records from all zone sources")
5555

@@ -60,14 +60,22 @@ func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
6060
var allEndpoints []*endpoint.Endpoint
6161

6262
for _, src := range p.registry.Sources() {
63+
zoneNamespaces := src.GetZoneNamespaces()
64+
6365
recordSets, err := src.RecordSets().List(ctx, p.ownerID)
6466
if err != nil {
6567
p.logger.WithError(err).Errorf("Failed to list records from source %q", src.Name())
6668
continue
6769
}
6870

6971
for _, rs := range recordSets {
70-
zone := p.resolveZoneForRecordSet(ctx, rs, src)
72+
if !zoneNamespaces[rs.Namespace] {
73+
p.logger.Debugf("Skipping record %s/%s: namespace not in discovered zone set",
74+
rs.Namespace, rs.Name)
75+
continue
76+
}
77+
78+
zone := p.resolveZoneForRecordSet(rs, src)
7179
if zone == nil {
7280
continue
7381
}
@@ -87,25 +95,17 @@ func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
8795
return allEndpoints, nil
8896
}
8997

90-
// resolveZoneForRecordSet attempts to find the zone that owns a record set,
91-
// first via the registry cache, then via a direct API lookup.
92-
func (p *Provider) resolveZoneForRecordSet(ctx context.Context, rs *dnsv1alpha1.DNSRecordSet, src *ZoneSource) *dnsv1alpha1.DNSZone {
93-
match, err := p.registry.GetZoneForDomain(rs.Spec.DNSZoneRef.Name)
94-
if err == nil {
95-
return match.Zone
96-
}
97-
98-
var zoneObj dnsv1alpha1.DNSZone
99-
if err := src.Client().Get(ctx, client.ObjectKey{
100-
Name: rs.Spec.DNSZoneRef.Name,
101-
Namespace: rs.Namespace,
102-
}, &zoneObj); err != nil {
103-
p.logger.Warnf("Failed to get zone %s for record set %s/%s: %v",
104-
rs.Spec.DNSZoneRef.Name, rs.Namespace, rs.Name, err)
98+
// resolveZoneForRecordSet looks up the zone for a record set from the source's
99+
// zone cache using the DNSZoneRef object name and namespace. No API fallback is
100+
// performed — if the zone isn't in the cache, the record is skipped.
101+
func (p *Provider) resolveZoneForRecordSet(rs *dnsv1alpha1.DNSRecordSet, src *ZoneSource) *dnsv1alpha1.DNSZone {
102+
zone := src.GetZoneByRef(rs.Spec.DNSZoneRef.Name, rs.Namespace)
103+
if zone == nil {
104+
p.logger.Warnf("Zone %s not found in cache for record %s/%s",
105+
rs.Spec.DNSZoneRef.Name, rs.Namespace, rs.Name)
105106
RecordTranslationError("zone_not_found")
106-
return nil
107107
}
108-
return &zoneObj
108+
return zone
109109
}
110110

111111
// ApplyChanges applies the given changes to DNS records, routing each operation

internal/provider/provider_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,159 @@ func TestProvider_NewProvider(t *testing.T) {
372372
})
373373
}
374374

375+
func TestProvider_Records_NamespaceLabelSelector_FiltersCorrectly(t *testing.T) {
376+
ctx := context.Background()
377+
378+
objects := []runtime.Object{
379+
&corev1.Namespace{
380+
ObjectMeta: metav1.ObjectMeta{
381+
Name: "managed-ns",
382+
Labels: map[string]string{"datum.net/managed-dns": "true"},
383+
},
384+
},
385+
&corev1.Namespace{
386+
ObjectMeta: metav1.ObjectMeta{Name: "unmanaged-ns"},
387+
},
388+
// Zone in labeled namespace
389+
&dnsv1alpha1.DNSZone{
390+
ObjectMeta: metav1.ObjectMeta{Name: "managed-com", Namespace: "managed-ns"},
391+
Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "managed.com"},
392+
},
393+
// Zone in unlabeled namespace — should NOT be discovered
394+
&dnsv1alpha1.DNSZone{
395+
ObjectMeta: metav1.ObjectMeta{Name: "unmanaged-com", Namespace: "unmanaged-ns"},
396+
Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "unmanaged.com"},
397+
},
398+
// Record in labeled namespace (owned by us)
399+
&dnsv1alpha1.DNSRecordSet{
400+
ObjectMeta: metav1.ObjectMeta{
401+
Name: "app-a-managed", Namespace: "managed-ns",
402+
Labels: map[string]string{LabelOwner: "test-owner", LabelManagedBy: ManagedByValue},
403+
},
404+
Spec: dnsv1alpha1.DNSRecordSetSpec{
405+
DNSZoneRef: corev1.LocalObjectReference{Name: "managed-com"},
406+
RecordType: dnsv1alpha1.RRTypeA,
407+
Records: []dnsv1alpha1.RecordEntry{{Name: "app", A: &dnsv1alpha1.ARecordSpec{Content: "192.0.2.1"}}},
408+
},
409+
},
410+
// Record in unlabeled namespace (owned by us) — should NOT be returned
411+
&dnsv1alpha1.DNSRecordSet{
412+
ObjectMeta: metav1.ObjectMeta{
413+
Name: "app-a-unmanaged", Namespace: "unmanaged-ns",
414+
Labels: map[string]string{LabelOwner: "test-owner", LabelManagedBy: ManagedByValue},
415+
},
416+
Spec: dnsv1alpha1.DNSRecordSetSpec{
417+
DNSZoneRef: corev1.LocalObjectReference{Name: "unmanaged-com"},
418+
RecordType: dnsv1alpha1.RRTypeA,
419+
Records: []dnsv1alpha1.RecordEntry{{Name: "app", A: &dnsv1alpha1.ARecordSpec{Content: "10.0.0.1"}}},
420+
},
421+
},
422+
}
423+
424+
scheme := runtime.NewScheme()
425+
require.NoError(t, dnsv1alpha1.AddToScheme(scheme))
426+
require.NoError(t, corev1.AddToScheme(scheme))
427+
428+
fakeClient := fake.NewClientBuilder().
429+
WithScheme(scheme).
430+
WithRuntimeObjects(objects...).
431+
Build()
432+
433+
config := &Config{DryRun: false}
434+
logger := logrus.New()
435+
logger.SetLevel(logrus.ErrorLevel)
436+
437+
src := NewZoneSource("test", fakeClient, ZoneSourceConfig{
438+
NamespaceLabelSelector: "datum.net/managed-dns=true",
439+
}, config, logger)
440+
441+
p, err := NewProvider(config, "test-owner", []*ZoneSource{src})
442+
require.NoError(t, err)
443+
require.NoError(t, p.registry.Refresh(ctx))
444+
445+
// Zone discovery should only find managed.com
446+
zones := p.registry.ListZones()
447+
assert.Len(t, zones, 1, "only zones from labeled namespaces should be discovered")
448+
assert.Equal(t, "managed.com", zones[0].Spec.DomainName)
449+
450+
// Domain filter should only include managed.com
451+
filter := p.GetDomainFilter()
452+
assert.True(t, filter.Match("app.managed.com"))
453+
assert.False(t, filter.Match("app.unmanaged.com"))
454+
455+
// Records() must only return records from labeled namespaces
456+
endpoints, err := p.Records(ctx)
457+
require.NoError(t, err)
458+
require.Len(t, endpoints, 1, "Records() should only return records from labeled namespaces")
459+
assert.Equal(t, "app.managed.com", endpoints[0].DNSName)
460+
assert.Equal(t, endpoint.Targets{"192.0.2.1"}, endpoints[0].Targets)
461+
}
462+
463+
func TestProvider_Records_NamespaceFlag_FiltersCorrectly(t *testing.T) {
464+
ctx := context.Background()
465+
466+
objects := []runtime.Object{
467+
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "watched-ns"}},
468+
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "other-ns"}},
469+
&dnsv1alpha1.DNSZone{
470+
ObjectMeta: metav1.ObjectMeta{Name: "watched-com", Namespace: "watched-ns"},
471+
Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "watched.com"},
472+
},
473+
&dnsv1alpha1.DNSZone{
474+
ObjectMeta: metav1.ObjectMeta{Name: "other-com", Namespace: "other-ns"},
475+
Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "other.com"},
476+
},
477+
&dnsv1alpha1.DNSRecordSet{
478+
ObjectMeta: metav1.ObjectMeta{
479+
Name: "app-a-watched", Namespace: "watched-ns",
480+
Labels: map[string]string{LabelOwner: "test-owner", LabelManagedBy: ManagedByValue},
481+
},
482+
Spec: dnsv1alpha1.DNSRecordSetSpec{
483+
DNSZoneRef: corev1.LocalObjectReference{Name: "watched-com"},
484+
RecordType: dnsv1alpha1.RRTypeA,
485+
Records: []dnsv1alpha1.RecordEntry{{Name: "app", A: &dnsv1alpha1.ARecordSpec{Content: "192.0.2.1"}}},
486+
},
487+
},
488+
&dnsv1alpha1.DNSRecordSet{
489+
ObjectMeta: metav1.ObjectMeta{
490+
Name: "app-a-other", Namespace: "other-ns",
491+
Labels: map[string]string{LabelOwner: "test-owner", LabelManagedBy: ManagedByValue},
492+
},
493+
Spec: dnsv1alpha1.DNSRecordSetSpec{
494+
DNSZoneRef: corev1.LocalObjectReference{Name: "other-com"},
495+
RecordType: dnsv1alpha1.RRTypeA,
496+
Records: []dnsv1alpha1.RecordEntry{{Name: "app", A: &dnsv1alpha1.ARecordSpec{Content: "10.0.0.1"}}},
497+
},
498+
},
499+
}
500+
501+
scheme := runtime.NewScheme()
502+
require.NoError(t, dnsv1alpha1.AddToScheme(scheme))
503+
require.NoError(t, corev1.AddToScheme(scheme))
504+
505+
fakeClient := fake.NewClientBuilder().
506+
WithScheme(scheme).
507+
WithRuntimeObjects(objects...).
508+
Build()
509+
510+
config := &Config{DryRun: false}
511+
logger := logrus.New()
512+
logger.SetLevel(logrus.ErrorLevel)
513+
514+
src := NewZoneSource("test", fakeClient, ZoneSourceConfig{
515+
Namespace: "watched-ns",
516+
}, config, logger)
517+
518+
p, err := NewProvider(config, "test-owner", []*ZoneSource{src})
519+
require.NoError(t, err)
520+
require.NoError(t, p.registry.Refresh(ctx))
521+
522+
endpoints, err := p.Records(ctx)
523+
require.NoError(t, err)
524+
require.Len(t, endpoints, 1, "Records() should only return records from the watched namespace")
525+
assert.Equal(t, "app.watched.com", endpoints[0].DNSName)
526+
}
527+
375528
func TestProvider_OwnershipConflict(t *testing.T) {
376529
objects := []runtime.Object{
377530
&dnsv1alpha1.DNSZone{

internal/provider/zonesource.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,33 @@ func (s *ZoneSource) GetZones() map[string]*dnsv1alpha1.DNSZone {
182182
return out
183183
}
184184

185+
// GetZoneByRef looks up a cached zone by its Kubernetes object name and
186+
// namespace. Returns nil if no matching zone is found in the cache.
187+
func (s *ZoneSource) GetZoneByRef(name, namespace string) *dnsv1alpha1.DNSZone {
188+
s.mu.RLock()
189+
defer s.mu.RUnlock()
190+
191+
for _, zone := range s.cache {
192+
if zone.Name == name && zone.Namespace == namespace {
193+
return zone
194+
}
195+
}
196+
return nil
197+
}
198+
199+
// GetZoneNamespaces returns the set of namespaces that contain discovered
200+
// zones. Only records in these namespaces should be considered managed.
201+
func (s *ZoneSource) GetZoneNamespaces() map[string]bool {
202+
s.mu.RLock()
203+
defer s.mu.RUnlock()
204+
205+
ns := make(map[string]bool)
206+
for _, zone := range s.cache {
207+
ns[zone.Namespace] = true
208+
}
209+
return ns
210+
}
211+
185212
// Refresh forces an immediate zone cache refresh.
186213
func (s *ZoneSource) Refresh(ctx context.Context) error {
187214
return s.refresh(ctx)

0 commit comments

Comments
 (0)