diff --git a/commands/printIR.go b/commands/printIR.go index cb463504fc..ad9e3de83d 100644 --- a/commands/printIR.go +++ b/commands/printIR.go @@ -11,7 +11,6 @@ import ( "github.com/StackExchange/dnscontrol/v4/pkg/js" "github.com/StackExchange/dnscontrol/v4/pkg/normalize" "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183" - "github.com/StackExchange/dnscontrol/v4/pkg/rtypes" "github.com/urfave/cli/v2" ) @@ -130,7 +129,7 @@ func ExecuteDSL(args ExecuteDSLArgs) (*models.DNSConfig, error) { return nil, fmt.Errorf("executing %s: %w", args.JSFile, err) } - err = rtypes.PostProcess(dnsConfig.Domains) + err = models.ConvertRawRecords(dnsConfig.Domains) if err != nil { return nil, err } diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 55c8babf4f..65e5d28173 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -184,7 +184,8 @@ * [Documentation Style Guide](styleguide-doc.md) * [DNSControl is an opinionated system](opinions.md) * [Writing new DNS providers](writing-providers.md) -* [Creating new DNS Resource Types (rtypes)](adding-new-rtypes.md) +* [Creating new DNS Resource Types (Rdata-style)](adding-new-rtypes-rdata.md) +* [Creating new DNS Resource Types (RC-Style)](adding-new-rtypes.md) * [Integration Tests](integration-tests.md) * [Unit Testing DNS Data](unittests.md) * [Bug Triage Process](bug-triage.md) diff --git a/documentation/adding-new-rtypes-rdata.md b/documentation/adding-new-rtypes-rdata.md new file mode 100644 index 0000000000..9220430102 --- /dev/null +++ b/documentation/adding-new-rtypes-rdata.md @@ -0,0 +1,538 @@ +# Creating new DNS Resource Types (Rdata-style) + +In September 2024 DNSControl gained a new way to implement rtypes called +"Rdata-style". This can be used to add RFC-standard types such as LOC, as well +as provider-specific types such as Cloudflare's "Single Redirect". + +This document explains how Rdata-style rtypes work and how to add a new record +type using this method. + +The old and new styles are both supported. All new rtypes should use +Rdata-style. There is no need to convert the old rtypes to use Rdata-style, +though we'll gladly accept PRs that convert existing rtypes to use Rdata. + +## Terminology + +* RType: A DNS "record type" such as an A, AAAA, CNAME, MX record. +* RC-style: The original way to add new rtypes. +* Rdata-style: The new way to add new rtypes, documented here. + +## Goals + +Goals of Rdata-style records: + +* **Goal: Make it significantly easier to add a new rtype.** + * Problem: RC-Style requires writing code in both Go and JavaScript. + * Solution: Rdata-style requires only writing Go (plus 1 line of JavaScript) +* **Goal: Make testing easier.** + * Problem: RC-Style has no support for unit testing the JavaScript in + helpers.js. + * Solution: Rdata-style only uses Go (with 1 minor exception) and permits the + use of the standard Go unit testing framework. +* **Goal: Reduce (or stop the growth of) models.RecordConfig.** + * Problem: RC-Style requires each new rtype to add fields to RecordConfig. + This consumes memory for every RecordConfig instance. For example, the + DNSKEY rtype added 4 fields, consuming 14 bytes of memory even when the + RecordConfig is not storying a DNSKEY. (Not to pick on DNSKEY... this was + the only option at the time!) + * Solution: RecordConfig now has one field that is a pointer to struct, which + is the right size for the rtype. +* **Goal: Isolate an rtype's implementation in the code base.** + * Problem: RC-Style spreads implementation all over the code base. + * Rdata-style: Code is isolated to a specific directory with many exceptions. + The list of exceptions should shrink over time. + +## Conceptual design + +To understand how Rdata-style works, first let's review the old way. + +The old way: + +RC-style implements a JavaScript function in helpers.js that accepts the +user-input fields, processes them, and makes a JSON version of RecordConfig +which is sent to the Go code for use. It is assumed that the JSON that is +delivered is complete. + +For example, `LOC_BUILDER_DD()` is implemented completely in JavaScript. This +is a complex function and, since we lack unit-testing in DNSControl's +JavaScript environment, has no test coverage. + +The new way: + +In Rdata-style, the helpers.js function simply delivers all parameters to the +Go code verbatium. A function in Go extracts the parameters and uses them to +build a struct. models.RecordConfig.Rdata points to that struct. + +For example, `CF_SINGLE_REDIRECT()`'s implementation in helpers.js is one line: + +```javascript +var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); +``` + +This creates a function called `CF_SINGLE_REDIRECT()` which users can use in +`dnsconfig.js`. It is unaware of how many parameters or their types. + +All the remaining code is in `dnscontrol/rtypes/rtype$NAME` (global rtypes) +or `dnscontrol/providers/$PROVIDER/rtypes/rtype$NAME` (provider-specific rtypes). +(`$PROVIDER` is the name of the provider, and $NAME is the name of the record.) +For example, the Cloudflare Single Redirect type would be in `providers/cloudflare/rtypes/rtypecfsingleredirect`. + +Yes, there is a lot of code outside the rtypes/rtype$NAME directory still. +However we're working on reducing that. + +## How to add a new rtype + +### Step 1: Update helpers.js + +Edit `pkg/js/helpers.js` + +At the end of the file, add a line such as: + +```javascript +var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); + ^^^^^ ^^^^^^^^^^^^^ + function name rtype token +``` + +* function name: This is the name that users will enter in `dnsconfig.js`. + * For RFC-standard types this should be the name of the type as it would appear in a zone file. (Example: `A`, `MX`, `LOC`) + * For provider-specific types, the prefix should be the provider's name or initials (`CF_` for CloudFlare). + * For pseudo-types that apply to any provider, use your best judgement. +* rtype token: The string that is used in the models.RecordConfig.Type field. + * For RFC-standard types this should be the same as the function name. + * For provider-specific types, the prefix should be the provider's name exactly as it is used in `creds.json`. + * For pseudo-types this should be the same as the function name. + +### Step X: Implement the rtype's functions + +Complete the "Rdataer" interface (which is Go-speak for "write all the functions required for that interface). + +Create the file in: + +```text +providers/$PROVIDER/rtypes/rtype$name/$name.go # Provider-specific +rtypes/rtype$name/$name.go # Global +``` + +Examples: + +```text +providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go +rtypes/rtypeloc/loc.go +``` + +In that file implement the following. (Copy from an existing file) + +* `const Name`: Same string as the "rtype token" in helpers.js +* `init()`: Copy verbatim. This is exactly the same for all rtypes. +* Define the struct. `type $Name struct` where `$Name` is the rtype name in mixed case. +* function `Name`: Copy verbatim. This is exactly the same for all rtypes. +* function `ComputeTarget`: returns the "target field" for the record. For example, an `MX` Record would return the hostname (not the preference number), an `A` record would return the IP address. +* function `ComputeComparableMini`: returns a string representation of all the rtype's fields. This string is used for comparing two records. If there are any differences, the two are not considered the same. This string should be human-readable, since it is used in the output of `dnscontrol preview`. For example, `MX` would output `50 example.com.` Note that the label is not included, nor the TTL. +* function `MarshalJSON`: returns a JSON representation of all the rtype's fields. Note that the label is not included, nor the TTL. +* function `FromRawArgs`: Described below. + +### Step X: Implement FromRawArgs + +This function takes the raw items from helpes.js and builds the struct. + +Copy from another rtype. Here's what the code does: + +```go +// FromRawArgs creates a Rdata... +// update a RecordConfig using the args (from a +// RawRecord.Args). In other words, use the data from dnsconfig.js's +// rawrecordBuilder to create (actually... update) a models.RecordConfig. +func FromRawArgs(items []any) (*SingleRedirect, error) { +``` + +The function takes the raw arguments, which arrive as an array of "any"... i.e. they can be any type. + +```go + // Pave the arguments. + if err := rtypecontrol.PaveArgs(items, "iss"); err != nil { + return nil, err + } +``` + +`rtypecontrol.PaveArgs()` takes the raw items and validates them, or fixes them. +The string (in this example, `"iss"`) includes 1 letter for each parameter. + +* `i`: uint16: Converts strings, truncates float64s, resizes ints, etc. +* `s`: string: Converts all types to string. + +If you desire other types, add them to `pkg/rtypecontrol/pave.go`. + +```go + // Unpack the arguments: + var code = items[0].(uint16) + if code != 301 && code != 302 { + return nil, fmt.Errorf("code (%03d) is not 301 or 302", code) + } + var when = items[1].(string) + var then = items[2].(string) +``` + +You are now certain of the type of each `item[]`. Assign each one to a variable of the appropriate type. +This is also where you can validate the inputs. In this example, `code` must be either 301 or 302. + +If you are new to go's "type assertions", here's a simple explanation: + +* Go doesn't know what type of data is in `item[]` (they are of type `any`). Therefore, we have to tell Go by adding `.(string)` or `.(uint16)`. We can trust that these are ZZ + +Here's the longer version: + +* Each element of `items[]` is an interface. It can be any type. Go needs us + to tell us what type to expect when accessing it. It can't guess for us. This + isn't Python! +* We tell Go it is a string by referring to it as `items[1].(string)`. This is + called a "type assertion" because we are asserting the type instead of allowing Go to figure it out. (because it can't) +* This works great, except there's a catch: We we assert wrong, the code will + panic. If we write `item[3].(uint16)` and the data is a `string`, the code + will panic at run-time. That's why we have to trust `PaveArgs` to do the + right thing. +* Wait! If Go can't guess the type, how does it know when we are wrong? Ah + ha! It does know. An interface stores both the value and the type. Therefore + it can check if we've asserted the wrong type. However, it can't generate + code that works for all types. The type assertion tells the code generator + what to do. +* The Pave Pattern is something I created for DNSControl to make it easier to + work with interfaces. You won't see it elsewhere. +* To learn more about Go's type assertions and "type switches", a good tutorial is here: [https://rednafi.com/go/type_assertion_vs_type_switches/](https://rednafi.com/go/type_assertion_vs_type_switches/) + +```go + // Use the arguments to perfect the record: + return makeSingleRedirectFromRawRec(code, name, when, then) +} +``` + +This calls a function that makes the struct (actually a pointer to a struct). +For simple record types there's no need to make this a separate function. + +### Step X: ConvertRawRecords + +Edit models/rawrecord.go + +In the function `ConvertRawRecords()`, add to the switch statement a case for the new type. + +Here's an example. Change "foo" to the name of your type. + +```go + case rtypefoo.Name: + rdata, error := rtypefoo.FromRawArgs(args, label) + if error != nil { + return err + } + rec.Seal(dc.Name, label, rdata) +``` + +### Step X: update casts.go + +This will be automated some day, but in the meanwhile this is done manually. + +Edit `models/casts.go` + +Add the rtype's module to the imports list. + +Add the rtype's `As*()` function. For example, if you are adding an rtype FOO, add a function`AsFOO()`. + +Follow the examples. + +```go +import ( + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypefoo" +) + +func (rc *RecordConfig) AsFOO() *rtypefoo.FOO { + return rc.Rdata.(*rtypefoo.FOO) +} +``` + +### Step X: update create.go + +This will be automated some day, but in the meanwhile this is done manually. + +Edit `pkg/create/create.go` + +Add the rtype's module to the imports list. + +Add the rtype's `Foo()` function. For example, if you are adding an rtype FOO, add a function`FOO()`. + +Follow the examples. It should be exactly the same as `SingleRedirect()` with `SingleRedirect` changed to `FOO`. + +### Step X: Add this to a provider + +### Step X: Add integration etsts + +### Step 2: Add a capability for the record + +You'll need to mark which providers support this record type. If BIND supports this record type, add support +to bind first. This is the easiest provider to update. Otherwise choose another provider. + +* 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` + +Install stringer: + +```shell +go install golang.org/x/tools/cmd/stringer@latest +``` + +Run stringer: + +```shell +cd providers +go generate +``` + +* 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" +``` + +### Step 4: Search for `#rtype_variations` + +Anywhere a `rtype` requires special handling has been marked with a +comment that includes the string `#rtype_variations`. Search for +this string and add your new type to this code. + +### Step 5: Add a `parse_tests` test case + +Add at least one test case to the `pkg/js/parse_tests` directory. +Test `013-mx.js` is a very simple one and is good for cloning. +See also `017-txt.js`. + +Run these tests via: + +```shell +cd pkg/js/ +go test ./... +``` + +If this works, then you know the `dnsconfig.js` and `helpers.js` +code is working correctly. + +As you debug, if there are places that haven't been marked +`#rtype_variations` that should be, add such a comment. +Every time you do this, an angel gets its wings. + +The tests also verify that for every "capability" there is a +validation. This is explained in Step 2 (search for +`TestCapabilitiesAreFiltered` or `MISSING`) + +### Step 6: Add an `integrationTest` test case + +Add at least one test case to the `integrationTest/integration_test.go` +file. Look for `func makeTests` and add the test to the end of this +list. + +Each `testgroup()` is a named list of tests. + +{% code title="integration_test.go" lineNumbers="true" %} +```go +testgroup("MX", + tc("MX record", mx("@", 5, "foo.com.")), + tc("Change MX pref", mx("@", 10, "foo.com.")), + tc("MX record", + mx("@", 10, "foo.com."), + mx("@", 20, "bar.com."), + ), +) +``` +{% endcode %} + +Line 1: `testgroup()` gives a name to a group of tests. It also tells +the system to delete all records for this domain so that the tests +begin with a blank slate. + +Line 2: +Each `tc()` encodes all the records of a zone. The test framework +will try to do the smallest changes to bring the zone up to date. +In this case, we know the zone is empty, so this will add one MX +record. + +Line 3: In this example, we just change one field of an existing +record. To get to this configuration, the provider will have to +either change the priority on an existing record, or delete the old +record and insert a new one. Either way, this test case assures us +that the diff'ing functionality is working properly. + +If you look at the tests for `CAA`, it inserts a few records then +attempts to modify each field of a record one at a time. This test +was useful because it turns out we hadn't written the code to +properly see a change in priority. We fixed this bug before the +code made it into production. + +Line 4: In this example, the next zone adds a second MX record. +To get to this configuration, the provider will add an +additional MX record to the same label. New tests don't need to do +this kind of test because we're pretty sure that that part of the diffing +engine works fine. It is here as an example. + +Also notice that some tests include `requires()`, `not()` and `only()` +statements. This is how we restrict tests to certain providers. +These options must be listed first in a `testgroup`. More details are +in the source code. + +To run the integration test with the BIND provider: + +```shell +cd integrationTest # NOTE: Not needed if already in that subdirectory +go test -v -verbose -provider BIND +``` + +Once the code works for BIND, consider submitting a PR at this point. +(The earlier you submit a PR, the earlier we can provide feedback.) + +If you find places that haven't been marked +`#rtype_variations` but should be, please add that comment. +Every time you fail to do this, God kills a cute little kitten. +Please do it for the kittens. + +### Step 7: Support more providers + +Now add support in other providers. Add the `providers.CanUse...` +flag to the provider and re-run the integration tests: + +For example, this will run the tests on Amazon AWS Route53: + +```shell +export R53_DOMAIN=dnscontroltest-r53.com # Use a test domain. +export R53_KEY_ID=CHANGE_TO_THE_ID +export R53_KEY='CHANGE_TO_THE_KEY' +cd integrationTest # NOTE: Not needed if already in that subdirectory +go test -v -verbose -provider ROUTE53 +``` + +The test should reveal any bugs. Keep iterating between fixing the +code and running the tests. When the tests all work, you are done. +(Well, you might want to clean up some code a bit, but at least you +know that everything is working.) + +If you find bugs that aren't covered by the tests, please please +please add a test that demonstrates the bug (then fix the bug, of +course). This +will help all future contributors. If you need help with adding +tests, please ask! + +### Step 8: Write documentation + +Add a new Markdown file to `documentation/language-reference/domain-modifiers`. Copy an existing file (`CNAME.md` is a good example). The section between the lines of `---` is called the front matter and it has the following keys: + +* `name`: The name of the record. This should match the file name and the name of the record in `helpers.js`. +* `parameters`: A list of parameter names, in order. Feel free to use spaces in the name if necessary. Your last parameter should be `modifiers...` to allow arbitrary modifiers like `TTL` to be applied to your record. +* `parameter_types`: an object with parameter names as keys and TypeScript type names as values. Check out existing record documentation if you’re not sure to put for a parameter. Note that this isn’t displayed on the website, it’s only used to generate the `.d.ts` file. + +The rest of the file is the documentation. You can use Markdown syntax to format the text. + +Add the new file `FOO.md` to the documentation table of contents [`documentation/SUMMARY.md`](SUMMARY.md#domain-modifiers), and/or to the [`Service Provider specific`](SUMMARY.md#service-provider-specific) section if you made a record specific to a provider, and to the [`Record Modifiers`](SUMMARY.md#record-modifiers) section if you created any `*_BUILDER` or `*_HELPER` or similar functions for the new record type: + +{% code title="documentation/SUMMARY.md" %} +```diff +... +* Domain Modifiers +... + * [DnsProvider](language-reference/domain-modifiers/DnsProvider.md) ++ * [FOO](language-reference/domain-modifiers/FOO.md) + * [FRAME](language-reference/domain-modifiers/FRAME.md) +... + * Service Provider specific +... + * ClouDNS + * [CLOUDNS_WR](language-reference/domain-modifiers/CLOUDNS_WR.md) ++ * ASDF ++ * [ASDF_NINJA](language-reference/domain-modifiers/ASDF_NINJA.md) + * NS1 + * [NS1_URLFWD](language-reference/domain-modifiers/NS1_URLFWD.md) +... +* Record Modifiers +... + * [DMARC_BUILDER](language-reference/domain-modifiers/DMARC_BUILDER.md) ++ * [FOO_HELPER](language-reference/record-modifiers/FOO_HELPER.md) + * [SPF_BUILDER](language-reference/domain-modifiers/SPF_BUILDER.md) +... +``` +{% endcode %} + +### Step 9: "go generate" + +Re-generate the documentation: + +```shell +go generate ./... +``` + +This will regenerate things like the table of which providers have which features and the `dnscontrol.d.ts` file. diff --git a/documentation/adding-new-rtypes.md b/documentation/adding-new-rtypes.md index acf054e035..6439e2f375 100644 --- a/documentation/adding-new-rtypes.md +++ b/documentation/adding-new-rtypes.md @@ -1,4 +1,10 @@ -# Creating new DNS Resource Types (rtypes) +# Creating new DNS Resource Types (RC-Style) + +{% hint style="warning" %} +WARNING: This is the old way to add new resource types. It should not be used +for new feature. The new way ("RData-style") is easier to implement and easier +to test. See [Creating new DNS Resource Types (Rdata-style)](adding-new-rtypes-rdata.md) +{% endhint %} Everyone is familiar with A, AAAA, CNAME, NS and other Rtypes. However there are new record types being added all the time. diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 8272b95f3e..f46b917d26 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -10,13 +10,13 @@ import ( "time" "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/create" "github.com/StackExchange/dnscontrol/v4/pkg/credsfile" "github.com/StackExchange/dnscontrol/v4/pkg/nameservers" "github.com/StackExchange/dnscontrol/v4/pkg/zonerecs" "github.com/StackExchange/dnscontrol/v4/providers" _ "github.com/StackExchange/dnscontrol/v4/providers/_all" "github.com/StackExchange/dnscontrol/v4/providers/cloudflare" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect" "github.com/miekg/dns/dnsutil" ) @@ -522,10 +522,9 @@ func cfSingleRedirectEnabled() bool { } func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { - r := makeRec("@", name, cfsingleredirect.SINGLEREDIRECT) - err := cfsingleredirect.FromRaw(r, []any{name, code, when, then}) + r, err := create.CloudFlareSingleRedirect(300, "@", name, []any{code, when, then}) if err != nil { - panic("Should not happen... cfSingleRedirect") + panic(err) } return r } diff --git a/models/casts.go b/models/casts.go new file mode 100644 index 0000000000..47ed388164 --- /dev/null +++ b/models/casts.go @@ -0,0 +1,11 @@ +package models + +import "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" + +// Helper functions (one per rtype2.0) so that users don't need to +// deal with type assertions and conversions. +// In the future, this will be autogenerated. + +func (rc *RecordConfig) AsCloudflareSingleRedirect() *rtypecfsingleredirect.SingleRedirect { + return rc.Rdata.(*rtypecfsingleredirect.SingleRedirect) +} diff --git a/models/rawrecord.go b/models/rawrecord.go index 47d30ef189..acab8dff89 100644 --- a/models/rawrecord.go +++ b/models/rawrecord.go @@ -1,5 +1,11 @@ package models +import ( + "fmt" + + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" +) + // RawRecordConfig stores the user-input from dnsconfig.js for a DNS // Record. This is later processed (in Go) to become a RecordConfig. // NOTE: Only newer rtypes are processed this way. Eventually the @@ -10,3 +16,69 @@ type RawRecordConfig struct { Metas []map[string]any `json:"metas,omitempty"` TTL uint32 `json:"ttl,omitempty"` } + +func ConvertRawRecords(domains []*DomainConfig) error { + + var err error + + for _, dc := range domains { + + for _, rawRec := range dc.RawRecords { + rec := &RecordConfig{ + Type: rawRec.Type, + TTL: rawRec.TTL, + Name: rawRec.Args[0].(string), + Metadata: map[string]string{}, + } + + // Copy the metadata (convert everything to string) + for _, m := range rawRec.Metas { + for mk, mv := range m { + if v, ok := mv.(string); ok { + rec.Metadata[mk] = v // Already a string. No new malloc. + } else { + rec.Metadata[mk] = fmt.Sprintf("%v", mv) + } + } + } + + // Call the proper initialize function. + // TODO(tlim): Good candiate for an interface or a lookup table. + + label := rawRec.Args[0].(string) + args := rawRec.Args[1:] + switch rawRec.Type { + + case rtypecfsingleredirect.Name: + rdata, error := rtypecfsingleredirect.FromRawArgs(args, label) + if error != nil { + return err + } + rec.Seal(dc.Name, label, rdata) + + // case "MX": + // rdata, error := rtypemx.FromRawArgs(args) + // if error != nil { + // return err + // } + // rec.Seal(dc.Name, label, rdata) + + default: + err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type) + } + if err != nil { + return fmt.Errorf("%s (%q, %q) record error: %w", rawRec.Type, rec.Name, dc.Name, err) + } + + // Free memeory: + clear(rawRec.Args) + rawRec.Args = nil + + dc.Records = append(dc.Records, rec) + } + clear(dc.RawRecords) + dc.RawRecords = nil + } + + return nil +} diff --git a/models/rdata.go b/models/rdata.go new file mode 100644 index 0000000000..db25aaeea2 --- /dev/null +++ b/models/rdata.go @@ -0,0 +1,33 @@ +package models + +// Rdataer is an interface for resource types. +type Rdataer interface { + // Return the rtype name used in RecordConfig.Name ("MX", etc.) + Name() string + + // Pre-compute the value stored in RecordConfig.target. + ComputeTarget() string + + // Pre-compute the value stored in RecordConfig.ComparableMini. + ComputeComparableMini() string +} + +// Seal finalizes a RecordConfig by setting .Rdata and pre-computing +// various values. +func (rc *RecordConfig) Seal(zone string, shortLabel string, rdata Rdataer) { + rc.Type = rdata.Name() + rc.SetLabel(shortLabel, zone) + rc.Rdata = rdata + + rc.ReSeal() // Fill in the pre-computed fields. +} + +// ReSeal re-computes the fields that are pre-computed. +func (rc *RecordConfig) ReSeal() { + if rc.Rdata == nil { + panic("Uninitialized Rdata") + } + //fmt.Printf("DEBUG: RESEAL target=%q\n", rc.Rdata.ComputeTarget()) + rc.SetTarget(rc.Rdata.ComputeTarget()) + rc.ComparableMini = rc.Rdata.ComputeComparableMini() +} diff --git a/models/record.go b/models/record.go index d83a11d7d7..bfa0da1aed 100644 --- a/models/record.go +++ b/models/record.go @@ -98,6 +98,9 @@ type RecordConfig struct { TTL uint32 `json:"ttl,omitempty"` Metadata map[string]string `json:"meta,omitempty"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. + // + Rdata Rdataer `json:"rdata,omitempty"` // The Resource Record data (RData) + ComparableMini string `json:"-"` // Pre-Computed string used to compare equality of two Rdatas // If you add a field to this struct, also add it to the list in the UnmarshalJSON function. MxPreference uint16 `json:"mxpreference,omitempty"` @@ -142,31 +145,6 @@ type RecordConfig struct { R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` UnknownTypeName string `json:"unknown_type_name,omitempty"` - - // Cloudflare-specific fields: - // When these are used, .target is set to a human-readable version (only to be used for display purposes). - CloudflareRedirect *CloudflareSingleRedirectConfig `json:"cloudflareapi_redirect,omitempty"` -} - -// CloudflareSingleRedirectConfig contains info about a Cloudflare Single Redirect. -// -// When these are used, .target is set to a human-readable version (only to be used for display purposes). -type CloudflareSingleRedirectConfig struct { - // - Code uint16 `json:"code,omitempty"` // 301 or 302 - // PR == PageRule - PRWhen string `json:"pr_when,omitempty"` - PRThen string `json:"pr_then,omitempty"` - PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule. - PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_REDIRECT/CF_TEMP_REDIRECT - // - // 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 } // MarshalJSON marshals RecordConfig. @@ -198,7 +176,9 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error { TTL uint32 `json:"ttl,omitempty"` Metadata map[string]string `json:"meta,omitempty"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. - Args []any `json:"args,omitempty"` + // + Rdata Rdataer `json:"rdata,omitempty"` // The Resource Record data (RData) + ComparableMini string `json:"-"` // Pre-Computed string used to compare equality of two Rdatas MxPreference uint16 `json:"mxpreference,omitempty"` SrvPriority uint16 `json:"srvpriority,omitempty"` @@ -355,6 +335,14 @@ func (rc *RecordConfig) GetLabelFQDN() string { // metafields. Provider-specific metafields like CF_PROXY are not the same as // pseudo-records like ANAME or R53_ALIAS func (rc *RecordConfig) ToComparableNoTTL() string { + + // rtype2.0 records pre-compute this answer. Once all other RecordConfig + // types are converted to rtype2.0 this function can be replaced with + // accesses to rc.ComparableMini. + if rc.ComparableMini != "" { + return rc.ComparableMini + } + switch rc.Type { case "SOA": return fmt.Sprintf("%s %v %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl) diff --git a/pkg/create/create.go b/pkg/create/create.go new file mode 100644 index 0000000000..84b28e8e3a --- /dev/null +++ b/pkg/create/create.go @@ -0,0 +1,21 @@ +package create + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" +) + +// Helper functions (one per rtype2.0) that create a RecordConfig of +// this type. +// In the future, this will be autogenerated. + +// CloudFlareSingleRedirect creates a RecordConfig of type CloudFlareSingleRedirect. +func CloudFlareSingleRedirect(ttl uint32, origin, label string, items []any) (*models.RecordConfig, error) { + r := &models.RecordConfig{TTL: ttl} + rd, err := rtypecfsingleredirect.FromRawArgs(items, label) + if err != nil { + return nil, err + } + r.Seal(origin, label, rd) + return r, nil +} diff --git a/pkg/js/js.go b/pkg/js/js.go index 06d9a7d300..693264659c 100644 --- a/pkg/js/js.go +++ b/pkg/js/js.go @@ -46,7 +46,10 @@ func ExecuteJavaScript(file string, devMode bool, variables map[string]string) ( // Record the directory path leading up to this file. currentDirectory = filepath.Dir(file) - return ExecuteJavascriptString(script, devMode, variables) + dnsConfig, err := ExecuteJavascriptString(script, devMode, variables) + models.ConvertRawRecords(dnsConfig.Domains) + + return dnsConfig, err } // ExecuteJavascriptString accepts a string containing javascript and runs it, returning the resulting dnsConfig. diff --git a/pkg/js/parse_tests/050-cfSingleRedirect.json b/pkg/js/parse_tests/050-cfSingleRedirect.json index 8b4965c31f..6360187415 100644 --- a/pkg/js/parse_tests/050-cfSingleRedirect.json +++ b/pkg/js/parse_tests/050-cfSingleRedirect.json @@ -14,64 +14,88 @@ "meta": "value" }, "target": "1.2.3.4" - } - ], - "rawrecords": [ + }, { "type": "CLOUDFLAREAPI_SINGLE_REDIRECT", - "args": [ - "name1", - 301, - "when1", - "then1" - ] + "name": "name1", + "rdata": { + "code": 301, + "pr_when": "UNKNOWABLE", + "pr_then": "UNKNOWABLE", + "pr_display": "UNKNOWABLE", + "sr_name": "name1", + "sr_when": "when1", + "sr_then": "then1", + "sr_display": "name1 code=(301) when=(when1) then=(then1)" + }, + "target": "name1" }, { "type": "CLOUDFLAREAPI_SINGLE_REDIRECT", - "args": [ - "name2", - 302, - "when2", - "then2" - ] + "name": "name2", + "rdata": { + "code": 302, + "pr_when": "UNKNOWABLE", + "pr_then": "UNKNOWABLE", + "pr_display": "UNKNOWABLE", + "sr_name": "name2", + "sr_when": "when2", + "sr_then": "then2", + "sr_display": "name2 code=(302) when=(when2) then=(then2)" + }, + "target": "name2" }, { "type": "CLOUDFLAREAPI_SINGLE_REDIRECT", - "args": [ - "name3", - "301", - "when3", - "then3" - ] + "name": "name3", + "rdata": { + "code": 301, + "pr_when": "UNKNOWABLE", + "pr_then": "UNKNOWABLE", + "pr_display": "UNKNOWABLE", + "sr_name": "name3", + "sr_when": "when3", + "sr_then": "then3", + "sr_display": "name3 code=(301) when=(when3) then=(then3)" + }, + "target": "name3" }, { "type": "CLOUDFLAREAPI_SINGLE_REDIRECT", - "args": [ - "namettl", - 302, - "whenttl", - "thenttl" - ], - "ttl": 999 + "name": "namettl", + "ttl": 999, + "rdata": { + "code": 302, + "pr_when": "UNKNOWABLE", + "pr_then": "UNKNOWABLE", + "pr_display": "UNKNOWABLE", + "sr_name": "namettl", + "sr_when": "whenttl", + "sr_then": "thenttl", + "sr_display": "namettl code=(302) when=(whenttl) then=(thenttl)" + }, + "target": "namettl" }, { "type": "CLOUDFLAREAPI_SINGLE_REDIRECT", - "args": [ - "namemeta", - 302, - "whenmeta", - "thenmeta" - ], - "metas": [ - { - "metastr": "stringy" - }, - { - "metanum": 22 - } - ] + "name": "namemeta", + "meta": { + "metanum": "22", + "metastr": "stringy" + }, + "rdata": { + "code": 302, + "pr_when": "UNKNOWABLE", + "pr_then": "UNKNOWABLE", + "pr_display": "UNKNOWABLE", + "sr_name": "namemeta", + "sr_when": "whenmeta", + "sr_then": "thenmeta", + "sr_display": "namemeta code=(302) when=(whenmeta) then=(thenmeta)" + }, + "target": "namemeta" } ] } ] -} \ No newline at end of file +} diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 1094f1127a..e7f41c88ce 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/pkg/transform" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns/dnsutil" @@ -79,7 +80,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin } _, ok := validTypes[rec.Type] if !ok { - cType := providers.GetCustomRecordType(rec.Type) + cType := rtypecontrol.GetCustomRecordType(rec.Type) if cType == nil { return fmt.Errorf("unsupported record type (%v) domain=%v name=%v", rec.Type, domain, rec.GetLabel()) } diff --git a/pkg/rtypecontrol/rtypecontrol.go b/pkg/rtypecontrol/rtypecontrol.go index f40fdf48bf..62b79c6d2d 100644 --- a/pkg/rtypecontrol/rtypecontrol.go +++ b/pkg/rtypecontrol/rtypecontrol.go @@ -1,21 +1,54 @@ package rtypecontrol -import "github.com/StackExchange/dnscontrol/v4/providers" +var validTypes = map[string]RegisterTypeOpts{} -var validTypes = map[string]struct{}{} +type RegisterTypeOpts = struct { + Name string + FromRawArgsFn func(items []any) (*any, error) +} + +func Register(ri RegisterTypeOpts) { -func Register(t string) { // Does this already exist? - if _, ok := validTypes[t]; ok { + if _, ok := validTypes[ri.Name]; ok { panic("rtype %q already registered. Can't register it a second time!") } - validTypes[t] = struct{}{} + validTypes[ri.Name] = ri - providers.RegisterCustomRecordType(t, "", "") + // Do it the old way for backwards compatibility. + RegisterCustomRecordType(ri.Name, "", "") } func IsValid(t string) bool { _, ok := validTypes[t] return ok } + +func Info(name string) RegisterTypeOpts { + return validTypes[name] +} + +// Legacy functions + +// CustomRType stores an rtype that is only valid for this DSP. +type CustomRType struct { + Name string + Provider string + RealType string +} + +// RegisterCustomRecordType registers a record type that is only valid for one provider. +// provider is the registered type of provider this is valid with +// name is the record type as it will appear in the js. (should be something like $PROVIDER_FOO) +// realType is the record type it will be replaced with after validation +func RegisterCustomRecordType(name, provider, realType string) { + customRecordTypes[name] = &CustomRType{Name: name, Provider: provider, RealType: realType} +} + +// GetCustomRecordType returns a registered custom record type, or nil if none +func GetCustomRecordType(rType string) *CustomRType { + return customRecordTypes[rType] +} + +var customRecordTypes = map[string]*CustomRType{} diff --git a/pkg/rtypes/postprocess.go b/pkg/rtypes/postprocess.go deleted file mode 100644 index 91a14ddad7..0000000000 --- a/pkg/rtypes/postprocess.go +++ /dev/null @@ -1,60 +0,0 @@ -package rtypes - -import ( - "fmt" - - "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect" -) - -func PostProcess(domains []*models.DomainConfig) error { - - var err error - - for _, dc := range domains { - - for _, rawRec := range dc.RawRecords { - rec := &models.RecordConfig{ - Type: rawRec.Type, - TTL: rawRec.TTL, - Name: rawRec.Args[0].(string), - Metadata: map[string]string{}, - } - - // Copy the metadata (convert everything to string) - for _, m := range rawRec.Metas { - for mk, mv := range m { - if v, ok := mv.(string); ok { - rec.Metadata[mk] = v // Already a string. No new malloc. - } else { - rec.Metadata[mk] = fmt.Sprintf("%v", mv) - } - } - } - - // Call the proper initialize function. - // TODO(tlim): Good candiate for an interface or a lookup table. - switch rawRec.Type { - - case "CLOUDFLAREAPI_SINGLE_REDIRECT": - err = cfsingleredirect.FromRaw(rec, rawRec.Args) - rec.SetLabel("@", dc.Name) - - default: - err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type) - } - if err != nil { - return fmt.Errorf("%s (%q, %q) record error: %w", rawRec.Type, rec.Name, dc.Name, err) - } - - // Free memeory: - clear(rawRec.Args) - rawRec.Args = nil - - dc.Records = append(dc.Records, rec) - } - dc.RawRecords = nil - } - - return nil -} diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go index 64c5a673a4..463070787c 100644 --- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go +++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go @@ -17,6 +17,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -56,7 +57,7 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("AKAMAICDN", providerName, "") + rtypecontrol.RegisterCustomRecordType("AKAMAICDN", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 856077ba7b..92a8c0119a 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -14,6 +14,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -85,7 +86,7 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("AZURE_ALIAS", providerName, "") + rtypecontrol.RegisterCustomRecordType("AZURE_ALIAS", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/azureprivatedns/azurePrivateDnsProvider.go b/providers/azureprivatedns/azurePrivateDnsProvider.go index 2e2b05fe63..fa7c12bd2b 100644 --- a/providers/azureprivatedns/azurePrivateDnsProvider.go +++ b/providers/azureprivatedns/azurePrivateDnsProvider.go @@ -14,6 +14,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -89,7 +90,7 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("AZURE_ALIAS", providerName, "") + rtypecontrol.RegisterCustomRecordType("AZURE_ALIAS", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 89b090b0cb..c687012958 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -15,9 +15,10 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/pkg/transform" "github.com/StackExchange/dnscontrol/v4/providers" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" "github.com/cloudflare/cloudflare-go" "github.com/fatih/color" ) @@ -71,9 +72,9 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("CF_REDIRECT", providerName, "") - providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", providerName, "") - providers.RegisterCustomRecordType("CF_WORKER_ROUTE", providerName, "") + rtypecontrol.RegisterCustomRecordType("CF_REDIRECT", providerName, "") + rtypecontrol.RegisterCustomRecordType("CF_TEMP_REDIRECT", providerName, "") + rtypecontrol.RegisterCustomRecordType("CF_WORKER_ROUTE", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } @@ -315,18 +316,18 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom case "PAGE_RULE": return []*models.Correction{{ Msg: msg, - F: func() error { return c.createPageRule(domainID, *newrec.CloudflareRedirect) }, + F: func() error { return c.createPageRule(domainID, *newrec.AsCloudflareSingleRedirect()) }, }} case "WORKER_ROUTE": return []*models.Correction{{ Msg: msg, F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) }, }} - case cfsingleredirect.SINGLEREDIRECT: + case rtypecfsingleredirect.Name: return []*models.Correction{{ Msg: msg, F: func() error { - return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect) + return c.createSingleRedirect(domainID, *newrec.AsCloudflareSingleRedirect()) }, }} default: @@ -342,8 +343,8 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon idTxt = oldrec.Original.(cloudflare.PageRule).ID case "WORKER_ROUTE": idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID - case cfsingleredirect.SINGLEREDIRECT: - idTxt = oldrec.CloudflareRedirect.SRRRulesetID + case rtypecfsingleredirect.Name: + idTxt = oldrec.AsCloudflareSingleRedirect().SRRRulesetID default: idTxt = oldrec.Original.(cloudflare.DNSRecord).ID } @@ -354,10 +355,10 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon return []*models.Correction{{ Msg: msg, F: func() error { - return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect) + return c.updatePageRule(idTxt, domainID, *newrec.AsCloudflareSingleRedirect()) }, }} - case cfsingleredirect.SINGLEREDIRECT: + case rtypecfsingleredirect.Name: return []*models.Correction{{ Msg: msg, F: func() error { @@ -390,7 +391,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models. idTxt = origRec.Original.(cloudflare.PageRule).ID case "WORKER_ROUTE": idTxt = origRec.Original.(cloudflare.WorkerRoute).ID - case cfsingleredirect.SINGLEREDIRECT: + case rtypecfsingleredirect.Name: idTxt = origRec.Original.(cloudflare.RulesetRule).ID default: idTxt = origRec.Original.(cloudflare.DNSRecord).ID @@ -405,8 +406,8 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models. return c.deletePageRule(origRec.Original.(cloudflare.PageRule).ID, domainID) case "WORKER_ROUTE": return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID) - case cfsingleredirect.SINGLEREDIRECT: - return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect) + case rtypecfsingleredirect.Name: + return c.deleteSingleRedirects(domainID, *origRec.AsCloudflareSingleRedirect()) default: return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID) } @@ -548,7 +549,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { prPriority++ // Convert this record to a PAGE_RULE. - cfsingleredirect.MakePageRule(rec, prPriority, code, prWhen, prThen) + makePageRule(rec, prPriority, code, prWhen, prThen) rec.SetLabel("@", dc.Name) if c.manageRedirects && !c.manageSingleRedirects { @@ -556,8 +557,8 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } else if !c.manageRedirects && c.manageSingleRedirects { // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT. - cfsingleredirect.TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { + TranscodePRtoSR(rec) + if err := c.LogTranscode(dc.Name, rec.AsCloudflareSingleRedirect()); err != nil { return err } @@ -571,8 +572,8 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { return err } // The copy becomes the CF SingleRedirect - cfsingleredirect.TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { + TranscodePRtoSR(rec) + if err := c.LogTranscode(dc.Name, rec.AsCloudflareSingleRedirect()); err != nil { return err } // Append the copy to the end of the list. @@ -581,7 +582,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { // The original PAGE_RULE remains untouched. } - } else if rec.Type == cfsingleredirect.SINGLEREDIRECT { + } else if rec.Type == rtypecfsingleredirect.Name { // SINGLEREDIRECT record types. Verify they are enabled. if !c.manageSingleRedirects { return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_SINGLE__REDIRECT records") @@ -623,7 +624,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { return nil } -func (c *cloudflareProvider) LogTranscode(zone string, redirect *models.CloudflareSingleRedirectConfig) error { +func (c *cloudflareProvider) LogTranscode(zone string, redirect *rtypecfsingleredirect.SingleRedirect) error { // No filename? Don't log anything. filename := c.tcLogFilename if filename == "" { diff --git a/providers/cloudflare/rtypes/cfsingleredirect/convert.go b/providers/cloudflare/convert.go similarity index 84% rename from providers/cloudflare/rtypes/cfsingleredirect/convert.go rename to providers/cloudflare/convert.go index 40ada49867..77a73702a4 100644 --- a/providers/cloudflare/rtypes/cfsingleredirect/convert.go +++ b/providers/cloudflare/convert.go @@ -1,4 +1,4 @@ -package cfsingleredirect +package cloudflare import ( "fmt" @@ -7,14 +7,15 @@ import ( "strings" "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" ) // TranscodePRtoSR takes a PAGE_RULE record, stores transcoded versions of the fields, and makes the record a CLOUDFLAREAPI_SINGLE_REDDIRECT. func TranscodePRtoSR(rec *models.RecordConfig) error { - rec.Type = SINGLEREDIRECT // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT + rec.Type = rtypecfsingleredirect.Name // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT // Extract the fields we're reading from: - sr := rec.CloudflareRedirect + sr := rec.AsCloudflareSingleRedirect() code := sr.Code prWhen := sr.PRWhen prThen := sr.PRThen @@ -36,6 +37,33 @@ func TranscodePRtoSR(rec *models.RecordConfig) error { return nil } +// makeSingleRedirectFromConvert updates a RecordConfig to be a SINGLEREDIRECT using data from a PAGE_RULE conversion. +func makeSingleRedirectFromConvert(rc *models.RecordConfig, + priority int, + prWhen, prThen string, + code uint16, + srName, srWhen, srThen string) { + + srDisplay := targetFromConverted(priority, code, prWhen, prThen, srWhen, srThen) + + rc.Type = rtypecfsingleredirect.Name + rc.TTL = 1 + sr := rc.AsCloudflareSingleRedirect() + sr.Code = code + + sr.SRName = srName + sr.SRWhen = srWhen + sr.SRThen = srThen + sr.SRDisplay = srDisplay + + rc.SetTarget(rc.AsCloudflareSingleRedirect().SRDisplay) +} + +// targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE. +func targetFromConverted(prPriority int, code uint16, prWhen, prThen, srWhen, srThen string) string { + return fmt.Sprintf("%d,%03d,%s,%s code=(%03d) when=(%s) then=(%s)", prPriority, code, prWhen, prThen, code, srWhen, srThen) +} + // makeRuleFromPattern compile old-style patterns and replacements into new-style rules and expressions. func makeRuleFromPattern(pattern, replacement string) (string, string, error) { diff --git a/providers/cloudflare/makers.go b/providers/cloudflare/makers.go new file mode 100644 index 0000000000..0496326437 --- /dev/null +++ b/providers/cloudflare/makers.go @@ -0,0 +1,30 @@ +package cloudflare + +import ( + "fmt" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" +) + +// makePageRule updates a RecordConfig to be a PAGE_RULE using PAGE_RULE data. +func makePageRule(rc *models.RecordConfig, priority int, code uint16, when, then string) { + display := mkPageRuleBlob(priority, code, when, then) + + rc.Type = "PAGE_RULE" + rc.TTL = 1 + rc.Rdata = &rtypecfsingleredirect.SingleRedirect{ + Code: code, + // + PRWhen: when, + PRThen: then, + PRPriority: priority, + PRDisplay: display, + } + rc.SetTarget(display) +} + +// mkPageRuleBlob creates the 1,301,when,then string used in displays. +func mkPageRuleBlob(priority int, code uint16, when, then string) string { + return fmt.Sprintf("%d,%03d,%s,%s", priority, code, when, then) +} diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 860d8cc5c9..5beba40ffe 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/idna" "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" "github.com/cloudflare/cloudflare-go" ) @@ -302,11 +302,12 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo srThen := pr.ActionParameters.FromValue.TargetURL.Expression code := uint16(pr.ActionParameters.FromValue.StatusCode) - cfsingleredirect.MakeSingleRedirectFromAPI(r, code, srName, srWhen, srThen) + makeSingleRedirectFromAPI(r, code, srName, srWhen, srThen) + r.ReSeal() r.SetLabel("@", domain) // Store the IDs - sr := r.CloudflareRedirect + sr := r.AsCloudflareSingleRedirect() sr.SRRRulesetID = rules.ID sr.SRRRulesetRuleID = pr.ID @@ -316,7 +317,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo return recs, nil } -func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.CloudflareSingleRedirectConfig) error { +func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr rtypecfsingleredirect.SingleRedirect) error { newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{} newSingleRedirectRule := cloudflare.RulesetRule{} @@ -360,7 +361,7 @@ func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.Cl return err } -func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr models.CloudflareSingleRedirectConfig) error { +func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr rtypecfsingleredirect.SingleRedirect) error { // This block should delete rules using the as is Cloudflare Golang lib in theory, need to debug why it isn't // updatedRuleset := cloudflare.UpdateEntrypointRulesetParams{} @@ -402,10 +403,10 @@ func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr models.C } func (c *cloudflareProvider) updateSingleRedirect(domainID string, oldrec, newrec *models.RecordConfig) error { - if err := c.deleteSingleRedirects(domainID, *oldrec.CloudflareRedirect); err != nil { + if err := c.deleteSingleRedirects(domainID, *oldrec.AsCloudflareSingleRedirect()); err != nil { return err } - return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect) + return c.createSingleRedirect(domainID, *newrec.AsCloudflareSingleRedirect()) } func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { @@ -434,7 +435,7 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R then := value["url"].(string) currentPrPrio := pr.Priority - cfsingleredirect.MakePageRule(r, currentPrPrio, code, when, then) + makePageRule(r, currentPrPrio, code, when, then) r.SetLabel("@", domain) recs = append(recs, r) @@ -446,7 +447,7 @@ func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error { return c.cfClient.DeletePageRule(context.Background(), domainID, recordID) } -func (c *cloudflareProvider) updatePageRule(recordID, domainID string, cfr models.CloudflareSingleRedirectConfig) error { +func (c *cloudflareProvider) updatePageRule(recordID, domainID string, cfr rtypecfsingleredirect.SingleRedirect) error { // maybe someday? //c.apiProvider.UpdatePageRule(context.Background(), domainId, recordID, ) if err := c.deletePageRule(recordID, domainID); err != nil { @@ -455,7 +456,7 @@ func (c *cloudflareProvider) updatePageRule(recordID, domainID string, cfr model return c.createPageRule(domainID, cfr) } -func (c *cloudflareProvider) createPageRule(domainID string, cfr models.CloudflareSingleRedirectConfig) error { +func (c *cloudflareProvider) createPageRule(domainID string, cfr rtypecfsingleredirect.SingleRedirect) error { priority := cfr.PRPriority code := cfr.Code prWhen := cfr.PRWhen diff --git a/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go deleted file mode 100644 index a00ca4a5dc..0000000000 --- a/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go +++ /dev/null @@ -1,40 +0,0 @@ -package cfsingleredirect - -import ( - "fmt" - - "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" -) - -// SINGLEREDIRECT is the string name for this rType. -const SINGLEREDIRECT = "CLOUDFLAREAPI_SINGLE_REDIRECT" - -func init() { - rtypecontrol.Register(SINGLEREDIRECT) -} - -// FromRaw convert RecordConfig using data from a RawRecordConfig's parameters. -func FromRaw(rc *models.RecordConfig, items []any) error { - - // Validate types. - if err := rtypecontrol.PaveArgs(items, "siss"); err != nil { - return err - } - - // Unpack the args: - var name, when, then string - var code uint16 - - name = items[0].(string) - code = items[1].(uint16) - if code != 301 && code != 302 { - return fmt.Errorf("code (%03d) is not 301 or 302", code) - } - when = items[2].(string) - then = items[3].(string) - - makeSingleRedirectFromRawRec(rc, code, name, when, then) - - return nil -} diff --git a/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go b/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go deleted file mode 100644 index 54e9bef7e2..0000000000 --- a/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package cfsingleredirect - -import ( - "regexp" - "testing" - - "github.com/gobwas/glob" -) - -func Test_makeSingleDirectRule(t *testing.T) { - tests := []struct { - name string - // - pattern string - replace string - // - wantMatch string - wantExpr string - wantErr bool - }{ - { - name: "000", - pattern: "example.com/", - replace: "foo.com", - wantMatch: `http.host eq "example.com" and http.request.uri.path eq "/"`, - wantExpr: `concat("https://foo.com", "")`, - wantErr: false, - }, - - /* - All the test-cases I could find in dnsconfig.js - - Generated with this: - - dnscontrol print-ir --pretty |grep '"target' |grep , | sed -e 's@"target":@@g' > /tmp/list - vim /tmp/list # removed the obvious duplicates - awk < /tmp/list -v q='"' -F, '{ print "{" ; print "name: " q NR q "," ; print "pattern: " $1 q "," ; print "replace: " q $2 "," ; print "wantMatch: `FIXME`," ; print "wantExpr: `FIXME`," ; print "wantErr: false," ; print "}," }' | pbcopy - - */ - - { - name: "1", - pattern: "https://i-dev.sstatic.net/", - replace: "https://stackexchange.com/", - wantMatch: `http.host eq "i-dev.sstatic.net" and http.request.uri.path eq "/"`, - wantExpr: `concat("https://stackexchange.com/", "")`, - wantErr: false, - }, - { - name: "2", - pattern: "https://i.stack.imgur.com/*", - replace: "https://i.sstatic.net/$1", - wantMatch: `http.host eq "i.stack.imgur.com"`, - wantExpr: `concat("https://i.sstatic.net", http.request.uri.path)`, - wantErr: false, - }, - { - name: "3", - pattern: "https://img.stack.imgur.com/*", - replace: "https://i.sstatic.net/$1", - wantMatch: `http.host eq "img.stack.imgur.com"`, - wantExpr: `concat("https://i.sstatic.net", http.request.uri.path)`, - wantErr: false, - }, - { - name: "4", - pattern: "https://insights.stackoverflow.com/", - replace: "https://survey.stackoverflow.co", - wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/"`, - wantExpr: `concat("https://survey.stackoverflow.co", "")`, - wantErr: false, - }, - { - name: "5", - pattern: "https://insights.stackoverflow.com/trends", - replace: "https://trends.stackoverflow.co", - wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/trends"`, - wantExpr: `concat("https://trends.stackoverflow.co", "")`, - wantErr: false, - }, - { - name: "6", - pattern: "https://insights.stackoverflow.com/trends/", - replace: "https://trends.stackoverflow.co", - wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/trends/"`, - wantExpr: `concat("https://trends.stackoverflow.co", "")`, - wantErr: false, - }, - { - name: "7", - pattern: "https://insights.stackoverflow.com/survey/2021", - replace: "https://survey.stackoverflow.co/2021", - wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/survey/2021"`, - wantExpr: `concat("https://survey.stackoverflow.co/2021", "")`, - wantErr: false, - }, - { - name: "28", - pattern: "*stackoverflow.help/support/solutions/articles/36000241656-write-an-article", - replace: "https://stackoverflow.help/en/articles/4397209-write-an-article", - wantMatch: `( http.host eq "stackoverflow.help" or ends_with(http.host, ".stackoverflow.help") ) and http.request.uri.path eq "/support/solutions/articles/36000241656-write-an-article"`, - wantExpr: `concat("https://stackoverflow.help/en/articles/4397209-write-an-article", "")`, - wantErr: false, - }, - { - name: "29", - pattern: "*stackoverflow.careers/*", - replace: "https://careers.stackoverflow.com/$2", - wantMatch: `http.host eq "stackoverflow.careers" or ends_with(http.host, ".stackoverflow.careers")`, - wantExpr: `concat("https://careers.stackoverflow.com", http.request.uri.path)`, - wantErr: false, - }, - { - name: "31", - pattern: "stackenterprise.com/*", - replace: "https://stackoverflow.co/teams/", - wantMatch: `http.host eq "stackenterprise.com"`, - wantExpr: `concat("https://stackoverflow.co/teams/", "")`, - wantErr: false, - }, - { - name: "33", - pattern: "meta.*yodeya.com/*", - replace: "https://judaism.meta.stackexchange.com/$2", - wantMatch: `http.host matches r###"^meta\..*yodeya\.com$"###`, - wantExpr: `concat("https://judaism.meta.stackexchange.com", http.request.uri.path)`, - wantErr: false, - }, - { - name: "34", - pattern: "chat.*yodeya.com/*", - replace: "https://chat.stackexchange.com/?tab=site\u0026host=judaism.stackexchange.com", - wantMatch: `http.host matches r###"^chat\..*yodeya\.com$"###`, - wantExpr: `concat("https://chat.stackexchange.com/?tab=site&host=judaism.stackexchange.com", "")`, - wantErr: false, - }, - { - name: "35", - pattern: "*yodeya.com/*", - replace: "https://judaism.stackexchange.com/$2", - wantMatch: `http.host eq "yodeya.com" or ends_with(http.host, ".yodeya.com")`, - wantExpr: `concat("https://judaism.stackexchange.com", http.request.uri.path)`, - wantErr: false, - }, - { - name: "36", - pattern: "meta.*seasonedadvice.com/*", - replace: "https://cooking.meta.stackexchange.com/$2", - wantMatch: `http.host matches r###"^meta\..*seasonedadvice\.com$"###`, - wantExpr: `concat("https://cooking.meta.stackexchange.com", http.request.uri.path)`, - wantErr: false, - }, - { - name: "70", - pattern: "collectivesonstackoverflow.co/*", - replace: "https://stackoverflow.com/collectives-on-stack-overflow", - wantMatch: `http.host eq "collectivesonstackoverflow.co"`, - wantExpr: `concat("https://stackoverflow.com/collectives-on-stack-overflow", "")`, - wantErr: false, - }, - { - name: "71", - pattern: "*collectivesonstackoverflow.co/*", - replace: "https://stackoverflow.com/collectives-on-stack-overflow", - wantMatch: `http.host eq "collectivesonstackoverflow.co" or ends_with(http.host, ".collectivesonstackoverflow.co")`, - wantExpr: `concat("https://stackoverflow.com/collectives-on-stack-overflow", "")`, - wantErr: false, - }, - { - name: "76", - pattern: "*stackexchange.ca/*", - replace: "https://stackexchange.com/$2", - wantMatch: `http.host eq "stackexchange.ca" or ends_with(http.host, ".stackexchange.ca")`, - wantExpr: `concat("https://stackexchange.com", http.request.uri.path)`, - wantErr: false, - }, - - // https://github.com/StackExchange/dnscontrol/issues/2313#issuecomment-2197296025 - { - name: "pro-sumer1", - pattern: "domain.tld/.well-known*", - replace: "https://social.domain.tld/.well-known$1", - wantMatch: `(starts_with(http.request.uri.path, "/.well-known") and http.host eq "domain.tld")`, - wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`, - wantErr: false, - }, - { - name: "pro-sumer2", - pattern: "domain.tld/users*", - replace: "https://social.domain.tld/users$1", - wantMatch: `(starts_with(http.request.uri.path, "/users") and http.host eq "domain.tld")`, - wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`, - wantErr: false, - }, - { - name: "pro-sumer3", - pattern: "domain.tld/@*", - replace: `https://social.domain.tld/@$1`, - wantMatch: `(starts_with(http.request.uri.path, "/@") and http.host eq "domain.tld")`, - wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`, - wantErr: false, - }, - - { - name: "stackentwild", - pattern: "*stackoverflowenterprise.com/*", - replace: "https://www.stackoverflowbusiness.com/enterprise/$2", - wantMatch: `http.host eq "stackoverflowenterprise.com" or ends_with(http.host, ".stackoverflowenterprise.com")`, - wantExpr: `concat("https://www.stackoverflowbusiness.com", "/enterprise", http.request.uri.path)`, - wantErr: false, - }, - - // - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotMatch, gotExpr, err := makeRuleFromPattern(tt.pattern, tt.replace) - if (err != nil) != tt.wantErr { - t.Errorf("makeSingleDirectRule() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotMatch != tt.wantMatch { - t.Errorf("makeSingleDirectRule() MATCH = %v\n want %v", gotMatch, tt.wantMatch) - } - if gotExpr != tt.wantExpr { - t.Errorf("makeSingleDirectRule() EXPR = %v\n want %v", gotExpr, tt.wantExpr) - } - //_ = gotType - }) - } -} - -func Test_normalizeURL(t *testing.T) { - tests := []struct { - name string - s string - want string - want1 string - want2 string - wantErr bool - }{ - { - s: "foo.com", - want: "https://foo.com", - want1: "foo.com", - want2: "", - }, - { - s: "http://foo.com", - want: "https://foo.com", - want1: "foo.com", - want2: "", - }, - { - s: "https://foo.com", - want: "https://foo.com", - want1: "foo.com", - want2: "", - }, - - { - s: "foo.com/bar", - want: "https://foo.com/bar", - want1: "foo.com", - want2: "/bar", - }, - { - s: "http://foo.com/bar", - want: "https://foo.com/bar", - want1: "foo.com", - want2: "/bar", - }, - { - s: "https://foo.com/bar", - want: "https://foo.com/bar", - want1: "foo.com", - want2: "/bar", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, got1, got2, err := normalizeURL(tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("normalizeURL() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("normalizeURL() got = %v, want %v", got, tt.want) - } - if got1 != tt.want1 { - t.Errorf("normalizeURL() got1 = %v, want1 %v", got1, tt.want1) - } - if got2 != tt.want2 { - t.Errorf("normalizeURL() got2 = %v, want2 %v", got2, tt.want2) - } - }) - } -} - -func Test_simpleGlobToRegex(t *testing.T) { - tests := []struct { - name string - pattern string - want string - }{ - {"1", `foo`, `^foo$`}, - {"2", `fo.o`, `^fo\.o$`}, - {"3", `*foo`, `.*foo$`}, - {"4", `foo*`, `^foo.*`}, - {"5", `f.oo*`, `^f\.oo.*`}, - {"6", `f*oo*`, `^f.*oo.*`}, - } - - data := []string{ - "bar", - "foo", - "foofoo", - "ONEfooTWO", - "fo", - "frankfodog", - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := simpleGlobToRegex(tt.pattern) - if got != tt.want { - t.Errorf("simpleGlobToRegex() = %v, want %v", got, tt.want) - } - - // Make sure the regex compiles and gets the same result when matching against strings in data. - for i, d := range data { - - rm, err := regexp.MatchString(got, d) - if err != nil { - t.Errorf("simpleGlobToRegex() = %003d can not compile: %v", i, err) - } - - g := glob.MustCompile(tt.pattern) - gm := g.Match(d) // true - - if gm != rm { - t.Errorf("simpleGlobToRegex() = %003d glob: %v '%v' regexp: %v '%v'", i, gm, tt.pattern, rm, got) - } - - } - }) - - } -} diff --git a/providers/cloudflare/rtypes/cfsingleredirect/from.go b/providers/cloudflare/rtypes/cfsingleredirect/from.go deleted file mode 100644 index f1bd3f71f0..0000000000 --- a/providers/cloudflare/rtypes/cfsingleredirect/from.go +++ /dev/null @@ -1,122 +0,0 @@ -package cfsingleredirect - -import ( - "fmt" - - "github.com/StackExchange/dnscontrol/v4/models" -) - -// MakePageRule updates a RecordConfig to be a PAGE_RULE using PAGE_RULE data. -func MakePageRule(rc *models.RecordConfig, priority int, code uint16, when, then string) { - display := mkPageRuleBlob(priority, code, when, then) - - rc.Type = "PAGE_RULE" - rc.TTL = 1 - rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{ - Code: code, - // - PRWhen: when, - PRThen: then, - PRPriority: priority, - PRDisplay: display, - } - rc.SetTarget(display) -} - -// mkPageRuleBlob creates the 1,301,when,then string used in displays. -func mkPageRuleBlob(priority int, code uint16, when, then string) string { - return fmt.Sprintf("%d,%03d,%s,%s", priority, code, when, then) -} - -// makeSingleRedirectFromRawRec updates a RecordConfig to be a -// SINGLEREDIRECT using the data from a RawRecord. -func makeSingleRedirectFromRawRec(rc *models.RecordConfig, code uint16, name, when, then string) { - target := targetFromRaw(name, code, when, then) - - rc.Type = SINGLEREDIRECT - rc.TTL = 1 - rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{ - Code: code, - // - PRWhen: "UNKNOWABLE", - PRThen: "UNKNOWABLE", - PRPriority: 0, - PRDisplay: "UNKNOWABLE", - // - SRName: name, - SRWhen: when, - SRThen: then, - SRDisplay: target, - } - rc.SetTarget(rc.CloudflareRedirect.SRDisplay) -} - -// targetFromRaw create the display text used for a normal Redirect. -func targetFromRaw(name string, code uint16, when, then string) string { - return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)", - name, - code, - when, - then, - ) -} - -// MakeSingleRedirectFromAPI updatese a RecordConfig to be a SINGLEREDIRECT using data downloaded via the API. -func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, then string) { - // The target is the same as the name. It is the responsibility of the record creator to name it something diffable. - target := targetFromAPIData(name, code, when, then) - - rc.Type = SINGLEREDIRECT - rc.TTL = 1 - rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{ - Code: code, - // - PRWhen: "UNKNOWABLE", - PRThen: "UNKNOWABLE", - PRPriority: 0, - PRDisplay: "UNKNOWABLE", - // - SRName: name, - SRWhen: when, - SRThen: then, - SRDisplay: target, - } - rc.SetTarget(rc.CloudflareRedirect.SRDisplay) -} - -// targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API. -func targetFromAPIData(name string, code uint16, when, then string) string { - return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)", - name, - code, - when, - then, - ) -} - -// makeSingleRedirectFromConvert updates a RecordConfig to be a SINGLEREDIRECT using data from a PAGE_RULE conversion. -func makeSingleRedirectFromConvert(rc *models.RecordConfig, - priority int, - prWhen, prThen string, - code uint16, - srName, srWhen, srThen string) { - - srDisplay := targetFromConverted(priority, code, prWhen, prThen, srWhen, srThen) - - rc.Type = SINGLEREDIRECT - rc.TTL = 1 - sr := rc.CloudflareRedirect - sr.Code = code - - sr.SRName = srName - sr.SRWhen = srWhen - sr.SRThen = srThen - sr.SRDisplay = srDisplay - - rc.SetTarget(rc.CloudflareRedirect.SRDisplay) -} - -// targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE. -func targetFromConverted(prPriority int, code uint16, prWhen, prThen, srWhen, srThen string) string { - return fmt.Sprintf("%d,%03d,%s,%s code=(%03d) when=(%s) then=(%s)", prPriority, code, prWhen, prThen, code, srWhen, srThen) -} diff --git a/providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go new file mode 100644 index 0000000000..12795548f9 --- /dev/null +++ b/providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go @@ -0,0 +1,107 @@ +package rtypecfsingleredirect + +import ( + "encoding/json" + "fmt" + + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" +) + +// Name is the string name for this rType. +const Name = "CLOUDFLAREAPI_SINGLE_REDIRECT" + +func init() { + rtypecontrol.Register(rtypecontrol.RegisterTypeOpts{ + Name: Name, + }) +} + +// SingleRedirect contains info about a Cloudflare Single Redirect. +type SingleRedirect struct { + // + Code uint16 `json:"code,omitempty"` // 301 or 302 + // PR == PageRule + PRWhen string `json:"pr_when,omitempty"` + PRThen string `json:"pr_then,omitempty"` + PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule. + PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_REDIRECT/CF_TEMP_REDIRECT + // + // 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 +} + +func (rdata *SingleRedirect) Name() string { + return Name +} + +func (rdata *SingleRedirect) ComputeTarget() string { + // The closest equivalent to a target "hostname" is the rule name. + return rdata.SRName +} + +func (rdata *SingleRedirect) ComputeComparableMini() string { + // The differencing engine uses this. + return rdata.SRDisplay +} + +func (rdata *SingleRedirect) MarshalJSON() ([]byte, error) { + return json.Marshal(*rdata) +} + +// FromRawArgs creates a Rdata... +// update a RecordConfig using the args (from a +// RawRecord.Args). In other words, use the data from dnsconfig.js's +// rawrecordBuilder to create (actually... update) a models.RecordConfig. +func FromRawArgs(items []any, name string) (*SingleRedirect, error) { + + // Pave the arguments. + if err := rtypecontrol.PaveArgs(items, "iss"); err != nil { + return nil, err + } + + // Unpack the arguments: + var code = items[0].(uint16) + if code != 301 && code != 302 { + return nil, fmt.Errorf("code (%03d) is not 301 or 302", code) + } + var when = items[1].(string) + var then = items[2].(string) + + // Use the arguments to perfect the record: + return makeSingleRedirect(code, name, when, then) +} + +// makeSingleRedirect +func makeSingleRedirect(code uint16, name, when, then string) (*SingleRedirect, error) { + target := targetFromRaw(name, code, when, then) + + rdata := &SingleRedirect{ + Code: code, + // + PRWhen: "UNKNOWABLE", + PRThen: "UNKNOWABLE", + PRPriority: 0, + PRDisplay: "UNKNOWABLE", + // + SRName: name, + SRWhen: when, + SRThen: then, + SRDisplay: target, + } + return rdata, nil +} + +// targetFromRaw create the display text used for a normal Redirect. +func targetFromRaw(name string, code uint16, when, then string) string { + return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)", + name, + code, + when, + then, + ) +} diff --git a/providers/cloudflare/singleredirect.go b/providers/cloudflare/singleredirect.go new file mode 100644 index 0000000000..f3bd8936f3 --- /dev/null +++ b/providers/cloudflare/singleredirect.go @@ -0,0 +1,41 @@ +package cloudflare + +import ( + "fmt" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" +) + +// makeSingleRedirectFromAPI updates a RecordConfig to be a CfSingleRedirect using data downloaded via the API. +func makeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, then string) { + // The target is the same as the name. It is the responsibility of the record creator to name it something diffable. + target := targetFromAPIData(name, code, when, then) + + rc.Type = rtypecfsingleredirect.Name + rc.TTL = 1 + rc.Rdata = &rtypecfsingleredirect.SingleRedirect{ + Code: code, + // + PRWhen: "UNKNOWABLE", + PRThen: "UNKNOWABLE", + PRPriority: 0, + PRDisplay: "UNKNOWABLE", + // + SRName: name, + SRWhen: when, + SRThen: then, + SRDisplay: target, + } + rc.ReSeal() +} + +// targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API. +func targetFromAPIData(name string, code uint16, when, then string) string { + return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)", + name, + code, + when, + then, + ) +} diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go index 16af5263ae..4a6a6679f4 100644 --- a/providers/cloudns/cloudnsProvider.go +++ b/providers/cloudns/cloudnsProvider.go @@ -8,6 +8,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns/dnsutil" "golang.org/x/time/rate" @@ -62,7 +63,7 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("CLOUDNS_WR", providerName, "") + rtypecontrol.RegisterCustomRecordType("CLOUDNS_WR", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/namecheap/namecheapProvider.go b/providers/namecheap/namecheapProvider.go index 057f95b3c8..9155060c2a 100644 --- a/providers/namecheap/namecheapProvider.go +++ b/providers/namecheap/namecheapProvider.go @@ -10,6 +10,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" nc "github.com/billputer/go-namecheap" "golang.org/x/net/publicsuffix" @@ -50,9 +51,9 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("URL", providerName, "") - providers.RegisterCustomRecordType("URL301", providerName, "") - providers.RegisterCustomRecordType("FRAME", providerName, "") + rtypecontrol.RegisterCustomRecordType("URL", providerName, "") + rtypecontrol.RegisterCustomRecordType("URL301", providerName, "") + rtypecontrol.RegisterCustomRecordType("FRAME", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/netlify/netlifyProvider.go b/providers/netlify/netlifyProvider.go index 65b106e5f5..f6452251ce 100644 --- a/providers/netlify/netlifyProvider.go +++ b/providers/netlify/netlifyProvider.go @@ -7,6 +7,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns" ) @@ -40,8 +41,8 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType(providerName, providerName, "") - providers.RegisterCustomRecordType("NETLIFYv6", providerName, "") + rtypecontrol.RegisterCustomRecordType(providerName, providerName, "") + rtypecontrol.RegisterCustomRecordType("NETLIFYv6", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/ns1/ns1Provider.go b/providers/ns1/ns1Provider.go index c6dd83661d..7ce38302ec 100644 --- a/providers/ns1/ns1Provider.go +++ b/providers/ns1/ns1Provider.go @@ -10,6 +10,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/providers" "gopkg.in/ns1/ns1-go.v2/rest" "gopkg.in/ns1/ns1-go.v2/rest/model/dns" @@ -50,7 +51,7 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, providers.CanUseSRV, docNotes) - providers.RegisterCustomRecordType("NS1_URLFWD", providerName, "") + rtypecontrol.RegisterCustomRecordType("NS1_URLFWD", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } diff --git a/providers/providers.go b/providers/providers.go index 86ab34a573..6e7e03fb2b 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -191,25 +191,3 @@ func init() { return None{}, nil }, featuresNone) } - -// CustomRType stores an rtype that is only valid for this DSP. -type CustomRType struct { - Name string - Provider string - RealType string -} - -// RegisterCustomRecordType registers a record type that is only valid for one provider. -// provider is the registered type of provider this is valid with -// name is the record type as it will appear in the js. (should be something like $PROVIDER_FOO) -// realType is the record type it will be replaced with after validation -func RegisterCustomRecordType(name, provider, realType string) { - customRecordTypes[name] = &CustomRType{Name: name, Provider: provider, RealType: realType} -} - -// GetCustomRecordType returns a registered custom record type, or nil if none -func GetCustomRecordType(rType string) *CustomRType { - return customRecordTypes[rType] -} - -var customRecordTypes = map[string]*CustomRType{} diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go index 05d3fdcb13..f69fade040 100644 --- a/providers/route53/route53Provider.go +++ b/providers/route53/route53Provider.go @@ -15,6 +15,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/aws/aws-sdk-go-v2/aws" @@ -99,7 +100,7 @@ func init() { } providers.RegisterDomainServiceProviderType(providerName, fns, features) providers.RegisterRegistrarType(providerName, newRoute53Reg) - providers.RegisterCustomRecordType("R53_ALIAS", providerName, "") + rtypecontrol.RegisterCustomRecordType("R53_ALIAS", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) }