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

Issue:
Escaped double quotes of target prevents creation of TXT, NAPTR and CAA records with DNSEndpoint
<!-- 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**: It is not possible to create NAPTR, TXT or CAA records using DNSEndpoint custom resource on both AWS and PowerDNS because double quotes are sent escaped to AWS/PowerDNS. While the API responses in the examples below are based on AWS, the same problem applies to PowerDNS.

**What you expected to happen**: It should be possible to create NAPTR, TXT or CAA records using the DNSEndpoint CR.

**How to reproduce it (as minimally and precisely as possible)**:

With quotes AWS will return an error because the NAPTR record is not in a RFC 2915 compliant and external dns escaped the quotes.
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: examplednsrecord-naptr
spec:
  endpoints:
  - dnsName: naptr.example.com
    recordTTL: 180
    recordType: NAPTR
    targets:
    - 1 1 "U" "123" "regular" myentry.example.com
```

Error:
```
time="2023-10-04T11:45:50Z" level=error msg="Failure in zone example.com. [Id: /hostedzone/____________] when submitting change batch: InvalidChangeBatch: [Invalid Resource Record: 'WARNING problem: NAPTRRRDATARegexpAndReplacementError (NAPTR record can have only one field in regexp and replacement) encountered with '1 1 \"U\" \"123\" \"regular\" myentry.example.com'']\n\tstatus code: 400, request id: ab86ccb1-3484-42f7-b7bc-794805f286c3"
```



Without quotes AWS will return an error because the NAPTR record is not in a RFC 2915 compliant.
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: examplednsrecord-naptr
spec:
  endpoints:
  - dnsName: naptr.example.com
    recordTTL: 180
    recordType: NAPTR
    targets:
    - 1 1 U 123 regular myentry.example.com
```

Error:
```
time="2023-10-04T11:41:20Z" level=error msg="Failure in zone example.com. [Id: /hostedzone/____________] when submitting change batch: InvalidChangeBatch: [Invalid Resource Record: 'FATAL problem: NAPTRRRDATANotValid (Invalid NAPTR record) encountered with '1 1 U 123 regular myentry.example.com'']\n\tstatus code: 400, request id: a1617559-42a9-48da-9f05-fa047f1c4291"
```


Single quotes are as well not RFC 2915 compliant and don't work with AWS.
```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: examplednsrecord-naptr
spec:
  endpoints:
  - dnsName: naptr.example.com
    recordTTL: 180
    recordType: NAPTR
    targets:
    - 1 1 'U' '123' 'regular' myentry.example.com
```

Error:
```
time="2023-10-04T11:55:20Z" level=error msg="Failure in zone example.com. [Id: /hostedzone/____________] when submitting change batch: InvalidChangeBatch: [Invalid Resource Record: 'FATAL problem: NAPTRRRDATANotValid (Invalid NAPTR record) encountered with '1 1 'U' '123' 'regular' myentry.example.com'']\n\tstatus code: 400, request id: affb55e8-8706-49e7-bc51-4fabc8a12752"
```

The same applies to all NAPTR, CAA and TXT records on both AWS and PowerDNS providers. Potentially, other providers are affected as well.

With TXT records, it is probably a different topic since the entire content should be enclosed in double quotes and not only certain fields.

**Anything else we need to know?**:

Problem: Some records – NAPTR, CAA and TXT – require double quotes for some of the fields. The Kubernetes API JSON returns spec.endpoints.targets as string where double quotes are escaped. The double quotes are sent escaped to the Provider APIs. However, some providers (e.g. PowerDNS or AWS) require RFC-compliant resource record data which means one must add the double quotes to these fields. The current behaviour does not allow creation of TXT, NAPTR or CAA records.

Affected record types:
- NAPTR records require double quotes for the third, fourth and fifth field (https://en.wikipedia.org/wiki/NAPTR_record)
- CAA records require double quotes for the third field (https://en.wikipedia.org/wiki/CAA_record)
- TXT records require double quotes (https://en.wikipedia.org/wiki/TXT_record)

For NAPTR, AWS returns 400 and the following error (without quotes, with single quotes, with double quotes):
```
Invalid Resource Record: 'FATAL problem: NAPTRRRDATANotValid (Invalid NAPTR record) encountered with '1 1 U 123 regular myentry.example.com''
Invalid Resource Record: 'FATAL problem: NAPTRRRDATANotValid (Invalid NAPTR record) encountered with '1 1 'U' '123' 'regular' myentry.example.com''
Invalid Resource Record: 'WARNING problem: NAPTRRRDATARegexpAndReplacementError (NAPTR record can have only one field in regexp and replacement) encountered with '1 1 \"U\" \"123\" \"regular\" myentry.example.com''
```

For TXT, AWS returns 400 and the following error:
```
Invalid Resource Record: 'FATAL problem: InvalidCharacterString (Value should be enclosed in quotation marks) encountered with 'bla bla a.b.c.d.org''
```

For CAA, AWS returns 400 and the following error:
```
Invalid Resource Record: 'FATAL problem: CAARRDATAInvalidValue (Invalid value) encountered with 'Value is not inside quotation mark ca.example.net''
```

Related issues:
- https://github.com/kubernetes-sigs/external-dns/issues/555
- https://github.com/kubernetes-sigs/external-dns/issues/2941
- https://github.com/kubernetes-sigs/external-dns/issues/2936


**Environment**:
- External-DNS version (use `external-dns --version`): v0.13.6
- DNS provider: aws / pdns




Discussion / hints:
Hey @ffledgling @jhoch-palantir @ottoyiu @tariq1890 @Raffo @anandsinghkunwar @matusf 
Question to you since you contributed to the PowerDNS provider: Would you encourage fixing this in the [pdns provider](https://github.com/kubernetes-sigs/external-dns/blame/master/provider/pdns/pdns.go) so ExternalDNS with PowerDNS supports NAPTR? If you support that this is being implemented in the provider, I can look into the implementation.
Heyo @jstudler I no longer have access to @ffledgling (Github 2FAC lockout) and I haven't actively used this setup in over 4 years, so I'm not really the best person to fix this anymore. You might want to find someone who still uses this setup, I think @jhoch-palantir no longer does, @anandsinghkunwar might?

That said, if my memory serves correctly, this just looks like a serialization/deserialization issue along the long chain from YAML -> Materialized Spec -> Kubernetes Scheduler -> external-dns -> powerdns-provider -> powerDNS. I feel like I ran into this issue with CNAME records as well. Getting it right is somewhat tricky and fixing it for just one family of records might break it for the rest unless handled with care.

If you feel up to it, please submit a code-fix and one of the maintainers of the parent external-dns repo can help get this merged.
Hey @jstudler, I don't look into this setup anymore. As @anhadjaisingh pointed out perhaps one of the core maintainers of the repo can get your PR merged if you're up for it. 
Hey @anhadjaisingh & @anandsinghkunwar,
Thanks for your replies. I will look into it and try to propose a fix in the upcoming months.


Fix patch:
diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go
index d2106ff356..a45f61ac42 100644
--- a/endpoint/endpoint.go
+++ b/endpoint/endpoint.go
@@ -44,6 +44,8 @@ const (
 	RecordTypePTR = "PTR"
 	// RecordTypeMX is a RecordType enum value
 	RecordTypeMX = "MX"
+	// RecordTypeNAPTR is a RecordType enum value
+	RecordTypeNAPTR = "NAPTR"
 )
 
 // TTL is a structure defining the TTL of a DNS record
diff --git a/source/crd.go b/source/crd.go
index 10be3fccea..d15b176edc 100644
--- a/source/crd.go
+++ b/source/crd.go
@@ -190,7 +190,11 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
 
 			illegalTarget := false
 			for _, target := range ep.Targets {
-				if strings.HasSuffix(target, ".") {
+				if ep.RecordType != "NAPTR" && strings.HasSuffix(target, ".") {
+					illegalTarget = true
+					break
+				}
+				if ep.RecordType == "NAPTR" && !strings.HasSuffix(target, ".") {
 					illegalTarget = true
 					break
 				}


Test patch:
diff --git a/source/crd_test.go b/source/crd_test.go
index 736e25f1a1..968ba8ad32 100644
--- a/source/crd_test.go
+++ b/source/crd_test.go
@@ -404,6 +404,69 @@ func testCRDSourceEndpoints(t *testing.T) {
 			expectEndpoints: true,
 			expectError:     false,
 		},
+		{
+			title:                "Create NAPTR record",
+			registeredAPIVersion: "test.k8s.io/v1alpha1",
+			apiVersion:           "test.k8s.io/v1alpha1",
+			registeredKind:       "DNSEndpoint",
+			kind:                 "DNSEndpoint",
+			namespace:            "foo",
+			registeredNamespace:  "foo",
+			labels:               map[string]string{"test": "that"},
+			labelFilter:          "test=that",
+			endpoints: []*endpoint.Endpoint{
+				{
+					DNSName:    "example.org",
+					Targets:    endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org.`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org.`},
+					RecordType: endpoint.RecordTypeNAPTR,
+					RecordTTL:  180,
+				},
+			},
+			expectEndpoints: true,
+			expectError:     false,
+		},
+		{
+			title:                "illegal target CNAME",
+			registeredAPIVersion: "test.k8s.io/v1alpha1",
+			apiVersion:           "test.k8s.io/v1alpha1",
+			registeredKind:       "DNSEndpoint",
+			kind:                 "DNSEndpoint",
+			namespace:            "foo",
+			registeredNamespace:  "foo",
+			labels:               map[string]string{"test": "that"},
+			labelFilter:          "test=that",
+			endpoints: []*endpoint.Endpoint{
+				{
+					DNSName:    "example.org",
+					Targets:    endpoint.Targets{"foo.example.org."},
+					RecordType: endpoint.RecordTypeCNAME,
+					RecordTTL:  180,
+				},
+			},
+			expectEndpoints: false,
+			expectError:     false,
+		},
+		{
+			title:                "illegal target NAPTR",
+			registeredAPIVersion: "test.k8s.io/v1alpha1",
+			apiVersion:           "test.k8s.io/v1alpha1",
+			registeredKind:       "DNSEndpoint",
+			kind:                 "DNSEndpoint",
+			namespace:            "foo",
+			registeredNamespace:  "foo",
+			labels:               map[string]string{"test": "that"},
+			labelFilter:          "test=that",
+			endpoints: []*endpoint.Endpoint{
+				{
+					DNSName:    "example.org",
+					Targets:    endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org`},
+					RecordType: endpoint.RecordTypeNAPTR,
+					RecordTTL:  180,
+				},
+			},
+			expectEndpoints: false,
+			expectError:     false,
+		},
 	} {
 		ti := ti
 		t.Run(ti.title, func(t *testing.T) {