/* Copyright 2026 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "testing" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/endpoint" logtest "sigs.k8s.io/external-dns/internal/testutils/log" ) func TestOwnerMismatchMetric(t *testing.T) { currentA := &endpoint.Endpoint{ DNSName: "example.domain.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, Labels: map[string]string{ endpoint.OwnerLabelKey: "other-owner", }, } desiredCname := &endpoint.Endpoint{ DNSName: "example.domain.com", Targets: endpoint.Targets{"target.example.com"}, RecordType: endpoint.RecordTypeCNAME, Labels: map[string]string{ endpoint.OwnerLabelKey: "my-owner", }, } p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: []*endpoint.Endpoint{currentA}, Desired: []*endpoint.Endpoint{desiredCname}, ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, OwnerID: "my-owner", } changes := p.Calculate().Changes assert.Empty(t, changes.Create, "expected no creates due to owner mismatch") testutils.TestHelperVerifyMetricsGaugeVectorWithLabels( t, 1.0, registryOwnerMismatchPerSync.Gauge, map[string]string{ "record_type": endpoint.RecordTypeA, "foreign_owner": "other-owner", "domain": "domain.com", }, ) } // TestCalculateOwnerMismatchDetection verifies that owner mismatch is detected // when desired endpoints want to create new record types on DNS names // that have current records owned by a different owner. func TestCalculateOwnerMismatchDetection(t *testing.T) { current := testutils.GenerateTestEndpointsWithDistribution( map[string]int{endpoint.RecordTypeA: 10}, map[string]int{"example.com": 1}, map[string]int{"other-owner": 1}, ) // Create desired endpoints: same DNS names but with different type records (new type triggers Create) var desired []*endpoint.Endpoint for _, ep := range current { desired = append(desired, &endpoint.Endpoint{ DNSName: ep.DNSName, Targets: endpoint.Targets{"abrakadabra"}, RecordType: endpoint.RecordTypeTXT, RecordTTL: 300, }) } p := &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: endpoint.KnownRecordTypes, OwnerID: "my-owner", } hook := logtest.LogsUnderTestWithLogLevel(log.DebugLevel, t) changes := p.Calculate().Changes assert.Empty(t, changes.Create, "expected no creates due to owner mismatch") logtest.TestHelperLogContains("owner id does not match for one or more items to create", hook, t) } func TestOwnerMismatchMetricDistribution(t *testing.T) { p := newOwnerMismatchFixture() p.Calculate() testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 44, registryOwnerMismatchPerSync.Gauge, map[string]string{"record_type": endpoint.RecordTypeSRV}) testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 41, registryOwnerMismatchPerSync.Gauge, map[string]string{"foreign_owner": "owner1"}) testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 125, registryOwnerMismatchPerSync.Gauge, map[string]string{"owner": "my-owner"}) testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 21, registryOwnerMismatchPerSync.Gauge, map[string]string{"foreign_owner": "owner1", "domain": "open.net"}) testutils.TestHelperVerifyMetricsGaugeVectorWithLabels(t, 2, registryOwnerMismatchPerSync.Gauge, map[string]string{"record_type": endpoint.RecordTypeCNAME, "foreign_owner": "owner1", "domain": "open.net"}) } func BenchmarkOwnerMismatchMetricDistribution(b *testing.B) { p := newOwnerMismatchFixture(1000) for b.Loop() { p.Calculate() } } func newOwnerMismatchFixture(scale ...int) *Plan { factor := 1 if len(scale) > 0 && scale[0] > 1 { factor = scale[0] } current := testutils.GenerateTestEndpointsWithDistribution( map[string]int{ endpoint.RecordTypeA: 12 * factor, endpoint.RecordTypeAAAA: 27 * factor, endpoint.RecordTypeCNAME: 42 * factor, endpoint.RecordTypeSRV: 44 * factor, }, map[string]int{ "example.com": 1, "tld.org": 2, "open.net": 3, }, map[string]int{"owner1": 1, "owner2": 1, "owner3": 1}, ) var desired []*endpoint.Endpoint for _, ep := range current { desired = append(desired, &endpoint.Endpoint{ DNSName: ep.DNSName, Targets: endpoint.Targets{"txt-value"}, RecordType: endpoint.RecordTypeTXT, RecordTTL: 300, }) } return &Plan{ Policies: []Policy{&SyncPolicy{}}, Current: current, Desired: desired, ManagedRecords: endpoint.KnownRecordTypes, OwnerID: "my-owner", } } func TestFlushOwnerMismatch(t *testing.T) { tests := []struct { name string owner string current *endpoint.Endpoint calls int expected float64 expectedTags map[string]string }{ { name: "handles_missing_foreign_owner_label", owner: "me", current: &endpoint.Endpoint{ DNSName: "sub.domain.net", RecordType: endpoint.RecordTypeTXT, Labels: map[string]string{}, }, calls: 1, expected: 1.0, expectedTags: map[string]string{ "record_type": endpoint.RecordTypeTXT, "owner": "me", "foreign_owner": "", "domain": "domain.net", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { registryOwnerMismatchPerSync.Gauge.Reset() for range tt.calls { recordOwnerMismatch(tt.owner, tt.current) } testutils.TestHelperVerifyMetricsGaugeVectorWithLabels( t, tt.expected, registryOwnerMismatchPerSync.Gauge, tt.expectedTags, ) }) } }