-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Support SRV record creation with cloudflare provider #4754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# SRV record with CRD source | ||
|
||
You can create and manage SRV records with the help of [CRD source](../contributing/crd-source.md) | ||
and `DNSEndpoint` CRD. The implementation of this feature depends on provider API support, this feature is currently known to be supported (at least) by `akamai`, `civo`, `cloudflare`, `ibmcloud`, `linode`, `rfc2136` and `pdns` providers. | ||
|
||
In order to start managing MX records you need to set the `--managed-record-types SRV` flag. | ||
|
||
```console | ||
external-dns --source crd --provider {akamai|civo|cloudflare|ibmcloud|linode|rfc2136|pdns} --managed-record-types A --managed-record-types CNAME --managed-record-types SRV | ||
``` | ||
|
||
Targets within the CRD need to be specified according to the RFC 2782. Below is an example of | ||
`example.com` DNS SRV record. It specifies a `sip` service of `udp` protocol. It has two targets | ||
of identical priority but with different weights. They point to different backend servers. | ||
|
||
```yaml | ||
apiVersion: externaldns.k8s.io/v1alpha1 | ||
kind: DNSEndpoint | ||
metadata: | ||
name: examplesrvrecord | ||
spec: | ||
endpoints: | ||
- dnsName: _sip._udp.example.com | ||
recordTTL: 180 | ||
recordType: SRV | ||
targets: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to share smoke tests as well, could you please also provide a way to test this manually with manifests and kubectl commands? Similar to this example #5111 (comment) for cloudflare is enough, I'll do for aws or google? I'm unsure where or not multiple targets supported, never tried that case. |
||
- 10 10 5060 sip1.example.com | ||
- 10 20 5060 sip2.example.com | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,13 +172,20 @@ type RecordParamsTypes interface { | |
|
||
// updateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in | ||
func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams { | ||
return cloudflare.UpdateDNSRecordParams{ | ||
recordParam := cloudflare.UpdateDNSRecordParams{ | ||
Name: cfc.ResourceRecord.Name, | ||
TTL: cfc.ResourceRecord.TTL, | ||
Proxied: cfc.ResourceRecord.Proxied, | ||
Type: cfc.ResourceRecord.Type, | ||
Content: cfc.ResourceRecord.Content, | ||
} | ||
|
||
if cfc.ResourceRecord.Type == "SRV" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems identical to cloudflare.CreateDNSRecordParams. Shell we create a shared method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
recordParam.Data = cfc.ResourceRecord.Data | ||
} else { | ||
recordParam.Content = cfc.ResourceRecord.Content | ||
} | ||
|
||
return recordParam | ||
} | ||
|
||
// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in | ||
|
@@ -191,13 +198,52 @@ func updateDataLocalizationRegionalHostnameParams(cfc cloudFlareChange) cloudfla | |
|
||
// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in | ||
func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams { | ||
return cloudflare.CreateDNSRecordParams{ | ||
recordParam := cloudflare.CreateDNSRecordParams{ | ||
Name: cfc.ResourceRecord.Name, | ||
TTL: cfc.ResourceRecord.TTL, | ||
Proxied: cfc.ResourceRecord.Proxied, | ||
Type: cfc.ResourceRecord.Type, | ||
Content: cfc.ResourceRecord.Content, | ||
} | ||
|
||
if cfc.ResourceRecord.Type == "SRV" { | ||
recordParam.Data = cfc.ResourceRecord.Data | ||
} else { | ||
recordParam.Content = cfc.ResourceRecord.Content | ||
} | ||
|
||
return recordParam | ||
} | ||
|
||
// parseSRVContent parses the SRV record content string into the structured data required by the Cloudflare API | ||
func parseSRVContent(content string) (map[string]interface{}, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
parts := strings.Fields(content) | ||
if len(parts) != 4 { | ||
return nil, fmt.Errorf("invalid SRV record content: %s", content) | ||
} | ||
|
||
priority, err := strconv.Atoi(parts[0]) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid priority in SRV record content: %s", parts[0]) | ||
} | ||
|
||
weight, err := strconv.Atoi(parts[1]) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid weight in SRV record content: %s", parts[1]) | ||
} | ||
|
||
port, err := strconv.Atoi(parts[2]) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid port in SRV record content: %s", parts[2]) | ||
} | ||
|
||
target := parts[3] | ||
|
||
|
||
return map[string]interface{}{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we have a struct instead? |
||
"priority": priority, | ||
"weight": weight, | ||
"port": port, | ||
"target": target, | ||
}, nil | ||
} | ||
|
||
// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. | ||
|
@@ -432,7 +478,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud | |
} | ||
recordParam := updateDNSRecordParam(*change) | ||
recordParam.ID = recordID | ||
err := p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam) | ||
err = p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam) | ||
if err != nil { | ||
failedChange = true | ||
log.WithFields(logFields).Errorf("failed to update record: %v", err) | ||
|
@@ -466,7 +512,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud | |
} | ||
} else if change.Action == cloudFlareCreate { | ||
recordParam := getCreateDNSRecordParam(*change) | ||
_, err := p.Client.CreateDNSRecord(ctx, resourceContainer, recordParam) | ||
_, err = p.Client.CreateDNSRecord(ctx, resourceContainer, recordParam) | ||
if err != nil { | ||
failedChange = true | ||
log.WithFields(logFields).Errorf("failed to create record: %v", err) | ||
|
@@ -549,12 +595,11 @@ func (p *CloudFlareProvider) getCustomHostnameIDbyOrigin(chs []cloudflare.Custom | |
func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoint.Endpoint, target string) *cloudFlareChange { | ||
ttl := defaultCloudFlareRecordTTL | ||
proxied := shouldBeProxied(endpoint, p.proxiedByDefault) | ||
|
||
if endpoint.RecordTTL.IsConfigured() { | ||
ttl = int(endpoint.RecordTTL) | ||
} | ||
dt := time.Now() | ||
return &cloudFlareChange{ | ||
change := &cloudFlareChange{ | ||
Action: action, | ||
ResourceRecord: cloudflare.DNSRecord{ | ||
Name: endpoint.DNSName, | ||
|
@@ -587,6 +632,19 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi | |
}, | ||
}, | ||
} | ||
|
||
if endpoint.RecordType == "SRV" { | ||
srvData, err := parseSRVContent(target) | ||
if err != nil { | ||
log.Errorf("Error parsing SRV content: %v", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return nil | ||
} | ||
change.ResourceRecord.Data = srvData | ||
} else { | ||
change.ResourceRecord.Content = target | ||
} | ||
|
||
return change | ||
} | ||
|
||
// listDNSRecordsWithAutoPagination performs automatic pagination of results on requests to cloudflare.ListDNSRecords with custom per_page values | ||
|
@@ -707,7 +765,19 @@ func groupByNameAndTypeWithCustomHostnames(records []cloudflare.DNSRecord, chs [ | |
} | ||
targets := make([]string, len(records)) | ||
for i, record := range records { | ||
targets[i] = record.Content | ||
if record.Type == "SRV" { | ||
if dataMap, ok := record.Data.(map[string]interface{}); ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
priority, _ := dataMap["priority"].(float64) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same apply, could we have a concrete struct, so we do not need to cast? |
||
weight, _ := dataMap["weight"].(float64) | ||
port, _ := dataMap["port"].(float64) | ||
target, _ := dataMap["target"].(string) | ||
targets[i] = fmt.Sprintf("%d %d %d %s", int(priority), int(weight), int(port), target) | ||
} else { | ||
log.Errorf("SRV record data is not in the expected format for record %s", record.Name) | ||
} | ||
} else { | ||
targets[i] = record.Content | ||
} | ||
} | ||
ep := endpoint.NewEndpointWithTTL( | ||
records[0].Name, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we sure all this providers support SRV?