mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 21:55:57 +08:00
- Closes https://github.com/StackExchange/dnscontrol/issues/3853 This PR is gracefully handling FQDN labels when listing records from the Hetzner DNS Control api. These records can be created via other tools or the browser UI. Testing: ```diff diff --git a/providers/hetzner/types.go b/providers/hetzner/types.go index 964f1b7b..3429acc2 100644 --- a/providers/hetzner/types.go +++ b/providers/hetzner/types.go @@ -3,2 +3,3 @@ package hetzner import ( + "fmt" "strings" @@ -63,3 +64,3 @@ func fromRecordConfig(in *models.RecordConfig, zone zone) record { r := record{ - Name: in.GetLabel(), + Name: in.GetLabelFQDN() + ".", Type: in.Type, @@ -69,2 +70,3 @@ func fromRecordConfig(in *models.RecordConfig, zone zone) record { } + fmt.Printf("CREATE: %q\n", r.Name) @@ -93,2 +95,3 @@ func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) { } + fmt.Printf("LISTING: %q\n", r.Name) if strings.HasSuffix(r.Name, "."+domain+".") { ``` Config: ```js var REG_NONE = NewRegistrar('none') var DSP = NewDnsProvider("HETZNER") D('testing1.dev', REG_NONE, DnsProvider(DSP), A('@', '127.0.0.1'), A('foo', '127.0.0.1') ) ``` First push: ``` Waiting for concurrent gathering(s) to complete...LISTING: "@" LISTING: "@" LISTING: "@" LISTING: "@" CREATE: "foo.testing1.dev." DONE ******************** Domain: testing1.dev 1 correction (HETZNER) #1: Batch creation of records: + CREATE A foo.testing1.dev 127.0.0.1 ttl=300 SUCCESS! Done. 1 corrections. ``` Second push (no-op): ``` Waiting for concurrent gathering(s) to complete...LISTING: "@" LISTING: "@" LISTING: "@" LISTING: "@" LISTING: "foo.testing1.dev." DONE ******************** Domain: testing1.dev Done. 0 corrections. ``` DNS query: ``` $ dig foo.testing1.dev. @helium.ns.hetzner.de. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53563 foo.testing1.dev. 300 IN A 127.0.0.1 ``` Additional testing: - update/delete `foo` when record `foo.testing1.dev.` exists, works - creating `foo.testing1.dev` is treated as `foo.testing1.dev.testing1.dev.` in the API, hence the specific suffix check for `<dot>DOMAIN<dot>` - Test with HETZNER_V2, rejects records with FQDN ``` FAILURE! has dot (.) suffix (invalid_input, 50f9cf872ed8f1f808fd33c25cf88a81) ``` <!-- ## Before submiting a pull request Please make sure you've run the following commands from the root directory. bin/generate-all.sh (this runs commands like "go generate", fixes formatting, and so on) ## Release changelog section Help keep the release changelog clear by pre-naming the proper section in the GitHub pull request title. Some examples: * CICD: Add required GHA permissions for goreleaser * DOCS: Fixed providers with "contributor support" table * ROUTE53: Allow R53_ALIAS records to enable target health evaluation More examples/context can be found in the file .goreleaser.yml under the 'build' > 'changelog' key. !-->
106 lines
2.7 KiB
Go
106 lines
2.7 KiB
Go
package hetzner
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
|
|
)
|
|
|
|
type bulkCreateRecordsRequest struct {
|
|
Records []record `json:"records"`
|
|
}
|
|
|
|
type bulkUpdateRecordsRequest struct {
|
|
Records []record `json:"records"`
|
|
}
|
|
|
|
type createZoneRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type createZoneResponse struct {
|
|
Zone zone `json:"zone"`
|
|
}
|
|
|
|
type getAllRecordsResponse struct {
|
|
Records []record `json:"records"`
|
|
Meta struct {
|
|
Pagination struct {
|
|
LastPage int `json:"last_page"`
|
|
TotalEntries int `json:"total_entries"`
|
|
} `json:"pagination"`
|
|
} `json:"meta"`
|
|
}
|
|
|
|
type getAllZonesResponse struct {
|
|
Zones []zone `json:"zones"`
|
|
Meta struct {
|
|
Pagination struct {
|
|
LastPage int `json:"last_page"`
|
|
TotalEntries int `json:"total_entries"`
|
|
} `json:"pagination"`
|
|
} `json:"meta"`
|
|
}
|
|
|
|
type record struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
TTL *uint32 `json:"ttl"`
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
ZoneID string `json:"zone_id"`
|
|
}
|
|
|
|
type zone struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
NameServers []string `json:"ns"`
|
|
TTL uint32 `json:"ttl"`
|
|
}
|
|
|
|
func fromRecordConfig(in *models.RecordConfig, zone zone) record {
|
|
r := record{
|
|
Name: in.GetLabel(),
|
|
Type: in.Type,
|
|
Value: in.GetTargetCombinedFunc(txtutil.EncodeQuoted),
|
|
TTL: &in.TTL,
|
|
ZoneID: zone.ID,
|
|
}
|
|
|
|
if r.Type == "TXT" && (in.GetTargetTXTSegmentCount() == 1) {
|
|
// HACK: HETZNER rejects values that fit into 255 bytes w/o quotes,
|
|
// but do not fit w/ added quotes (via GetTargetCombined()).
|
|
// Sending the raw, non-quoted value works for the comprehensive
|
|
// suite of integrations tests.
|
|
// The HETZNER validation does not provide helpful error messages.
|
|
// {"error":{"message":"422 Unprocessable Entity: missing: ; ","code":422}}
|
|
// Last checked: 2023-04-01
|
|
valueNotQuoted := in.GetTargetTXTSegmented()[0]
|
|
if len(valueNotQuoted) == 254 || len(valueNotQuoted) == 255 {
|
|
r.Value = valueNotQuoted
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) {
|
|
rc := models.RecordConfig{
|
|
Type: r.Type,
|
|
TTL: *r.TTL,
|
|
Original: r,
|
|
}
|
|
if strings.HasSuffix(r.Name, "."+domain+".") {
|
|
// Records created through other tools or the browser UI can contain FQDN labels.
|
|
rc.SetLabelFromFQDN(r.Name, domain)
|
|
} else {
|
|
rc.SetLabel(r.Name, domain)
|
|
}
|
|
|
|
// HACK: Hetzner is inserting a trailing space after multiple, quoted values.
|
|
// NOTE: The actual DNS answer does not contain the space.
|
|
// NOTE: The txtutil.ParseQuoted parser handles this just fine.
|
|
// Last checked: 2023-04-01
|
|
return &rc, rc.PopulateFromStringFunc(r.Type, r.Value, domain, txtutil.ParseQuoted)
|
|
}
|