From cbbb3960264800aefa7c9fc854e23a6348a2eead Mon Sep 17 00:00:00 2001 From: Thomas Limoncelli Date: Sun, 30 Nov 2025 18:07:00 -0500 Subject: [PATCH] RP works FromStruct but hardcoding needs to be removed --- .../advanced-features/debugging-with-dlv.md | 30 ++++++++- integrationTest/helpers_integration_test.go | 8 +-- integrationTest/integration_test.go | 6 +- pkg/rtype/rp.go | 43 +++++++++++++ pkg/rtypecontrol/import.go | 59 +++++++++-------- providers/bind/bindProvider.go | 28 +++++++- providers/cloudflare/cloudflareProvider.go | 64 ------------------- providers/cloudflare/rest.go | 15 +---- .../rtypes/cfsingleredirect/cfredirect.go | 8 +++ .../cfsingleredirect/cfsingleredirect.go | 16 +++-- 10 files changed, 157 insertions(+), 120 deletions(-) create mode 100644 pkg/rtype/rp.go diff --git a/documentation/advanced-features/debugging-with-dlv.md b/documentation/advanced-features/debugging-with-dlv.md index e69e1cace..de90464fc 100644 --- a/documentation/advanced-features/debugging-with-dlv.md +++ b/documentation/advanced-features/debugging-with-dlv.md @@ -11,5 +11,33 @@ dlv test github.com/StackExchange/dnscontrol/v4/pkg/diff2 -- -test.run Test_anal Debug the integration tests: ```shell -dlv test github.com/StackExchange/dnscontrol/v4/integrationTest -- -test.v -test.run ^TestDNSProviders -verbose -profile NAMEDOTCOM -start 1 -end 1 +dlv test github.com/StackExchange/dnscontrol/v4/integrationTest -- -test.v -test.run ^TestDNSProviders -verbose -profile BIND -start 7 -end 7 +``` + +If you are using VSCode, the equivalent configuration is: + +``` + "configurations": [ + { + "name": "Debug Integration Test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/integrationTest", + "args": [ + "-test.v", + "-test.run", + "^TestDNSProviders", + "-verbose", + "-profile", + "BIND", + "-start", + "7", + "-end", + "7" + ], + "buildFlags": "", + "env": {}, + "showLog": true + }, ``` diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index b5a96f361..a03fde593 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -343,7 +343,7 @@ func cfSingleRedirectEnabled() bool { } func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw("CLOUDFLAREAPI_SINGLE_REDIRECT", []any{name, code, when, then}, globalDC) + rec, err := rtypecontrol.NewRecordConfigFromRaw("CLOUDFLAREAPI_SINGLE_REDIRECT", 1, []any{name, code, when, then}, globalDC) panicOnErr(err) return rec } @@ -355,13 +355,13 @@ func cfWorkerRoute(pattern, target string) *models.RecordConfig { } func cfRedir(pattern, target string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw("CF_REDIRECT", []any{pattern, target}, globalDC) + rec, err := rtypecontrol.NewRecordConfigFromRaw("CF_REDIRECT", 1, []any{pattern, target}, globalDC) panicOnErr(err) return rec } func cfRedirTemp(pattern, target string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw("CF_TEMP_REDIRECT", []any{pattern, target}, globalDC) + rec, err := rtypecontrol.NewRecordConfigFromRaw("CF_TEMP_REDIRECT", 1, []any{pattern, target}, globalDC) panicOnErr(err) return rec } @@ -487,7 +487,7 @@ func r53alias(name, aliasType, target, evalTargetHealth string) *models.RecordCo } func rp(name string, m, t string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw("RP", []any{name, m, t}, globalDC) + rec, err := rtypecontrol.NewRecordConfigFromRaw("RP", 300, []any{name, m, t}, globalDC) panicOnErr(err) return rec } diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index bad4819ee..0854052df 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -174,9 +174,9 @@ func makeTests() []*TestGroup { ), testgroup("RP", - tc("Create RP", rp("foo", "usr@example.com", "bar.com")), - tc("Create RP", rp("foo", "other@example.com", "bar.com")), - tc("Create RP", rp("foo", "other@example.com", "example.com")), + 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.")), ), // TXT diff --git a/pkg/rtype/rp.go b/pkg/rtype/rp.go new file mode 100644 index 000000000..f133629f8 --- /dev/null +++ b/pkg/rtype/rp.go @@ -0,0 +1,43 @@ +package rtype + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" + "github.com/miekg/dns" +) + +func init() { + rtypecontrol.Register(&RP{}) +} + +// RP RR. See RFC 1138, Section 2.2. +type RP struct { + dns.RP +} + +func (handle *RP) Name() string { + return "RP" +} + +func (handle *RP) FromArgs(dc *models.DomainConfig, rec *models.RecordConfig, args []any) error { + if err := rtypecontrol.PaveArgs(args[1:], "ss"); err != nil { + return err + } + rec.F = &RP{ + dns.RP{ + Mbox: args[1].(string), + Txt: args[2].(string), + }, + } + + // TODO: Generate friendly Comparable and ZonefilePartial values. + rec.Comparable = rec.F.(*RP).Mbox + " " + rec.F.(*RP).Txt + rec.ZonefilePartial = rec.Comparable + + return nil +} + +func (handle *RP) CopyToLegacyFields(rec *models.RecordConfig) { + rp := rec.F.(*RP) + _ = rec.SetTarget(rp.Mbox + " " + rp.Txt) +} diff --git a/pkg/rtypecontrol/import.go b/pkg/rtypecontrol/import.go index c59d4582d..cd9e50125 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" ) @@ -14,7 +15,7 @@ func ImportRawRecords(domains []*models.DomainConfig) error { for _, dc := range domains { for _, rawRec := range dc.RawRecords { - rec, err := NewRecordConfigFromRaw(rawRec.Type, rawRec.Args, dc) + rec, err := NewRecordConfigFromRaw(rawRec.Type, rawRec.TTL, rawRec.Args, dc) rec.FilePos = models.FixPosition(rawRec.FilePos) if err != nil { return fmt.Errorf("%s: %w", rec.FilePos, err) @@ -32,25 +33,22 @@ func ImportRawRecords(domains []*models.DomainConfig) error { return nil } -func NewRecordConfigFromRaw(t string, args []any, dc *models.DomainConfig) (*models.RecordConfig, error) { - //fmt.Printf("DEBUG: NewRecordConfigFromRaw t=%q args=%+v\n", t, args) +func NewRecordConfigFromRaw(t string, ttl uint32, args []any, 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: NewRecordConfigFromRaw: empty record type") + } // Create as much of the RecordConfig as we can now. Allow New() to fill in the reset. rec := &models.RecordConfig{ Type: t, - Name: args[0].(string), // May be fixed later. + TTL: ttl, Metadata: map[string]string{}, } - setRecordNames(rec, dc, args[0].(string)) - if rec.Type == "" { - panic("rtypecontrol: NewRecordConfigFromRaw: empty record type") - } - // Fill in the .F/.Fields* fields. err := Func[t].FromArgs(dc, rec, args) if err != nil { @@ -60,19 +58,30 @@ func NewRecordConfigFromRaw(t string, args []any, dc *models.DomainConfig) (*mod return rec, nil } -// func stringifyMetas(metas []map[string]any) map[string]string { -// result := make(map[string]string) -// for _, m := range metas { -// for mk, mv := range m { -// if v, ok := mv.(string); ok { -// result[mk] = v // Already a string. No new malloc. -// } else { -// result[mk] = fmt.Sprintf("%v", mv) -// } -// } -// } -// return result -// } +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) + } + if t == "" { + panic("rtypecontrol: NewRecordConfigFromStruct: empty record type") + } + + // Create as much of the RecordConfig as we can now. Allow New() to fill in the reset. + rec := &models.RecordConfig{ + Type: t, + TTL: ttl, + Metadata: map[string]string{}, + } + 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 + } + + return rec, nil +} func setRecordNames(rec *models.RecordConfig, dc *models.DomainConfig, n string) { @@ -87,9 +96,9 @@ func setRecordNames(rec *models.RecordConfig, dc *models.DomainConfig, n string) rec.NameRaw = n rec.NameUnicode = domaintags.EfficientToUnicode(n) } - rec.NameFQDN = dnsutil.AddOrigin(rec.Name, dc.Name) - rec.NameFQDNRaw = dnsutil.AddOrigin(rec.NameRaw, dc.NameRaw) - rec.NameFQDNUnicode = dnsutil.AddOrigin(rec.NameUnicode, dc.NameUnicode) + rec.NameFQDN = dc.Name + rec.NameFQDNRaw = dc.NameRaw + rec.NameFQDNUnicode = dc.NameUnicode } else { // _EXTEND() mode: // FIXME(tlim): Not implemented. diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 525e8c74f..fa125b443 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -27,6 +27,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/pkg/domaintags" "github.com/StackExchange/dnscontrol/v4/pkg/prettyzone" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns" ) @@ -204,10 +205,31 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo foundRecords := models.Records{} for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { - rec, err := models.RRtoRCTxtBug(rr, zoneName) - if err != nil { - return nil, err + var rec models.RecordConfig + var prec *models.RecordConfig + var err error + + // Modern types: + rtype := rr.Header().Rrtype + switch rtype { + case dns.TypeRP: + name := rr.Header().Name + name = strings.TrimSuffix(name, ".") + prec, err = rtypecontrol.NewRecordConfigFromStruct(name, rr.Header().Ttl, "RP", rr, models.MakeFakeDomainConfig(zoneName)) + if err != nil { + return nil, err + } + rec = *prec + rec.TTL = rr.Header().Ttl + fmt.Printf("DEBUG: RP record parsed as %+v\n", rec) + default: + // Legacy types: + rec, err = models.RRtoRCTxtBug(rr, zoneName) + if err != nil { + return nil, err + } } + foundRecords = append(foundRecords, &rec) } diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 2cdd9c2f8..bbfacb32e 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -509,58 +509,6 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } } - // // CF_REDIRECT record types: - // if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" { - // if !c.manageRedirects && !c.manageSingleRedirects { - // return errors.New("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_REDIRECT/CF_TEMP_REDIRECT records") - // } - // code := uint16(301) - // if rec.Type == "CF_TEMP_REDIRECT" { - // code = 302 - // } - - // part := strings.SplitN(rec.GetTargetField(), ",", 2) - // prWhen, prThen := part[0], part[1] - // prPriority++ - - // // Convert this record to a PAGE_RULE. - // if err := cfsingleredirect.MakePageRule(rec, prPriority, code, prWhen, prThen); err != nil { - // return err - // } - // rec.SetLabel("@", dc.Name) - - // if c.manageRedirects && !c.manageSingleRedirects { - // // Old-Style only. No additional work needed. - // } else if !c.manageRedirects && c.manageSingleRedirects { - // // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT. - // if err := cfsingleredirect.TranscodePRtoSR(rec); err != nil { - // return err - // } - // if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { - // return err - // } - // } else { - // // Both old-style and new-style enabled! - // // Retain the PAGE_RULE and append an additional SINGLEREDIRECT. - - // // make a copy: - // newRec, err := rec.Copy() - // if err != nil { - // return err - // } - // // The copy becomes the CF SingleRedirect - // if err := cfsingleredirect.TranscodePRtoSR(rec); err != nil { - // return err - // } - // if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { - // return err - // } - // // Append the copy to the end of the list. - // dc.Records = append(dc.Records, newRec) - - // // The original PAGE_RULE remains untouched. - // } - // } else if rec.Type == "CLOUDFLAREAPI_SINGLE_REDIRECT" { // SINGLEREDIRECT record types. Verify they are enabled. if !c.manageSingleRedirects { @@ -797,18 +745,6 @@ func uint16Zero(value interface{}) uint16 { return 0 } -// // intZero converts value to uint16 or returns 0. -// func intZero(value interface{}) uint16 { -// switch v := value.(type) { -// case float64: -// return uint16(v) -// case int: -// return uint16(v) -// case nil: -// } -// return 0 -// } - // stringDefault returns the value as a string or returns the default value if nil. func stringDefault(value interface{}, def string) string { switch v := value.(type) { diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 1014d350b..0d3af0253 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -299,6 +299,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo rec, err := rtypecontrol.NewRecordConfigFromRaw( "CLOUDFLAREAPI_SINGLE_REDIRECT", + 1, []any{srName, code, srWhen, srThen}, models.MakeFakeDomainConfig(domain)) if err != nil { @@ -434,17 +435,3 @@ func (c *cloudflareProvider) createWorkerRoute(domainID string, target string) e _, err := c.cfClient.CreateWorkerRoute(context.Background(), cloudflare.ZoneIdentifier(domainID), wr) return err } - -// https://github.com/dominikh/go-tools/issues/1137 which is a dup of -// https://github.com/dominikh/go-tools/issues/810 -// -//lint:ignore U1000 false positive due to -// type pageRuleConstraint struct { -// Operator string `json:"operator"` -// Value string `json:"value"` -// } - -// type pageRuleFwdInfo struct { -// URL string `json:"url"` -// StatusCode uint16 `json:"status_code"` -// } diff --git a/providers/cloudflare/rtypes/cfsingleredirect/cfredirect.go b/providers/cloudflare/rtypes/cfsingleredirect/cfredirect.go index 749963983..b2e33e0e5 100644 --- a/providers/cloudflare/rtypes/cfsingleredirect/cfredirect.go +++ b/providers/cloudflare/rtypes/cfsingleredirect/cfredirect.go @@ -24,6 +24,10 @@ func (handle *CfRedirect) FromArgs(dc *models.DomainConfig, rec *models.RecordCo return FromArgs_helper(dc, rec, args, 301) } +// func (handle *CfRedirect) FromStruct(dc *models.DomainConfig, rec *models.RecordConfig, fields any) error { +// panic("CF_REDIRECT: FromStruct not implemented") +// } + func (handle *CfRedirect) CopyToLegacyFields(rec *models.RecordConfig) { // Nothing needs to be copied. The CLOUDFLAREAPI_SINGLE_REDIRECT FromArgs copies everything needed. } @@ -39,6 +43,10 @@ func (handle *CfTempRedirect) FromArgs(dc *models.DomainConfig, rec *models.Reco return FromArgs_helper(dc, rec, args, 302) } +// func (handle *CfTempRedirect) FromStruct(dc *models.DomainConfig, rec *models.RecordConfig, fields any) error { +// panic("CF_TEMP_REDIRECT: FromStruct not implemented") +// } + func (handle *CfTempRedirect) CopyToLegacyFields(rec *models.RecordConfig) { // Nothing needs to be copied. The CLOUDFLAREAPI_SINGLE_REDIRECT FromArgs copies everything needed. } diff --git a/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go index 839107def..17bdebaca 100644 --- a/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go +++ b/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go @@ -17,12 +17,12 @@ type SingleRedirectConfig struct { Code uint16 `json:"code,omitempty"` // 301 or 302 // // SR == SingleRedirect - SRName string `json:"sr_name,omitempty"` // How is this displayed to the user - SRWhen string `json:"sr_when,omitempty"` - SRThen string `json:"sr_then,omitempty"` - SRRRulesetID string `json:"sr_rulesetid,omitempty"` - SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"` - SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_SINGLE_REDIRECT + SRName string `json:"sr_name,omitempty"` // How is this displayed to the user + SRWhen string `json:"sr_when,omitempty"` // Condition for redirect + SRThen string `json:"sr_then,omitempty"` // Formula for redirect + SRRRulesetID string `json:"sr_rulesetid,omitempty"` // ID of the ruleset containing this rule (populated by API) + SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"` // ID of this rule within the ruleset (populated by API) + SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_SINGLE_REDIRECT } // Name returns the text (all caps) name of the rtype. @@ -76,6 +76,10 @@ func (handle *SingleRedirectConfig) FromArgs(dc *models.DomainConfig, rec *model return nil } +// func (handle *SingleRedirectConfig) FromStruct(dc *models.DomainConfig, rec *models.RecordConfig, fields any) error { +// panic("CLOUDFLAREAPI_SINGLE_REDIRECT: FromStruct not implemented") +// } + // targetFromRaw create the display text used for a normal Redirect. func targetFromRaw(name string, code uint16, when, then string) string { return fmt.Sprintf("name=(%s) code=(%03d) when=(%s) then=(%s)",