Repository: kubernetes-sigs/external-dns (language: go)

Issue:
Panic crash on unexpected content in TXT record on google provider
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

-->

**What happened**:
ExternalDNS crashed on unexpected TXT record returned by google provider in zone it manages

Panic log:
```
time="2025-02-26T16:55:30Z" level=info msg="config: {APIServerURL: KubeConfig: RequestTimeout:30s DefaultTargets:[] GlooNamespaces:[gloo-system] SkipperRouteGroupVersion:zalando.org/v1 Sources:[service ingress] Namespace: AnnotationFilter: LabelFilter: IngressClassNames:[] FQDNTemplate: CombineFQDNAndAnnotation:false IgnoreHostnameAnnotation:false IgnoreIngressTLSSpec:false IgnoreIngressRulesSpec:false GatewayNamespace: GatewayLabelFilter: Compatibility: PublishInternal:false PublishHostIP:false AlwaysPublishNotReadyAddresses:false ConnectorSourceServer:localhost:8080 Provider:google ProviderCacheTime:0s GoogleProject:d360-devops-tests GoogleBatchChangeSize:1000 GoogleBatchChangeInterval:1s GoogleZoneVisibility: DomainFilter:[dev-apps-gcloud-666.europe-central2-gcloud.example.com dev-apps-gcloud-666.example.com dev-apps-gcloud-666.europe-central2-gcloud.internal.example.com dev-apps-gcloud-666.mtls.internal.example.com] ExcludeDomains:[] RegexDomainFilter: RegexDomainExclusion: ZoneNameFilter:[] ZoneIDFilter:[] TargetNetFilter:[] ExcludeTargetNets:[] AlibabaCloudConfigFile:/etc/kubernetes/alibaba-cloud.json AlibabaCloudZoneType: AWSZoneType: AWSZoneTagFilter:[] AWSAssumeRole: AWSProfiles:[] AWSAssumeRoleExternalID: AWSBatchChangeSize:1000 AWSBatchChangeSizeBytes:32000 AWSBatchChangeSizeValues:1000 AWSBatchChangeInterval:1s AWSEvaluateTargetHealth:true AWSAPIRetries:3 AWSPreferCNAME:false AWSZoneCacheDuration:0s AWSSDServiceCleanup:false AWSSDCreateTag:map[] AWSZoneMatchParent:false AWSDynamoDBRegion: AWSDynamoDBTable:external-dns AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: AzureSubscriptionID: AzureUserAssignedIdentityClientID: AzureActiveDirectoryAuthorityHost: AzureZonesCacheDuration:0s CloudflareProxied:false CloudflareDNSRecordsPerPage:100 CloudflareRegionKey: CoreDNSPrefix:/skydns/ AkamaiServiceConsumerDomain: AkamaiClientToken: AkamaiClientSecret: AkamaiAccessToken: AkamaiEdgercPath: AkamaiEdgercSection: OCIConfigFile:/etc/kubernetes/oci.yaml OCICompartmentOCID: OCIAuthInstancePrincipal:false OCIZoneScope:GLOBAL OCIZoneCacheDuration:0s InMemoryZones:[] OVHEndpoint:ovh-eu OVHApiRateLimit:20 PDNSServer:http://localhost:8081 PDNSServerID:localhost PDNSAPIKey: PDNSSkipTLSVerify:false TLSCA: TLSClientCert: TLSClientCertKey: Policy:sync Registry:txt TXTOwnerID:default TXTPrefix: TXTSuffix: TXTEncryptEnabled:false TXTEncryptAESKey: Interval:1m0s MinEventSyncInterval:5s Once:false DryRun:false UpdateEvents:false LogFormat:text MetricsAddress::7979 LogLevel:debug TXTCacheInterval:0s TXTWildcardReplacement: ExoscaleEndpoint: ExoscaleAPIKey: ExoscaleAPISecret: ExoscaleAPIEnvironment:api ExoscaleAPIZone:ch-gva-2 CRDSourceAPIVersion:externaldns.k8s.io/v1alpha1 CRDSourceKind:DNSEndpoint ServiceTypeFilter:[] CFAPIEndpoint: CFUsername: CFPassword: ResolveServiceLoadBalancerHostname:false RFC2136Host: RFC2136Port:0 RFC2136Zone:[] RFC2136Insecure:false RFC2136GSSTSIG:false RFC2136CreatePTR:false RFC2136KerberosRealm: RFC2136KerberosUsername: RFC2136KerberosPassword: RFC2136TSIGKeyName: RFC2136TSIGSecret: RFC2136TSIGSecretAlg: RFC2136TAXFR:false RFC2136MinTTL:0s RFC2136BatchChangeSize:50 RFC2136UseTLS:false RFC2136SkipTLSVerify:false NS1Endpoint: NS1IgnoreSSL:false NS1MinTTLSeconds:0 TransIPAccountName: TransIPPrivateKeyFile: DigitalOceanAPIPageSize:50 ManagedDNSRecordTypes:[A AAAA CNAME] ExcludeDNSRecordTypes:[] GoDaddyAPIKey: GoDaddySecretKey: GoDaddyTTL:0 GoDaddyOTE:false OCPRouterName: IBMCloudProxied:false IBMCloudConfigFile:/etc/kubernetes/ibmcloud.json TencentCloudConfigFile:/etc/kubernetes/tencent-cloud.json TencentCloudZoneType: PiholeServer: PiholePassword: PiholeTLSInsecureSkipVerify:false PluralCluster: PluralProvider: WebhookProviderURL:http://localhost:8888 WebhookProviderReadTimeout:5s WebhookProviderWriteTimeout:10s WebhookServer:false TraefikDisableLegacy:false TraefikDisableNew:false NAT64Networks:[]}"
time="2025-02-26T16:55:30Z" level=info msg="Instantiating new Kubernetes client"
time="2025-02-26T16:55:30Z" level=debug msg="apiServerURL: "
time="2025-02-26T16:55:30Z" level=debug msg="kubeConfig: "
time="2025-02-26T16:55:30Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2025-02-26T16:55:30Z" level=info msg="Created Kubernetes client https://10.14.192.1:443"
time="2025-02-26T16:55:31Z" level=debug msg="Matching zones against domain filters: {[dev-apps-gcloud-666.europe-central2-gcloud.example.com dev-apps-gcloud-666.example.com dev-apps-gcloud-666.europe-central2-gcloud.internal.example.com dev-apps-gcloud-666.mtls.internal.example.com] [] <nil> <nil>}"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered jx.cicd.internal.example.com. (zone: cicd-internal-example-com) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-upgrade.europe-west3-gcloud.internal.example.com. (zone: picked-tick-internal) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-upgrade.europe-west3-gcloud.example.com. (zone: picked-tick-public) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-upgrade.example.com. (zone: picked-tick-public-short) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Matched dev-apps-gcloud-666.europe-central2-gcloud.internal.example.com. (zone: ruling-kite-internal) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Matched dev-apps-gcloud-666.mtls.internal.example.com. (zone: ruling-kite-internal-mtls) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Matched dev-apps-gcloud-666.europe-central2-gcloud.example.com. (zone: ruling-kite-public) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Matched dev-apps-gcloud-666.example.com. (zone: ruling-kite-public-short) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-777.europe-central2-gcloud.internal.example.com. (zone: skilled-bird-internal) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-777.mtls.internal.example.com. (zone: skilled-bird-internal-mtls) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-777.europe-central2-gcloud.example.com. (zone: skilled-bird-public) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-777.example.com. (zone: skilled-bird-public-short) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-888.europe-west3-gcloud.internal.example.com. (zone: social-kingfish-internal) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-888.mtls.internal.example.com. (zone: social-kingfish-internal-mtls) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-888.europe-west3-gcloud.example.com. (zone: social-kingfish-public) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Filtered dev-apps-gcloud-888.example.com. (zone: social-kingfish-public-short) (visibility: public)"
time="2025-02-26T16:55:31Z" level=debug msg="Considering zone: ruling-kite-internal (domain: dev-apps-gcloud-666.europe-central2-gcloud.internal.example.com.)"
time="2025-02-26T16:55:31Z" level=debug msg="Considering zone: ruling-kite-internal-mtls (domain: dev-apps-gcloud-666.mtls.internal.example.com.)"
time="2025-02-26T16:55:31Z" level=debug msg="Considering zone: ruling-kite-public (domain: dev-apps-gcloud-666.europe-central2-gcloud.example.com.)"
time="2025-02-26T16:55:31Z" level=debug msg="Considering zone: ruling-kite-public-short (domain: dev-apps-gcloud-666.example.com.)"
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
sigs.k8s.io/external-dns/registry.(*TXTRegistry).Records(0xc00098f6c0, {0x4599c58?, 0xc00090a370?})
        sigs.k8s.io/external-dns/registry/txt.go:133 +0xd51
sigs.k8s.io/external-dns/controller.(*Controller).RunOnce(0xc000c4ca80, {0x4599c58, 0xc00090a370})
        sigs.k8s.io/external-dns/controller/controller.go:212 +0xf8
sigs.k8s.io/external-dns/controller.(*Controller).Run(0xc000c4ca80, {0x4599c58, 0xc00090a370})
        sigs.k8s.io/external-dns/controller/controller.go:360 +0xc5
main.main()
        sigs.k8s.io/external-dns/main.go:434 +0x396f
```

DNS record on which externalDNS crashes:
```
❯ gcloud dns record-sets list --zone ruling-kite-public-short --name 'test.dev-apps-gcloud-666.example.com.'
NAME                                    TYPE  TTL  DATA
test.dev-apps-gcloud-666.example.com.  TXT   300  20.0: "test-dst"
```

```
❯ dig @ns-cloud-a1.googledomains.com. test.dev-apps-gcloud-666.example.com. -t txt

; <<>> DiG 9.18.33 <<>> @ns-cloud-a1.googledomains.com. test.dev-apps-gcloud-666.example.com. -t txt
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40507
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; COOKIE: bfba31b944a3d4bf01b4cd56d22f9098fc460ddd94c30939a608faf7d8 (good)
;; QUESTION SECTION:
;test.dev-apps-gcloud-666.example.com. IN TXT

;; ANSWER SECTION:
test.dev-apps-gcloud-666.example.com. 300 IN TXT "test-dst"

;; Query time: 107 msec
;; SERVER: 216.239.32.106#53(ns-cloud-a1.googledomains.com.) (UDP)
;; MSG SIZE  rcvd: 120
```


**What you expected to happen**:
ExternalDNS ignores unexpected TXT records and continues to work (and probably logs ignored records)  

**How to reproduce it (as minimally and precisely as possible)**:

Setup externalDNS to use google provider and create "weighted round robin" record like this
```
gcloud dns record-sets create 'test.dev-apps-gcloud-666.example.com.' --zone=ruling-kite-public-short --type=TXT --routing-policy-type=WRR --routing-policy-data="20=test-dst" --ttl=300
```

**Anything else we need to know?**:

**Environment**:

- External-DNS version (use `external-dns --version`): `registry.k8s.io/external-dns/external-dns:v0.15.1`
- DNS provider: `google`
- Others: CLI parameters
```
        - --metrics-address=:7979
        - --log-level=debug
        - --log-format=text
        - --domain-filter=dev-apps-gcloud-666.europe-central2-gcloud.example.com
        - --domain-filter=dev-apps-gcloud-666.example.com
        - --domain-filter=dev-apps-gcloud-666.europe-central2-gcloud.internal.example.com
        - --domain-filter=dev-apps-gcloud-666.mtls.internal.example.com
        - --policy=sync
        - --provider=google
        - --registry=txt
        - --interval=1m
        - --source=service
        - --source=ingress
        - --google-project=d360-devops-tests
        - --google-batch-change-size=1000
```



Discussion / hints:
/help
@ivankatliarchuk: 
	This request has been marked as needing help from a contributor.

### Guidelines
Please ensure that the issue body includes answers to the following questions:
- Why are we solving this issue?
- To address this issue, are there any code changes? If there are code changes, what needs to be done in the code and what places can the assignee treat as reference points?
- Does this issue have zero to low barrier of entry?
- How can the assignee reach out to you for help?


For more details on the requirements of such an issue, please see [here](https://git.k8s.io/community/contributors/guide/help-wanted.md) and ensure that they are met.

If this request no longer meets these requirements, the label can be removed
by commenting with the `/remove-help` command.


<details>

In response to [this](https://github.com/kubernetes-sigs/external-dns/issues/5129):

>/help


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
Note: It does not seem to support the `Routing policy` with Google Cloud DNS.

More context: if the A record has been updated to have a routing policy lets say Weighted Round Robin,
it throws error while updating the record
```
time="2025-02-26T18:33:29Z" level=fatal msg="Failed to do run once: googleapi: Error 400: Invalid value for 'entity.change.deletions[xxx.example.com.][A].rrdata': '<empty>', invalid" 
```

and here some more info:
```
# data type for the record with routing policy

{
  "name": "xx.example.com.",
  "routingPolicy": {
    "wrr": {
      "items": [
        {
          "rrdatas": [
            "34.xxx.xx.XxX"
          ],
          "weight": 50.0
        },
        {
          "rrdatas": [
            "34.xxx.xx.xx"
          ],
          "weight": 50.0
        }
      ]
    }
  },
  "ttl": 300,
  "type": "A"
}


# Record without routing policy
{
  "name": "xxx.example.com.",
  "rrdatas": [
    "34.xx.xx.xx"
  ],
  "ttl": 300,
  "type": "A"
}
```
/area provider/gcp

Fix patch:
diff --git a/registry/txt.go b/registry/txt.go
index 1ed7cb0b92..06a8314a78 100644
--- a/registry/txt.go
+++ b/registry/txt.go
@@ -146,6 +146,11 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
 			continue
 		}
 		// We simply assume that TXT records for the registry will always have only one target.
+		// If there are no targets (e.g for routing policy based records in google), direct targets will be empty
+		if len(record.Targets) == 0 {
+			log.Errorf("TXT record has no targets %s", record.DNSName)
+			continue
+		}
 		labels, err := endpoint.NewLabelsFromString(record.Targets[0], im.txtEncryptAESKey)
 		if errors.Is(err, endpoint.ErrInvalidHeritage) {
 			// if no heritage is found or it is invalid


Test patch:
diff --git a/registry/txt_test.go b/registry/txt_test.go
index cdd190b123..6cb598b25a 100644
--- a/registry/txt_test.go
+++ b/registry/txt_test.go
@@ -26,6 +26,8 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	log "github.com/sirupsen/logrus"
+
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/internal/testutils"
 	"sigs.k8s.io/external-dns/plan"
@@ -1798,3 +1800,40 @@ func TestApplyChangesWithNewFormatOnly(t *testing.T) {
 			"TXT record should have 'a-' prefix when using new format only")
 	}
 }
+
+func TestTXTRegistryRecordsWithEmptyTargets(t *testing.T) {
+	ctx := context.Background()
+	p := inmemory.NewInMemoryProvider()
+	p.CreateZone(testZone)
+	p.ApplyChanges(ctx, &plan.Changes{
+		Create: []*endpoint.Endpoint{
+			{
+				DNSName:    "empty-targets.test-zone.example.org",
+				RecordType: endpoint.RecordTypeTXT,
+				Targets:    endpoint.Targets{},
+			},
+			{
+				DNSName:    "valid-targets.test-zone.example.org",
+				RecordType: endpoint.RecordTypeTXT,
+				Targets:    endpoint.Targets{"target1"},
+			},
+		},
+	})
+
+	r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
+	b := testutils.LogsToBuffer(log.ErrorLevel, t)
+	records, err := r.Records(ctx)
+	require.NoError(t, err)
+
+	expectedRecords := []*endpoint.Endpoint{
+		{
+			DNSName:    "valid-targets.test-zone.example.org",
+			Targets:    endpoint.Targets{"target1"},
+			RecordType: endpoint.RecordTypeTXT,
+			Labels:     map[string]string{},
+		},
+	}
+
+	assert.True(t, testutils.SameEndpoints(records, expectedRecords))
+	assert.Contains(t, b.String(), "TXT record has no targets empty-targets.test-zone.example.org")
+}