From 76dbb0ed67dfce193f85d5bae34eeb3de3c55456 Mon Sep 17 00:00:00 2001 From: Thomas Limoncelli Date: Mon, 1 Dec 2025 14:51:34 -0500 Subject: [PATCH] working on RP --- build/generate/featureMatrix.go | 5 + .../advanced-features/adding-new-rtypes-v2.md | 104 +++++++++++++++++- integrationTest/integration_test.go | 1 + pkg/normalize/validate.go | 1 + pkg/rtype/rp.go | 16 +++ pkg/rtypecontrol/import.go | 22 +++- providers/bind/bindProvider.go | 5 +- providers/capabilities.go | 3 + providers/capability_string.go | 29 ++--- providers/cloudflare/cloudflareProvider.go | 28 +---- providers/gandiv5/convert.go | 49 ++++++--- providers/gandiv5/gandi_v5Provider.go | 1 + 12 files changed, 203 insertions(+), 61 deletions(-) diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go index cb78c58db..ac7ecc631 100644 --- a/build/generate/featureMatrix.go +++ b/build/generate/featureMatrix.go @@ -115,6 +115,7 @@ func matrixData() *FeatureMatrix { DomainModifierNaptr = "[`NAPTR`](../language-reference/domain-modifiers/NAPTR.md)" DomainModifierOpenpgpkey = "[`DNSKEY`](../language-reference/domain-modifiers/OPENPGPKEY.md)" DomainModifierPtr = "[`PTR`](../language-reference/domain-modifiers/PTR.md)" + DomainModifierRP = "[`RP`](../language-reference/domain-modifiers/RP.md)" DomainModifierSMIMEA = "[`SMIMEA`](../language-reference/domain-modifiers/SMIMEA.md)" DomainModifierSoa = "[`SOA`](../language-reference/domain-modifiers/SOA.md)" DomainModifierSrv = "[`SRV`](../language-reference/domain-modifiers/SRV.md)" @@ -280,6 +281,10 @@ func matrixData() *FeatureMatrix { DomainModifierPtr, providers.CanUsePTR, ) + setCapability( + DomainModifierRP, + providers.CanUseRP, + ) setCapability( DomainModifierSMIMEA, providers.CanUseSMIMEA, diff --git a/documentation/advanced-features/adding-new-rtypes-v2.md b/documentation/advanced-features/adding-new-rtypes-v2.md index 48a0a89dc..c812560d2 100644 --- a/documentation/advanced-features/adding-new-rtypes-v2.md +++ b/documentation/advanced-features/adding-new-rtypes-v2.md @@ -78,7 +78,7 @@ func init() { } ``` -STep 2b: Create the struct +Step 2b: Create the struct Create a struct that will store the fields of this rtype. @@ -172,6 +172,108 @@ TODO: * Run the integration tests for BIND * Write documentation +## Add a capability for the record type + +You'll need to mark which providers support this record type. The +initial PR should implement this record for the `BIND` provider at +a minimum. `BIND` outputs non-standard rtypes as a comment. + +- Add the capability to the file `dnscontrol/providers/capabilities.go` (look for `CanUseAlias` and add + it to the end of the list.) +- Run stringer to auto-update the file `dnscontrol/providers/capability_string.go` + +```shell +pushd providers/ +go tool stringer -type=Capability +popd +``` +alternatively + +```shell +pushd providers/ +go generate +popd +``` + +- Add this feature to the feature matrix in `dnscontrol/build/generate/featureMatrix.go`. Add it to the variable `matrix` maintaining alphabetical ordering, which should look like this: + + {% code title="dnscontrol/build/generate/featureMatrix.go" %} + ```diff + func matrixData() *FeatureMatrix { + const ( + ... + DomainModifierCaa = "[`CAA`](language-reference/domain-modifiers/CAA.md)" + + DomainModifierFoo = "[`FOO`](language-reference/domain-modifiers/FOO.md)" + DomainModifierLoc = "[`LOC`](language-reference/domain-modifiers/LOC.md)" + ... + ) + matrix := &FeatureMatrix{ + Providers: map[string]FeatureMap{}, + Features: []string{ + ... + DomainModifierCaa, + + DomainModifierFoo, + DomainModifierLoc, + ... + }, + } + ``` + {% endcode %} + + then add it later in the file with a `setCapability()` statement, which should look like this: + + {% code title="dnscontrol/build/generate/featureMatrix.go" %} + ```diff + ... + + setCapability( + + DomainModifierFoo, + + providers.CanUseFOO, + + ) + ... + ``` + {% endcode %} + +- Add the capability to the list of features that zones are validated + against (i.e. if you want DNSControl to report an error if this + feature is used with a DNS provider that doesn't support it). That's + in the `checkProviderCapabilities` function in + `pkg/normalize/validate.go`. It should look like this: + + {% code title="pkg/normalize/validate.go" %} + ```diff + var providerCapabilityChecks = []pairTypeCapability{ + ... + + capabilityCheck("FOO", providers.CanUseFOO), + ... + ``` + {% endcode %} + +- Mark the `bind` provider as supporting this record type by updating `dnscontrol/providers/bind/bindProvider.go` (look for `providers.CanUse` and you'll see what to do). + +DNSControl will warn/error if this new record is used with a +provider that does not support the capability. + +- Add the capability to the validations in `pkg/normalize/validate.go` + by adding it to `providerCapabilityChecks` +- Some capabilities can't be tested for. If + such testing can't be done, add it to the whitelist in function + `TestCapabilitiesAreFiltered` in + `pkg/normalize/capabilities_test.go` + +If the capabilities testing is not configured correctly, `go test ./...` +will report something like the `MISSING` message below. In this +example we removed `providers.CanUseCAA` from the +`providerCapabilityChecks` list. + +```text +--- FAIL: TestCapabilitiesAreFiltered (0.00s) + capabilities_test.go:66: ok: providers.CanUseAlias (0) is checked for with "ALIAS" + capabilities_test.go:68: MISSING: providers.CanUseCAA (1) is not checked by checkProviderCapabilities + capabilities_test.go:66: ok: providers.CanUseNAPTR (3) is checked for with "NAPTR" +``` + + + # Update providers When a provider needs to create a THING, they have two choices diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 796029807..2ad8a9016 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -174,6 +174,7 @@ func makeTests() []*TestGroup { ), testgroup("RP", + requires(providers.CanUseRP), tc("Create RP", rp("foo", "user.example.com.", "bar.com.")), tc("Create RP", rp("foo", "other.example.com.", "bar.com.")), tc("Create RP", rp("foo", "other.example.com.", "example.com.")), diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 1c080ecea..0e676af29 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -763,6 +763,7 @@ var providerCapabilityChecks = []pairTypeCapability{ capabilityCheck("OPENPGPKEY", providers.CanUseOPENPGPKEY), capabilityCheck("PTR", providers.CanUsePTR), capabilityCheck("R53_ALIAS", providers.CanUseRoute53Alias), + capabilityCheck("RP", providers.CanUseRP), capabilityCheck("SMIMEA", providers.CanUseSMIMEA), capabilityCheck("SOA", providers.CanUseSOA), capabilityCheck("SRV", providers.CanUseSRV), diff --git a/pkg/rtype/rp.go b/pkg/rtype/rp.go index 6bb0681e6..4adcc69da 100644 --- a/pkg/rtype/rp.go +++ b/pkg/rtype/rp.go @@ -48,3 +48,19 @@ func (handle *RP) CopyToLegacyFields(rec *models.RecordConfig) { rp := rec.F.(*RP) _ = rec.SetTarget(rp.Mbox + " " + rp.Txt) } + +// func (handle *RP) TestData() { +// return []itest.TestGroup[ +// itest.Testgroup("RP", +// itest.tc("Create RP", rp("foo", "user.example.com.", "bar.com.")), +// itest.tc("Create RP", rp("foo", "other.example.com.", "bar.com.")), +// itest.tc("Create RP", rp("foo", "other.example.com.", "example.com.")), +// ), +// itest.Testgroup("RP-apex", +// itest.tc("Create RP", rp("@", "user.example.com.", "bar.com.")), +// itest.tc("Create RP", rp("@", "other.example.com.", "bar.com.")), +// itest.tc("Create RP", rp("@", "other.example.com.", "example.com.")), +// ), +// ] + +// } diff --git a/pkg/rtypecontrol/import.go b/pkg/rtypecontrol/import.go index aba4cece2..ce3e8a410 100644 --- a/pkg/rtypecontrol/import.go +++ b/pkg/rtypecontrol/import.go @@ -5,6 +5,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/domaintags" + "github.com/miekg/dns" "github.com/miekg/dns/dnsutil" ) @@ -57,6 +58,22 @@ func NewRecordConfigFromRaw(t string, ttl uint32, args []any, dc *models.DomainC return rec, nil } +func NewRecordConfigFromString(name string, ttl uint32, t string, s string, dc *models.DomainConfig) (*models.RecordConfig, error) { + if _, ok := Func[t]; !ok { + return nil, fmt.Errorf("record type %q is not supported", t) + } + if t == "" { + panic("rtypecontrol: NewRecordConfigFromStruct: empty record type") + } + + rec, err := dns.NewRR(fmt.Sprintf("$ORIGIN .\n. %d IN %s %s", ttl, t, s)) + if err != nil { + return nil, err + } + return NewRecordConfigFromStruct(name, ttl, t, rec, dc) + +} + func NewRecordConfigFromStruct(name string, ttl uint32, t string, fields any, dc *models.DomainConfig) (*models.RecordConfig, error) { if _, ok := Func[t]; !ok { return nil, fmt.Errorf("record type %q is not supported", t) @@ -73,11 +90,6 @@ func NewRecordConfigFromStruct(name string, ttl uint32, t string, fields any, dc } setRecordNames(rec, dc, name) - // // Fill in the .F/.Fields* fields. - // err := Func[t].FromArgs(dc, rec, []any{name, fields.(*dns.RP).Mbox, fields.(*dns.RP).Txt}) - // if err != nil { - // return nil, err - // } err := Func[t].FromStruct(dc, rec, name, fields) if err != nil { return nil, err diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index fa125b443..83b4c797c 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -36,18 +36,19 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"), - providers.CanGetZones: providers.Can(), providers.CanConcur: providers.Can(), + providers.CanGetZones: providers.Can(), providers.CanUseCAA: providers.Can(), providers.CanUseDHCID: providers.Can(), providers.CanUseDNAME: providers.Can(), - providers.CanUseDS: providers.Can(), providers.CanUseDNSKEY: providers.Can(), + providers.CanUseDS: providers.Can(), providers.CanUseHTTPS: providers.Can(), providers.CanUseLOC: providers.Can(), providers.CanUseNAPTR: providers.Can(), providers.CanUseOPENPGPKEY: providers.Can(), providers.CanUsePTR: providers.Can(), + providers.CanUseRP: providers.Can(), providers.CanUseSMIMEA: providers.Can(), providers.CanUseSOA: providers.Can(), providers.CanUseSRV: providers.Can(), diff --git a/providers/capabilities.go b/providers/capabilities.go index 299762dff..6f8730166 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -79,6 +79,9 @@ const ( // CanUseRoute53Alias indicates the provider support the specific R53_ALIAS records that only the Route53 provider supports CanUseRoute53Alias + // CanUseRP indicates the provider can handle RP records + CanUseRP + // CanUseSMIMEA indicates the provider can handle SMIMEA records CanUseSMIMEA diff --git a/providers/capability_string.go b/providers/capability_string.go index a51692f7c..ac6ca2723 100644 --- a/providers/capability_string.go +++ b/providers/capability_string.go @@ -25,23 +25,24 @@ func _() { _ = x[CanUseNAPTR-14] _ = x[CanUsePTR-15] _ = x[CanUseRoute53Alias-16] - _ = x[CanUseSMIMEA-17] - _ = x[CanUseSOA-18] - _ = x[CanUseSRV-19] - _ = x[CanUseSSHFP-20] - _ = x[CanUseSVCB-21] - _ = x[CanUseTLSA-22] - _ = x[CanUseDNSKEY-23] - _ = x[CanUseOPENPGPKEY-24] - _ = x[DocCreateDomains-25] - _ = x[DocDualHost-26] - _ = x[DocOfficiallySupported-27] - _ = x[CanUseAKAMAITLC-28] + _ = x[CanUseRP-17] + _ = x[CanUseSMIMEA-18] + _ = x[CanUseSOA-19] + _ = x[CanUseSRV-20] + _ = x[CanUseSSHFP-21] + _ = x[CanUseSVCB-22] + _ = x[CanUseTLSA-23] + _ = x[CanUseDNSKEY-24] + _ = x[CanUseOPENPGPKEY-25] + _ = x[DocCreateDomains-26] + _ = x[DocDualHost-27] + _ = x[DocOfficiallySupported-28] + _ = x[CanUseAKAMAITLC-29] } -const _Capability_name = "CanAutoDNSSECCanConcurCanGetZonesCanOnlyDiff1FeaturesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDNAMECanUseDSCanUseDSForChildrenCanUseHTTPSCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSMIMEACanUseSOACanUseSRVCanUseSSHFPCanUseSVCBCanUseTLSACanUseDNSKEYCanUseOPENPGPKEYDocCreateDomainsDocDualHostDocOfficiallySupportedCanUseAKAMAITLC" +const _Capability_name = "CanAutoDNSSECCanConcurCanGetZonesCanOnlyDiff1FeaturesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDNAMECanUseDSCanUseDSForChildrenCanUseHTTPSCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseRPCanUseSMIMEACanUseSOACanUseSRVCanUseSSHFPCanUseSVCBCanUseTLSACanUseDNSKEYCanUseOPENPGPKEYDocCreateDomainsDocDualHostDocOfficiallySupportedCanUseAKAMAITLC" -var _Capability_index = [...]uint16{0, 13, 22, 33, 53, 68, 79, 95, 104, 115, 126, 134, 153, 164, 173, 184, 193, 211, 223, 232, 241, 252, 262, 272, 284, 300, 316, 327, 349, 364} +var _Capability_index = [...]uint16{0, 13, 22, 33, 53, 68, 79, 95, 104, 115, 126, 134, 153, 164, 173, 184, 193, 211, 219, 231, 240, 249, 260, 270, 280, 292, 308, 324, 335, 357, 372} func (i Capability) String() string { idx := int(i) - 0 diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index c55d5126e..cfc3ae029 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -77,13 +77,16 @@ func init() { //providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", providerName, "") providers.RegisterCustomRecordType("CF_WORKER_ROUTE", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) + + // providers.SupportedRecordTypes(provderName, + // "CLOUDFLAREAPI_SINGLE_REDIRECT", + // ) } // cloudflareProvider is the handle for API calls. type cloudflareProvider struct { ipConversions []transform.IPConversion ignoredLabels []string - //manageRedirects bool // Old "Page Rule"-style redirects. manageWorkers bool accountID string cfClient *cloudflare.API @@ -137,15 +140,6 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin } } - // if c.manageRedirects { // if old-style "page rules" are still being managed. - // fmt.Printf("DEBUG: Getting old-style page rules for %s???\n", domain) - // // prs, err := c.getPageRules(domainID, domain) - // // if err != nil { - // // return nil, err - // // } - // // records = append(records, prs...) - // } - if c.manageSingleRedirects { // if new xor old // Download the list of Single Redirects. // For each one, generate a SINGLEREDIRECT record @@ -285,11 +279,6 @@ func genComparable(rec *models.RecordConfig) string { func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, domainID, msg string) []*models.Correction { switch newrec.Type { - // case "PAGE_RULE": - // return []*models.Correction{{ - // Msg: msg, - // F: func() error { return c.createPageRule(domainID, *newrec.CloudflareRedirect) }, - // }} case "WORKER_ROUTE": return []*models.Correction{{ Msg: msg, @@ -310,8 +299,6 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordConfig, domainID string, msg string) []*models.Correction { var idTxt string switch oldrec.Type { - // case "PAGE_RULE": - // idTxt = oldrec.Original.(cloudflare.PageRule).ID case "WORKER_ROUTE": idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID case "CLOUDFLAREAPI_SINGLE_REDIRECT": @@ -322,13 +309,6 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon msg = msg + color.YellowString(" id=%v", idTxt) switch newrec.Type { - // case "PAGE_RULE": - // return []*models.Correction{{ - // Msg: msg, - // F: func() error { - // return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect) - // }, - // }} case "CLOUDFLAREAPI_SINGLE_REDIRECT": return []*models.Correction{{ Msg: msg, diff --git a/providers/gandiv5/convert.go b/providers/gandiv5/convert.go index b5795450f..a6d020cff 100644 --- a/providers/gandiv5/convert.go +++ b/providers/gandiv5/convert.go @@ -7,6 +7,8 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypeinfo" "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/go-gandi/go-gandi/livedns" ) @@ -18,25 +20,42 @@ func nativeToRecords(n livedns.DomainRecord, origin string) (rcs []*models.Recor // records for a label, all the IP addresses are listed in // n.RrsetValues rather than having many livedns.DomainRecord's. // We must split them out into individual records, one for each value. + + dc := models.MakeFakeDomainConfig(origin) + for _, value := range n.RrsetValues { - rc := &models.RecordConfig{ - TTL: uint32(n.RrsetTTL), - Original: n, - } - rc.SetLabel(n.RrsetName, origin) + var rc *models.RecordConfig + var err error - switch rtype := n.RrsetType; rtype { - case "ALIAS": - rc.Type = "ALIAS" - err = rc.SetTarget(value) - default: - err = rc.PopulateFromStringFunc(rtype, value, origin, txtutil.ParseQuoted) - } - if err != nil { - return nil, fmt.Errorf("unparsable record received from gandi: %w", err) - } + rtype := n.RrsetType + if rtypeinfo.IsModernType(rtype) { + // func NewRecordConfigFromString(name string, ttl uint32, t string, s string, dc *models.DomainConfig) (*models.RecordConfig, error) { + rc, err = rtypecontrol.NewRecordConfigFromString(n.RrsetName, uint32(n.RrsetTTL), rtype, value, dc) + if err != nil { + return nil, fmt.Errorf("unparsable record received from gandi: %w", err) + } + rc.Original = n + } else { + rc = &models.RecordConfig{ + TTL: uint32(n.RrsetTTL), + Original: n, + } + rc.SetLabel(n.RrsetName, origin) + + switch rtype := n.RrsetType; rtype { + case "ALIAS": + rc.Type = "ALIAS" + err = rc.SetTarget(value) + default: + err = rc.PopulateFromStringFunc(rtype, value, origin, txtutil.ParseQuoted) + } + if err != nil { + return nil, fmt.Errorf("unparsable record received from gandi: %w", err) + } + } rcs = append(rcs, rc) + } return rcs, nil diff --git a/providers/gandiv5/gandi_v5Provider.go b/providers/gandiv5/gandi_v5Provider.go index 55f481260..b4f6d9e8d 100644 --- a/providers/gandiv5/gandi_v5Provider.go +++ b/providers/gandiv5/gandi_v5Provider.go @@ -60,6 +60,7 @@ var features = providers.DocumentationNotes{ providers.CanUseDSForChildren: providers.Can(), providers.CanUseLOC: providers.Cannot(), providers.CanUsePTR: providers.Can(), + providers.CanUseRP: providers.Can(), providers.CanUseSRV: providers.Can(), providers.CanUseSSHFP: providers.Can(), providers.CanUseTLSA: providers.Can(),