RP works FromStruct but hardcoding needs to be removed

This commit is contained in:
Thomas Limoncelli 2025-11-30 18:07:00 -05:00
parent a3ed1dc6b2
commit cbbb396026
No known key found for this signature in database
10 changed files with 157 additions and 120 deletions

View file

@ -11,5 +11,33 @@ dlv test github.com/StackExchange/dnscontrol/v4/pkg/diff2 -- -test.run Test_anal
Debug the integration tests: Debug the integration tests:
```shell ```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
},
``` ```

View file

@ -343,7 +343,7 @@ func cfSingleRedirectEnabled() bool {
} }
func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { 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) panicOnErr(err)
return rec return rec
} }
@ -355,13 +355,13 @@ func cfWorkerRoute(pattern, target string) *models.RecordConfig {
} }
func cfRedir(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) panicOnErr(err)
return rec return rec
} }
func cfRedirTemp(pattern, target string) *models.RecordConfig { 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) panicOnErr(err)
return rec return rec
} }
@ -487,7 +487,7 @@ func r53alias(name, aliasType, target, evalTargetHealth string) *models.RecordCo
} }
func rp(name string, m, t string) *models.RecordConfig { 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) panicOnErr(err)
return rec return rec
} }

View file

@ -174,9 +174,9 @@ func makeTests() []*TestGroup {
), ),
testgroup("RP", testgroup("RP",
tc("Create RP", rp("foo", "usr@example.com", "bar.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.", "bar.com.")),
tc("Create RP", rp("foo", "other@example.com", "example.com")), tc("Create RP", rp("foo", "other.example.com.", "example.com.")),
), ),
// TXT // TXT

43
pkg/rtype/rp.go Normal file
View file

@ -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)
}

View file

@ -5,6 +5,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags" "github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
) )
@ -14,7 +15,7 @@ func ImportRawRecords(domains []*models.DomainConfig) error {
for _, dc := range domains { for _, dc := range domains {
for _, rawRec := range dc.RawRecords { 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) rec.FilePos = models.FixPosition(rawRec.FilePos)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", rec.FilePos, err) return fmt.Errorf("%s: %w", rec.FilePos, err)
@ -32,25 +33,22 @@ func ImportRawRecords(domains []*models.DomainConfig) error {
return nil return nil
} }
func NewRecordConfigFromRaw(t string, args []any, dc *models.DomainConfig) (*models.RecordConfig, error) { func NewRecordConfigFromRaw(t string, ttl uint32, args []any, dc *models.DomainConfig) (*models.RecordConfig, error) {
//fmt.Printf("DEBUG: NewRecordConfigFromRaw t=%q args=%+v\n", t, args)
if _, ok := Func[t]; !ok { if _, ok := Func[t]; !ok {
return nil, fmt.Errorf("record type %q is not supported", t) 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. // Create as much of the RecordConfig as we can now. Allow New() to fill in the reset.
rec := &models.RecordConfig{ rec := &models.RecordConfig{
Type: t, Type: t,
Name: args[0].(string), // May be fixed later. TTL: ttl,
Metadata: map[string]string{}, Metadata: map[string]string{},
} }
setRecordNames(rec, dc, args[0].(string)) setRecordNames(rec, dc, args[0].(string))
if rec.Type == "" {
panic("rtypecontrol: NewRecordConfigFromRaw: empty record type")
}
// Fill in the .F/.Fields* fields. // Fill in the .F/.Fields* fields.
err := Func[t].FromArgs(dc, rec, args) err := Func[t].FromArgs(dc, rec, args)
if err != nil { if err != nil {
@ -60,19 +58,30 @@ func NewRecordConfigFromRaw(t string, args []any, dc *models.DomainConfig) (*mod
return rec, nil return rec, nil
} }
// func stringifyMetas(metas []map[string]any) map[string]string { func NewRecordConfigFromStruct(name string, ttl uint32, t string, fields any, dc *models.DomainConfig) (*models.RecordConfig, error) {
// result := make(map[string]string) if _, ok := Func[t]; !ok {
// for _, m := range metas { return nil, fmt.Errorf("record type %q is not supported", t)
// for mk, mv := range m { }
// if v, ok := mv.(string); ok { if t == "" {
// result[mk] = v // Already a string. No new malloc. panic("rtypecontrol: NewRecordConfigFromStruct: empty record type")
// } else { }
// result[mk] = fmt.Sprintf("%v", mv)
// } // Create as much of the RecordConfig as we can now. Allow New() to fill in the reset.
// } rec := &models.RecordConfig{
// } Type: t,
// return result 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) { 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.NameRaw = n
rec.NameUnicode = domaintags.EfficientToUnicode(n) rec.NameUnicode = domaintags.EfficientToUnicode(n)
} }
rec.NameFQDN = dnsutil.AddOrigin(rec.Name, dc.Name) rec.NameFQDN = dc.Name
rec.NameFQDNRaw = dnsutil.AddOrigin(rec.NameRaw, dc.NameRaw) rec.NameFQDNRaw = dc.NameRaw
rec.NameFQDNUnicode = dnsutil.AddOrigin(rec.NameUnicode, dc.NameUnicode) rec.NameFQDNUnicode = dc.NameUnicode
} else { } else {
// _EXTEND() mode: // _EXTEND() mode:
// FIXME(tlim): Not implemented. // FIXME(tlim): Not implemented.

View file

@ -27,6 +27,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags" "github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
"github.com/StackExchange/dnscontrol/v4/pkg/prettyzone" "github.com/StackExchange/dnscontrol/v4/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v4/pkg/printer" "github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
"github.com/StackExchange/dnscontrol/v4/providers" "github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -204,10 +205,31 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo
foundRecords := models.Records{} foundRecords := models.Records{}
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
rec, err := models.RRtoRCTxtBug(rr, zoneName) var rec models.RecordConfig
if err != nil { var prec *models.RecordConfig
return nil, err 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) foundRecords = append(foundRecords, &rec)
} }

View file

@ -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" { if rec.Type == "CLOUDFLAREAPI_SINGLE_REDIRECT" {
// SINGLEREDIRECT record types. Verify they are enabled. // SINGLEREDIRECT record types. Verify they are enabled.
if !c.manageSingleRedirects { if !c.manageSingleRedirects {
@ -797,18 +745,6 @@ func uint16Zero(value interface{}) uint16 {
return 0 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. // stringDefault returns the value as a string or returns the default value if nil.
func stringDefault(value interface{}, def string) string { func stringDefault(value interface{}, def string) string {
switch v := value.(type) { switch v := value.(type) {

View file

@ -299,6 +299,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo
rec, err := rtypecontrol.NewRecordConfigFromRaw( rec, err := rtypecontrol.NewRecordConfigFromRaw(
"CLOUDFLAREAPI_SINGLE_REDIRECT", "CLOUDFLAREAPI_SINGLE_REDIRECT",
1,
[]any{srName, code, srWhen, srThen}, []any{srName, code, srWhen, srThen},
models.MakeFakeDomainConfig(domain)) models.MakeFakeDomainConfig(domain))
if err != nil { 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) _, err := c.cfClient.CreateWorkerRoute(context.Background(), cloudflare.ZoneIdentifier(domainID), wr)
return err 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"`
// }

View file

@ -24,6 +24,10 @@ func (handle *CfRedirect) FromArgs(dc *models.DomainConfig, rec *models.RecordCo
return FromArgs_helper(dc, rec, args, 301) 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) { func (handle *CfRedirect) CopyToLegacyFields(rec *models.RecordConfig) {
// Nothing needs to be copied. The CLOUDFLAREAPI_SINGLE_REDIRECT FromArgs copies everything needed. // 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) 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) { func (handle *CfTempRedirect) CopyToLegacyFields(rec *models.RecordConfig) {
// Nothing needs to be copied. The CLOUDFLAREAPI_SINGLE_REDIRECT FromArgs copies everything needed. // Nothing needs to be copied. The CLOUDFLAREAPI_SINGLE_REDIRECT FromArgs copies everything needed.
} }

View file

@ -17,12 +17,12 @@ type SingleRedirectConfig struct {
Code uint16 `json:"code,omitempty"` // 301 or 302 Code uint16 `json:"code,omitempty"` // 301 or 302
// //
// SR == SingleRedirect // SR == SingleRedirect
SRName string `json:"sr_name,omitempty"` // How is this displayed to the user SRName string `json:"sr_name,omitempty"` // How is this displayed to the user
SRWhen string `json:"sr_when,omitempty"` SRWhen string `json:"sr_when,omitempty"` // Condition for redirect
SRThen string `json:"sr_then,omitempty"` SRThen string `json:"sr_then,omitempty"` // Formula for redirect
SRRRulesetID string `json:"sr_rulesetid,omitempty"` SRRRulesetID string `json:"sr_rulesetid,omitempty"` // ID of the ruleset containing this rule (populated by API)
SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"` 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 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. // 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 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. // targetFromRaw create the display text used for a normal Redirect.
func targetFromRaw(name string, code uint16, when, then string) string { func targetFromRaw(name string, code uint16, when, then string) string {
return fmt.Sprintf("name=(%s) code=(%03d) when=(%s) then=(%s)", return fmt.Sprintf("name=(%s) code=(%03d) when=(%s) then=(%s)",