From 759ba0b1a6d34db44b7dd8570f253ee7a64d2a84 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 14:31:07 -0400 Subject: [PATCH 01/15] wip! --- commands/printIR.go | 3 +- documentation/adding-new-rtypes-2.md | 509 ++++++++++++++++++ integrationTest/integration_test.go | 7 +- models/casts.go | 13 + models/rawrecord.go | 72 +++ models/rdata.go | 33 ++ models/record.go | 40 +- pkg/js/js.go | 5 +- pkg/js/parse_tests/050-cfSingleRedirect.json | 110 ++-- pkg/normalize/validate.go | 3 +- pkg/rtypecontrol/rtypecontrol.go | 45 +- pkg/rtypes/postprocess.go | 60 --- providers/cloudflare/cloudflareProvider.go | 29 +- .../{rtypes/cfsingleredirect => }/convert.go | 32 +- providers/cloudflare/rest.go | 14 +- .../cfsingleredirect/cfsingleredirect.go | 40 -- .../rtypes/cfsingleredirect/convert_test.go | 350 ------------ .../rtypes/cfsingleredirect/from.go | 122 ----- .../rtypesingleredirect/cfsingleredirect.go | 111 ++++ providers/cloudflare/singleredirect.go | 41 ++ providers/providers.go | 22 - 21 files changed, 962 insertions(+), 699 deletions(-) create mode 100644 documentation/adding-new-rtypes-2.md create mode 100644 models/casts.go create mode 100644 models/rdata.go delete mode 100644 pkg/rtypes/postprocess.go rename providers/cloudflare/{rtypes/cfsingleredirect => }/convert.go (85%) delete mode 100644 providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go delete mode 100644 providers/cloudflare/rtypes/cfsingleredirect/convert_test.go delete mode 100644 providers/cloudflare/rtypes/cfsingleredirect/from.go create mode 100644 providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go create mode 100644 providers/cloudflare/singleredirect.go 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/adding-new-rtypes-2.md b/documentation/adding-new-rtypes-2.md new file mode 100644 index 0000000000..5c540a472e --- /dev/null +++ b/documentation/adding-new-rtypes-2.md @@ -0,0 +1,509 @@ +# Adding DNS Resource Types the "Rdata" way + +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. + +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 work together. 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. + +## Goals + +Goals of Rdata-style records: + +* Goal: Make it considerably 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 in helpers.js. + * Solution: Rdata-style permits the user of the standard Go unit testing. +* Goal: Stop increasing the size of models.RecordConfig: + * Problem: RC-Style requires each new rtype to add fields to RecordConfig. This consumes memory for every RecordConfig. 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 that implements the rtype is spread all over the code base. + * RData-style: Code is isolated to a specific directory with a few specific exceptions. We hope to eliminate the need for these exceptions over time. + +## Conceptual design. + +The old way: + +RC-style implements a JavaScript function in helpers.js +that accepts the 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 collects all the parameters +and delivers them to the Go code unchanged. 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: + +``` +var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); +``` + +This creates a function called `CF_SINGLE_REDIRECT()` which can be used in `dnsconfig.js`. + +All the remaining code is in `dnscontrol/rtypes/rtype$NAME` (global rtypes) +or `dnscontrol/providers/$PROVIDER/types/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/rtypesingleredirect`. + +# 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: + +``` +var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); + ^^^^^ ^^^^^^^^^^^^^ + function name rtype token +``` + +* function name: This is the name that appears in `dnsconfig.js`. + * For RFC-standard types this should be the name of the type as it would appear in a zone file. + * 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 name of the type as it would appear in a zone file. + * For provider-specific types, the prefix should be the provider's name exactly as it is used in `creds.json`. + * For pseudo-types that apply to any provider, it should be exactly the same as the function name. + +## Step X: Implement the rtype's functions + +`providers/cloudflare/rtypes/rtype$NAME/$NAME.go` +`providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go` + +Implement: + +* `const Name`: Same string as the "rtype token" in helpers.js +* `init()`: Copy verbatim +* Define the struct. `type $Name struct` where `$Name` is the rtype name in mixed case. +* function `Name`: Copy verbatium +* function `ComputeTarget`: returns the hostname (or whatever is closest) for the record. For example, an MX Record would return the hostname (not the preference number). +* 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 is output in `dnscontrol preview` so make it human-readable. For example, an MX record this would be `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. + +Here's how the function works: + +``` +// 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. + +``` + // 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`. + +``` + // 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. + +If you are new to go's "type assertions", here's how they work: +* 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, since Go can't + guess it for us. +* This works great, except there's a catch: We we assert wrong, the code will + panic. 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 it is wrong? Well, 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. Most projects make you do all the work yourself. +* 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/) + +``` + // 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. + +``` + 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. + +``` +import ( + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypefoo" +) + +func (rc *RecordConfig) AsFOO() *rtypefoo.FOO { + return rc.Rdata.(*rtypefoo.FOO) +} +``` + + +------------------- + + +It is important to leave the `omitempty` flag present so that tests for +other record types do not start to fail because your new record types insist on +being present. + +## Step 2: Add a capability for the record + +You'll need to mark which providers support this record type. The +initial PR should implement this record for the `bind` provider at +a minimum. + +- Add the capability to the file `dnscontrol/providers/capabilities.go` (look for `CanUseAlias` and add + it to the end of the list.) +- Run stringer to auto-update the file `dnscontrol/providers/capability_string.go` + +```shell +pushd; cd providers/; +stringer -type=Capability +popd +``` +alternatively + +```shell +pushd; cd providers/; +go generate +popd +``` + +- Add this feature to the feature matrix in `dnscontrol/build/generate/featureMatrix.go`. Add it to the variable `matrix` maintaining alphabetical ordering, which should look like this: + + {% code title="dnscontrol/build/generate/featureMatrix.go" %} + ```diff + func matrixData() *FeatureMatrix { + const ( + ... + DomainModifierCaa = "[`CAA`](language-reference/domain-modifiers/CAA.md)" + + DomainModifierFoo = "[`FOO`](language-reference/domain-modifiers/FOO.md)" + DomainModifierLoc = "[`LOC`](language-reference/domain-modifiers/LOC.md)" + ... + ) + matrix := &FeatureMatrix{ + Providers: map[string]FeatureMap{}, + Features: []string{ + ... + DomainModifierCaa, + + DomainModifierFoo, + DomainModifierLoc, + ... + }, + } + ``` + {% endcode %} + + then add it later in the file with a `setCapability()` statement, which should look like this: + + {% code title="dnscontrol/build/generate/featureMatrix.go" %} + ```diff + ... + + setCapability( + + DomainModifierFoo, + + providers.CanUseFOO, + + ) + ... + ``` + {% endcode %} + +- Add the capability to the list of features that zones are validated + against (i.e. if you want DNSControl to report an error if this + feature is used with a DNS provider that doesn't support it). That's + in the `checkProviderCapabilities` function in + `pkg/normalize/validate.go`. It should look like this: + + {% code title="pkg/normalize/validate.go" %} + ```diff + var providerCapabilityChecks = []pairTypeCapability{ + ... + + capabilityCheck("FOO", providers.CanUseFOO), + ... + ``` + {% endcode %} + +- Mark the `bind` provider as supporting this record type by updating `dnscontrol/providers/bind/bindProvider.go` (look for `providers.CanUse` and you'll see what to do). + +DNSControl will warn/error if this new record is used with a +provider that does not support the capability. + +- Add the capability to the validations in `pkg/normalize/validate.go` + by adding it to `providerCapabilityChecks` +- Some capabilities can't be tested for. If + such testing can't be done, add it to the whitelist in function + `TestCapabilitiesAreFiltered` in + `pkg/normalize/capabilities_test.go` + +If the capabilities testing is not configured correctly, `go test ./...` +will report something like the `MISSING` message below. In this +example we removed `providers.CanUseCAA` from the +`providerCapabilityChecks` list. + +```text +--- FAIL: TestCapabilitiesAreFiltered (0.00s) + capabilities_test.go:66: ok: providers.CanUseAlias (0) is checked for with "ALIAS" + capabilities_test.go:68: MISSING: providers.CanUseCAA (1) is not checked by checkProviderCapabilities + capabilities_test.go:66: ok: providers.CanUseNAPTR (3) is checked for with "NAPTR" +``` + +## Step 3: Add a helper function + +Add a function to `pkg/js/helpers.js` for the new record type. This +is the JavaScript file that defines `dnsconfig.js`'s functions like +[`A()`](language-reference/domain-modifiers/A.md) and [`MX()`](language-reference/domain-modifiers/MX.md). Look at the definition of `A`, `MX` and `CAA` for good +examples to use as a base. + +Please add the function alphabetically with the others. Also, please run +[prettier](https://github.com/prettier/prettier) on the file to ensure +your code conforms to our coding standard: + +```shell +npm install prettier +node_modules/.bin/prettier --write pkg/js/helpers.js +``` + +## 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/integrationTest/integration_test.go b/integrationTest/integration_test.go index 36d3cacadb..bd92442a3b 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -16,7 +16,7 @@ import ( "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/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" "github.com/miekg/dns/dnsutil" ) @@ -503,11 +503,12 @@ 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 := makeRec("@", name, rtypesingleredirect.Name) + rdata, err := rtypesingleredirect.FromRawArgs([]any{code, when, then}, name) if err != nil { panic("Should not happen... cfSingleRedirect") } + r.Rdata = rdata return r } diff --git a/models/casts.go b/models/casts.go new file mode 100644 index 0000000000..b530330cbe --- /dev/null +++ b/models/casts.go @@ -0,0 +1,13 @@ +package models + +// 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. + +import ( + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" +) + +func (rc *RecordConfig) AsSingleRedirect() *rtypesingleredirect.SingleRedirect { + return rc.Rdata.(*rtypesingleredirect.SingleRedirect) +} diff --git a/models/rawrecord.go b/models/rawrecord.go index 47d30ef189..8fe9a58799 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/rtypesingleredirect" +) + // 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 rtypesingleredirect.Name: + rdata, error := rtypesingleredirect.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 a00c92654a..c6f1983b23 100644 --- a/models/record.go +++ b/models/record.go @@ -97,6 +97,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"` @@ -141,31 +144,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. @@ -197,7 +175,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"` @@ -354,6 +334,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/js/js.go b/pkg/js/js.go index 7112e48f26..fa4db74eaa 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 acff2857f8..6e3f5681b0 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -7,6 +7,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" @@ -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/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 0188aa36c4..9f0447dafb 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/rtypesingleredirect" "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) } @@ -322,7 +323,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom Msg: msg, F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) }, }} - case cfsingleredirect.SINGLEREDIRECT: + case rtypesingleredirect.Name: return []*models.Correction{{ Msg: msg, F: func() error { @@ -342,7 +343,7 @@ 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: + case rtypesingleredirect.Name: idTxt = oldrec.CloudflareRedirect.SRRRulesetID default: idTxt = oldrec.Original.(cloudflare.DNSRecord).ID @@ -357,7 +358,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect) }, }} - case cfsingleredirect.SINGLEREDIRECT: + case rtypesingleredirect.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 rtypesingleredirect.Name: idTxt = origRec.Original.(cloudflare.RulesetRule).ID default: idTxt = origRec.Original.(cloudflare.DNSRecord).ID @@ -405,7 +406,7 @@ 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: + case rtypesingleredirect.Name: return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect) 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,7 +557,7 @@ 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) + TranscodePRtoSR(rec) if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { return err } @@ -571,7 +572,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { return err } // The copy becomes the CF SingleRedirect - cfsingleredirect.TranscodePRtoSR(rec) + TranscodePRtoSR(rec) if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { return err } @@ -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 == rtypesingleredirect.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 *rtypesingleredirect.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 85% rename from providers/cloudflare/rtypes/cfsingleredirect/convert.go rename to providers/cloudflare/convert.go index 40ada49867..836cfcfa26 100644 --- a/providers/cloudflare/rtypes/cfsingleredirect/convert.go +++ b/providers/cloudflare/convert.go @@ -1,4 +1,4 @@ -package cfsingleredirect +package cloudflare import ( "fmt" @@ -7,11 +7,12 @@ import ( "strings" "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" ) // 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 = rtypesingleredirect.Name // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT // Extract the fields we're reading from: sr := rec.CloudflareRedirect @@ -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 = rtypesingleredirect.Name + 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) +} + // 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/rest.go b/providers/cloudflare/rest.go index 4a81ca8805..94b8324073 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/rtypesingleredirect" "github.com/cloudflare/cloudflare-go" ) @@ -301,7 +301,7 @@ 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.SetLabel("@", domain) // Store the IDs @@ -315,7 +315,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 rtypesingleredirect.SingleRedirect) error { newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{} newSingleRedirectRule := cloudflare.RulesetRule{} @@ -359,7 +359,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 rtypesingleredirect.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{} @@ -433,7 +433,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) @@ -445,7 +445,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 rtypesingleredirect.SingleRedirect) error { // maybe someday? //c.apiProvider.UpdatePageRule(context.Background(), domainId, recordID, ) if err := c.deletePageRule(recordID, domainID); err != nil { @@ -454,7 +454,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 rtypesingleredirect.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/rtypesingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go new file mode 100644 index 0000000000..b8b906aaa7 --- /dev/null +++ b/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go @@ -0,0 +1,111 @@ +package rtypesingleredirect + +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 makeSingleRedirectFromRawRec(code, name, when, then) +} + +// makeSingleRedirectFromRawRec updates a RecordConfig to be a +// SINGLEREDIRECT using the data from a RawRecord. +func makeSingleRedirectFromRawRec(code uint16, name, when, then string) (*SingleRedirect, error) { + target := targetFromRaw(name, code, when, then) + + //rc.Type = SINGLEREDIRECT + //rc.TTL = 1 + rdata := &SingleRedirect{ + Code: code, + // + PRWhen: "UNKNOWABLE", + PRThen: "UNKNOWABLE", + PRPriority: 0, + PRDisplay: "UNKNOWABLE", + // + SRName: name, + SRWhen: when, + SRThen: then, + SRDisplay: target, + } + //rc.SetTarget(rc.CloudflareRedirect.SRDisplay) + 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..57c81ce596 --- /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/rtypesingleredirect" +) + +// 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 = rtypesingleredirect.Name + rc.TTL = 1 + rc.CloudflareRedirect = &rtypesingleredirect.SingleRedirect{ + 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, + ) +} diff --git a/providers/providers.go b/providers/providers.go index fd36c2d8df..03c0be50bd 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{} From 292198a909b42c31b51d96a1075176345827b06d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 14:39:50 -0400 Subject: [PATCH 02/15] wip! --- .../akamaiedgedns/akamaiEdgeDnsProvider.go | 3 +- providers/azuredns/azureDnsProvider.go | 3 +- .../azurePrivateDnsProvider.go | 3 +- providers/cloudflare/cloudflareProvider.go | 14 ++++----- providers/cloudflare/convert.go | 6 ++-- providers/cloudflare/makers.go | 30 +++++++++++++++++++ providers/cloudns/cloudnsProvider.go | 3 +- providers/namecheap/namecheapProvider.go | 7 +++-- providers/netlify/netlifyProvider.go | 5 ++-- providers/ns1/ns1Provider.go | 3 +- providers/route53/route53Provider.go | 3 +- 11 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 providers/cloudflare/makers.go diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go index dbe6fb5f76..5f4ddcd3ac 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 0ab74a72a6..e94c44601a 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 5a901380e8..b60c7ec296 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 9f0447dafb..a971089e06 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -316,7 +316,7 @@ 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.AsSingleRedirect().CloudflareRedirect) }, }} case "WORKER_ROUTE": return []*models.Correction{{ @@ -327,7 +327,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom return []*models.Correction{{ Msg: msg, F: func() error { - return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect) + return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect().CloudflareRedirect) }, }} default: @@ -344,7 +344,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon case "WORKER_ROUTE": idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID case rtypesingleredirect.Name: - idTxt = oldrec.CloudflareRedirect.SRRRulesetID + idTxt = oldrec.AsSingleRedirect().CloudflareRedirect.SRRRulesetID default: idTxt = oldrec.Original.(cloudflare.DNSRecord).ID } @@ -355,7 +355,7 @@ 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.AsSingleRedirect().CloudflareRedirect) }, }} case rtypesingleredirect.Name: @@ -407,7 +407,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models. case "WORKER_ROUTE": return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID) case rtypesingleredirect.Name: - return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect) + return c.deleteSingleRedirects(domainID, *origRec.AsSingleRedirect().CloudflareRedirect) default: return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID) } @@ -558,7 +558,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } else if !c.manageRedirects && c.manageSingleRedirects { // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT. TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect().CloudflareRedirect); err != nil { return err } @@ -573,7 +573,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } // The copy becomes the CF SingleRedirect TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect().CloudflareRedirect); err != nil { return err } // Append the copy to the end of the list. diff --git a/providers/cloudflare/convert.go b/providers/cloudflare/convert.go index 836cfcfa26..6051e2bd47 100644 --- a/providers/cloudflare/convert.go +++ b/providers/cloudflare/convert.go @@ -15,7 +15,7 @@ func TranscodePRtoSR(rec *models.RecordConfig) error { rec.Type = rtypesingleredirect.Name // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT // Extract the fields we're reading from: - sr := rec.CloudflareRedirect + sr := rec.AsSingleRedirect().CloudflareRedirect code := sr.Code prWhen := sr.PRWhen prThen := sr.PRThen @@ -48,7 +48,7 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, rc.Type = rtypesingleredirect.Name rc.TTL = 1 - sr := rc.CloudflareRedirect + sr := rc.AsSingleRedirect().CloudflareRedirect sr.Code = code sr.SRName = srName @@ -56,7 +56,7 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, sr.SRThen = srThen sr.SRDisplay = srDisplay - rc.SetTarget(rc.CloudflareRedirect.SRDisplay) + rc.SetTarget(rc.AsSingleRedirect().CloudflareRedirect.SRDisplay) } // targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE. diff --git a/providers/cloudflare/makers.go b/providers/cloudflare/makers.go new file mode 100644 index 0000000000..0285ec3f06 --- /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/rtypesingleredirect" +) + +// 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 = &rtypesingleredirect.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/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go index d3f7952e53..b5138f286d 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" ) @@ -64,7 +65,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 a69dfe6f97..73befbdfd1 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 ad90aac5b7..cb09ef263e 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 d62b244b88..29f56d221e 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/route53/route53Provider.go b/providers/route53/route53Provider.go index ad2c1c598c..8c1549aff2 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) } From 67a93e8a3802e555903e5c009f7c8df053bcda34 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 14:57:03 -0400 Subject: [PATCH 03/15] wip! --- providers/cloudflare/cloudflareProvider.go | 14 +++++++------- providers/cloudflare/convert.go | 6 +++--- providers/cloudflare/makers.go | 2 +- providers/cloudflare/rest.go | 6 +++--- providers/cloudflare/singleredirect.go | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index a971089e06..2cab0dd332 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -316,7 +316,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom case "PAGE_RULE": return []*models.Correction{{ Msg: msg, - F: func() error { return c.createPageRule(domainID, *newrec.AsSingleRedirect().CloudflareRedirect) }, + F: func() error { return c.createPageRule(domainID, *newrec.AsSingleRedirect()) }, }} case "WORKER_ROUTE": return []*models.Correction{{ @@ -327,7 +327,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom return []*models.Correction{{ Msg: msg, F: func() error { - return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect().CloudflareRedirect) + return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect()) }, }} default: @@ -344,7 +344,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon case "WORKER_ROUTE": idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID case rtypesingleredirect.Name: - idTxt = oldrec.AsSingleRedirect().CloudflareRedirect.SRRRulesetID + idTxt = oldrec.AsSingleRedirect().SRRRulesetID default: idTxt = oldrec.Original.(cloudflare.DNSRecord).ID } @@ -355,7 +355,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon return []*models.Correction{{ Msg: msg, F: func() error { - return c.updatePageRule(idTxt, domainID, *newrec.AsSingleRedirect().CloudflareRedirect) + return c.updatePageRule(idTxt, domainID, *newrec.AsSingleRedirect()) }, }} case rtypesingleredirect.Name: @@ -407,7 +407,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models. case "WORKER_ROUTE": return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID) case rtypesingleredirect.Name: - return c.deleteSingleRedirects(domainID, *origRec.AsSingleRedirect().CloudflareRedirect) + return c.deleteSingleRedirects(domainID, *origRec.AsSingleRedirect()) default: return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID) } @@ -558,7 +558,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } else if !c.manageRedirects && c.manageSingleRedirects { // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT. TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect().CloudflareRedirect); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect()); err != nil { return err } @@ -573,7 +573,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } // The copy becomes the CF SingleRedirect TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect().CloudflareRedirect); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect()); err != nil { return err } // Append the copy to the end of the list. diff --git a/providers/cloudflare/convert.go b/providers/cloudflare/convert.go index 6051e2bd47..d3ab8e2f65 100644 --- a/providers/cloudflare/convert.go +++ b/providers/cloudflare/convert.go @@ -15,7 +15,7 @@ func TranscodePRtoSR(rec *models.RecordConfig) error { rec.Type = rtypesingleredirect.Name // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT // Extract the fields we're reading from: - sr := rec.AsSingleRedirect().CloudflareRedirect + sr := rec.AsSingleRedirect() code := sr.Code prWhen := sr.PRWhen prThen := sr.PRThen @@ -48,7 +48,7 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, rc.Type = rtypesingleredirect.Name rc.TTL = 1 - sr := rc.AsSingleRedirect().CloudflareRedirect + sr := rc.AsSingleRedirect() sr.Code = code sr.SRName = srName @@ -56,7 +56,7 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, sr.SRThen = srThen sr.SRDisplay = srDisplay - rc.SetTarget(rc.AsSingleRedirect().CloudflareRedirect.SRDisplay) + rc.SetTarget(rc.AsSingleRedirect().SRDisplay) } // targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE. diff --git a/providers/cloudflare/makers.go b/providers/cloudflare/makers.go index 0285ec3f06..12f5449440 100644 --- a/providers/cloudflare/makers.go +++ b/providers/cloudflare/makers.go @@ -13,7 +13,7 @@ func MakePageRule(rc *models.RecordConfig, priority int, code uint16, when, then rc.Type = "PAGE_RULE" rc.TTL = 1 - rc.CloudflareRedirect = &rtypesingleredirect.SingleRedirect{ + rc.Rdata = &rtypesingleredirect.SingleRedirect{ Code: code, // PRWhen: when, diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 94b8324073..cc3326db23 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -305,7 +305,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo r.SetLabel("@", domain) // Store the IDs - sr := r.CloudflareRedirect + sr := r.AsSingleRedirect() sr.SRRRulesetID = rules.ID sr.SRRRulesetRuleID = pr.ID @@ -401,10 +401,10 @@ func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr rtypesin } 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.AsSingleRedirect()); err != nil { return err } - return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect) + return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect()) } func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { diff --git a/providers/cloudflare/singleredirect.go b/providers/cloudflare/singleredirect.go index 57c81ce596..5d0d97150c 100644 --- a/providers/cloudflare/singleredirect.go +++ b/providers/cloudflare/singleredirect.go @@ -14,7 +14,7 @@ func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, rc.Type = rtypesingleredirect.Name rc.TTL = 1 - rc.CloudflareRedirect = &rtypesingleredirect.SingleRedirect{ + rc.Rdata = &rtypesingleredirect.SingleRedirect{ Code: code, // PRWhen: "UNKNOWABLE", @@ -27,7 +27,7 @@ func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, SRThen: then, SRDisplay: target, } - rc.SetTarget(rc.CloudflareRedirect.SRDisplay) + rc.SetTarget(rc.AsSingleRedirect().SRDisplay) } // targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API. From 13e1b8a5cd4a5c5c6258053cb046dd498ed203c2 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 15:24:49 -0400 Subject: [PATCH 04/15] wip! --- providers/cloudflare/rest.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index cc3326db23..1ad60c95d6 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -278,6 +278,8 @@ func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) { } func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*models.RecordConfig, error) { + fmt.Printf("DEBUG: getSingleRedirects id=%v cfid=%v\n", id, cloudflare.ZoneIdentifier(id)) + fmt.Printf("DEBUG: getSingleRedirects client=%v\n", c.cfClient) rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(id), "http_request_dynamic_redirect") if err != nil { var e *cloudflare.NotFoundError From 149ba6b515d592f968e70cfa805df7a264e1fcaf Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 16:16:39 -0400 Subject: [PATCH 05/15] tests work --- integrationTest/integration_test.go | 1 + providers/cloudflare/rest.go | 3 +-- providers/cloudflare/singleredirect.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index bd92442a3b..7163328db6 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -509,6 +509,7 @@ func cfSingleRedirect(name string, code any, when, then string) *models.RecordCo panic("Should not happen... cfSingleRedirect") } r.Rdata = rdata + r.ReSeal() return r } diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 1ad60c95d6..2971f520f9 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -278,8 +278,6 @@ func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) { } func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*models.RecordConfig, error) { - fmt.Printf("DEBUG: getSingleRedirects id=%v cfid=%v\n", id, cloudflare.ZoneIdentifier(id)) - fmt.Printf("DEBUG: getSingleRedirects client=%v\n", c.cfClient) rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(id), "http_request_dynamic_redirect") if err != nil { var e *cloudflare.NotFoundError @@ -304,6 +302,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo code := uint16(pr.ActionParameters.FromValue.StatusCode) MakeSingleRedirectFromAPI(r, code, srName, srWhen, srThen) + r.ReSeal() r.SetLabel("@", domain) // Store the IDs diff --git a/providers/cloudflare/singleredirect.go b/providers/cloudflare/singleredirect.go index 5d0d97150c..7f9dee3b46 100644 --- a/providers/cloudflare/singleredirect.go +++ b/providers/cloudflare/singleredirect.go @@ -27,7 +27,7 @@ func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, SRThen: then, SRDisplay: target, } - rc.SetTarget(rc.AsSingleRedirect().SRDisplay) + rc.ReSeal() } // targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API. From ce2cca416f1ebb35249a0a384446615bd381754f Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 16:22:43 -0400 Subject: [PATCH 06/15] unexport MakePageRule --- integrationTest/integration_test.go | 1 + providers/cloudflare/cloudflareProvider.go | 2 +- providers/cloudflare/makers.go | 4 ++-- providers/cloudflare/rest.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7163328db6..7eb1a32ffb 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -503,6 +503,7 @@ func cfSingleRedirectEnabled() bool { } func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { + // TODO(tlim): Create a generic way to do this. r := makeRec("@", name, rtypesingleredirect.Name) rdata, err := rtypesingleredirect.FromRawArgs([]any{code, when, then}, name) if err != nil { diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 2cab0dd332..c49c591862 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -549,7 +549,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { prPriority++ // Convert this record to a PAGE_RULE. - MakePageRule(rec, prPriority, code, prWhen, prThen) + makePageRule(rec, prPriority, code, prWhen, prThen) rec.SetLabel("@", dc.Name) if c.manageRedirects && !c.manageSingleRedirects { diff --git a/providers/cloudflare/makers.go b/providers/cloudflare/makers.go index 12f5449440..8cb572180d 100644 --- a/providers/cloudflare/makers.go +++ b/providers/cloudflare/makers.go @@ -7,8 +7,8 @@ import ( "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" ) -// 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) { +// 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" diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 2971f520f9..56ae9cec31 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -434,7 +434,7 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R then := value["url"].(string) currentPrPrio := pr.Priority - MakePageRule(r, currentPrPrio, code, when, then) + makePageRule(r, currentPrPrio, code, when, then) r.SetLabel("@", domain) recs = append(recs, r) From 70e95c755a1ee0e25fc92179da07e00c403eb550 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 14 Aug 2024 16:52:34 -0400 Subject: [PATCH 07/15] wip! --- documentation/adding-new-rtypes-2.md | 21 +++++++++++++++---- integrationTest/integration_test.go | 10 +++------ pkg/create/create.go | 21 +++++++++++++++++++ .../rtypesingleredirect/cfsingleredirect.go | 10 +++------ 4 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 pkg/create/create.go diff --git a/documentation/adding-new-rtypes-2.md b/documentation/adding-new-rtypes-2.md index 5c540a472e..d800df0fd7 100644 --- a/documentation/adding-new-rtypes-2.md +++ b/documentation/adding-new-rtypes-2.md @@ -96,6 +96,7 @@ var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); Implement: +* TODO: init? * `const Name`: Same string as the "rtype token" in helpers.js * `init()`: Copy verbatim * Define the struct. `type $Name struct` where `$Name` is the rtype name in mixed case. @@ -210,13 +211,25 @@ func (rc *RecordConfig) AsFOO() *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 + + +------------------- -It is important to leave the `omitempty` flag present so that tests for -other record types do not start to fail because your new record types insist on -being present. ## Step 2: Add a capability for the record diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7eb1a32ffb..5a485bf832 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/rtypesingleredirect" "github.com/miekg/dns/dnsutil" ) @@ -503,14 +503,10 @@ func cfSingleRedirectEnabled() bool { } func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { - // TODO(tlim): Create a generic way to do this. - r := makeRec("@", name, rtypesingleredirect.Name) - rdata, err := rtypesingleredirect.FromRawArgs([]any{code, when, then}, name) + r, err := create.SingleRedirect(300, "@", name, []any{code, when, then}) if err != nil { - panic("Should not happen... cfSingleRedirect") + panic(err) } - r.Rdata = rdata - r.ReSeal() return r } diff --git a/pkg/create/create.go b/pkg/create/create.go new file mode 100644 index 0000000000..acc9810792 --- /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/rtypesingleredirect" +) + +// Helper functions (one per rtype2.0) that create a RecordConfig of +// this type. +// In the future, this will be autogenerated. + +// SingleRedirect creates a RecordConfig of type SingleRedirect. +func SingleRedirect(ttl uint32, origin, label string, items []any) (*models.RecordConfig, error) { + r := &models.RecordConfig{TTL: ttl} + rd, err := rtypesingleredirect.FromRawArgs(items, label) + if err != nil { + return nil, err + } + r.Seal(origin, label, rd) + return r, nil +} diff --git a/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go index b8b906aaa7..0a28d9e7e9 100644 --- a/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go +++ b/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go @@ -73,16 +73,13 @@ func FromRawArgs(items []any, name string) (*SingleRedirect, error) { var then = items[2].(string) // Use the arguments to perfect the record: - return makeSingleRedirectFromRawRec(code, name, when, then) + return makeSingleRedirect(code, name, when, then) } -// makeSingleRedirectFromRawRec updates a RecordConfig to be a -// SINGLEREDIRECT using the data from a RawRecord. -func makeSingleRedirectFromRawRec(code uint16, name, when, then string) (*SingleRedirect, error) { +// makeSingleRedirect +func makeSingleRedirect(code uint16, name, when, then string) (*SingleRedirect, error) { target := targetFromRaw(name, code, when, then) - //rc.Type = SINGLEREDIRECT - //rc.TTL = 1 rdata := &SingleRedirect{ Code: code, // @@ -96,7 +93,6 @@ func makeSingleRedirectFromRawRec(code uint16, name, when, then string) (*Single SRThen: then, SRDisplay: target, } - //rc.SetTarget(rc.CloudflareRedirect.SRDisplay) return rdata, nil } From c87735c3de15e43c472ebf47f9b88fe181c40bff Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 15 Aug 2024 13:27:02 -0400 Subject: [PATCH 08/15] rename --- .../{adding-new-rtypes-2.md => adding-new-rtypes-rdata.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename documentation/{adding-new-rtypes-2.md => adding-new-rtypes-rdata.md} (100%) diff --git a/documentation/adding-new-rtypes-2.md b/documentation/adding-new-rtypes-rdata.md similarity index 100% rename from documentation/adding-new-rtypes-2.md rename to documentation/adding-new-rtypes-rdata.md From d0387248d69ee12167906b95037df920c2fa713e Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 15 Aug 2024 17:45:30 -0400 Subject: [PATCH 09/15] fixup! --- documentation/adding-new-rtypes-rdata.md | 144 ++++++++++++----------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/documentation/adding-new-rtypes-rdata.md b/documentation/adding-new-rtypes-rdata.md index d800df0fd7..20fb55fc8b 100644 --- a/documentation/adding-new-rtypes-rdata.md +++ b/documentation/adding-new-rtypes-rdata.md @@ -1,57 +1,67 @@ # Adding DNS Resource Types the "Rdata" way Terminology: -* RType: A DNS record type, such as an A, AAAA, CNAME, MX record. +* 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. -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". +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. +This document explains how RData-style rtypes work and how to add a new record +type using this method. -The old and new styles work together. 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. +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. ## Goals Goals of Rdata-style records: -* Goal: Make it considerably easier to add a new rtype. +* **Goal: Make it considerably 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 in helpers.js. - * Solution: Rdata-style permits the user of the standard Go unit testing. -* Goal: Stop increasing the size of models.RecordConfig: - * Problem: RC-Style requires each new rtype to add fields to RecordConfig. This consumes memory for every RecordConfig. 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 that implements the rtype is spread all over the code base. - * RData-style: Code is isolated to a specific directory with a few specific exceptions. We hope to eliminate the need for these exceptions over time. +* **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: Stop increasing the size 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 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. +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 collects all the parameters -and delivers them to the Go code unchanged. A function in Go extracts the parameters -and uses them to build a struct. models.RecordConfig.Rdata points to that struct. +The new way: + +In RData-style, the helpers.js function simply collects all the parameters and +delivers them 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: @@ -59,13 +69,16 @@ For example, `CF_SINGLE_REDIRECT()`'s implementation in helpers.js is one line: var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); ``` -This creates a function called `CF_SINGLE_REDIRECT()` which can be used in `dnsconfig.js`. +This creates a function called `CF_SINGLE_REDIRECT()` which users can use in `dnsconfig.js`. All the remaining code is in `dnscontrol/rtypes/rtype$NAME` (global rtypes) -or `dnscontrol/providers/$PROVIDER/types/rtype$NAME` (provider-specific 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/rtypesingleredirect`. +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 @@ -81,7 +94,7 @@ var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); ``` * function name: This is the name that appears in `dnsconfig.js`. - * For RFC-standard types this should be the name of the type as it would appear in a zone file. + * 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. @@ -91,18 +104,26 @@ var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); ## Step X: Implement the rtype's functions -`providers/cloudflare/rtypes/rtype$NAME/$NAME.go` -`providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go` +General form: + +``` +providers/cloudflare/rtypes/rtype$NAME/$NAME.go +``` + +Example: + +``` +providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go +``` Implement: -* TODO: init? * `const Name`: Same string as the "rtype token" in helpers.js * `init()`: Copy verbatim * Define the struct. `type $Name struct` where `$Name` is the rtype name in mixed case. * function `Name`: Copy verbatium -* function `ComputeTarget`: returns the hostname (or whatever is closest) for the record. For example, an MX Record would return the hostname (not the preference number). -* 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 is output in `dnscontrol preview` so make it human-readable. For example, an MX record this would be `50 example.com.` Note that the label is not included, nor the TTL. +* 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. @@ -110,7 +131,7 @@ Implement: This function takes the raw items from helpes.js and builds the struct. -Here's how the function works: +Copy from another rtype. Here's what the code does: ``` // FromRawArgs creates a Rdata... @@ -148,8 +169,15 @@ If you desire other types, add them to `pkg/rtypecontrol/pave.go`. ``` 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 -If you are new to go's "type assertions", here's how they work: +Here's the longer version: + +here's how they work: * 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! @@ -228,30 +256,26 @@ Follow the examples. It should be exactly the same as `SingleRedirect()` with `S ## Step X: Add integration etsts -------------------- - - ## Step 2: Add a capability for the record -You'll need to mark which providers support this record type. The -initial PR should implement this record for the `bind` provider at -a minimum. +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` -```shell -pushd; cd providers/; -stringer -type=Capability -popd +Install stringer: + +``` +go install golang.org/x/tools/cmd/stringer@latest ``` -alternatively + +Run stringer: ```shell -pushd; cd providers/; +cd providers go generate -popd ``` - Add this feature to the feature matrix in `dnscontrol/build/generate/featureMatrix.go`. Add it to the variable `matrix` maintaining alphabetical ordering, which should look like this: @@ -331,22 +355,6 @@ example we removed `providers.CanUseCAA` from the capabilities_test.go:66: ok: providers.CanUseNAPTR (3) is checked for with "NAPTR" ``` -## Step 3: Add a helper function - -Add a function to `pkg/js/helpers.js` for the new record type. This -is the JavaScript file that defines `dnsconfig.js`'s functions like -[`A()`](language-reference/domain-modifiers/A.md) and [`MX()`](language-reference/domain-modifiers/MX.md). Look at the definition of `A`, `MX` and `CAA` for good -examples to use as a base. - -Please add the function alphabetically with the others. Also, please run -[prettier](https://github.com/prettier/prettier) on the file to ensure -your code conforms to our coding standard: - -```shell -npm install prettier -node_modules/.bin/prettier --write pkg/js/helpers.js -``` - ## Step 4: Search for `#rtype_variations` Anywhere a `rtype` requires special handling has been marked with a From 5e1419f30eb3f8e349bf7eb7ede987258ba714de Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 16 Aug 2024 10:30:04 -0400 Subject: [PATCH 10/15] renames --- documentation/adding-new-rtypes-rdata.md | 189 +++++++++--------- integrationTest/integration_test.go | 2 +- models/casts.go | 10 +- models/rawrecord.go | 6 +- pkg/create/create.go | 8 +- providers/cloudflare/cloudflareProvider.go | 30 +-- providers/cloudflare/convert.go | 12 +- providers/cloudflare/makers.go | 4 +- providers/cloudflare/rest.go | 18 +- .../rtypesingleredirect/cfsingleredirect.go | 107 ---------- providers/cloudflare/singleredirect.go | 10 +- 11 files changed, 144 insertions(+), 252 deletions(-) delete mode 100644 providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go diff --git a/documentation/adding-new-rtypes-rdata.md b/documentation/adding-new-rtypes-rdata.md index 20fb55fc8b..bfe360562a 100644 --- a/documentation/adding-new-rtypes-rdata.md +++ b/documentation/adding-new-rtypes-rdata.md @@ -1,6 +1,7 @@ # Adding DNS Resource Types the "Rdata" way 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. @@ -41,7 +42,7 @@ Goals of Rdata-style records: * RData-style: Code is isolated to a specific directory with many exceptions. The list of exceptions should shrink over time. -## Conceptual design. +## Conceptual design To understand how Rdata-style works, first let's review the old way. @@ -65,7 +66,7 @@ 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'); ``` @@ -74,20 +75,20 @@ This creates a function called `CF_SINGLE_REDIRECT()` which users can use in `dn 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/rtypesingleredirect`. +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: +## How to add a new rtype -## Step 1: Update helpers.js +### 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 @@ -102,18 +103,18 @@ var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); * For provider-specific types, the prefix should be the provider's name exactly as it is used in `creds.json`. * For pseudo-types that apply to any provider, it should be exactly the same as the function name. -## Step X: Implement the rtype's functions +### Step X: Implement the rtype's functions General form: -``` +```text providers/cloudflare/rtypes/rtype$NAME/$NAME.go ``` Example: -``` -providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go +```text +providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go ``` Implement: @@ -127,13 +128,13 @@ Implement: * 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 +### 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 @@ -143,7 +144,7 @@ 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 @@ -158,7 +159,7 @@ The string (in this example, `"iss"`) includes 1 letter for each parameter. 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 { @@ -177,7 +178,8 @@ If you are new to go's "type assertions", here's a simple explanation: Here's the longer version: -here's how they work: +Here's how they work: + * 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! @@ -192,7 +194,7 @@ here's how they work: * The Pave Pattern is something I created for DNSControl to make it easier to work with interfaces. You won't see it elsewhere. Most projects make you do all the work yourself. * 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) } @@ -200,7 +202,7 @@ here's how they work: 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 +### Step X: ConvertRawRecords Edit models/rawrecord.go @@ -208,7 +210,7 @@ In the function `ConvertRawRecords()`, add to the switch statement a case for th 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 { @@ -217,7 +219,7 @@ Here's an example. Change "foo" to the name of your type. rec.Seal(dc.Name, label, rdata) ``` -## Step X: update casts.go +### Step X: update casts.go This will be automated some day, but in the meanwhile this is done manually. @@ -229,7 +231,7 @@ Add the rtype's `As*()` function. For example, if you are adding an rtype FOO, a Follow the examples. -``` +```go import ( "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypefoo" ) @@ -239,7 +241,7 @@ func (rc *RecordConfig) AsFOO() *rtypefoo.FOO { } ``` -## Step X: update create.go +### Step X: update create.go This will be automated some day, but in the meanwhile this is done manually. @@ -251,23 +253,22 @@ Add the rtype's `Foo()` function. For example, if you are adding an rtype FOO, a 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 X: Add this to a provider +### Step X: Add integration etsts -## Step 2: Add a capability for the record +### 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 +* 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` +* Run stringer to auto-update the file `dnscontrol/providers/capability_string.go` Install stringer: -``` +```shell go install golang.org/x/tools/cmd/stringer@latest ``` @@ -278,70 +279,70 @@ 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: +* 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 ( +{% 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 = "[`CAA`](language-reference/domain-modifiers/CAA.md)" - + DomainModifierFoo = "[`FOO`](language-reference/domain-modifiers/FOO.md)" - DomainModifierLoc = "[`LOC`](language-reference/domain-modifiers/LOC.md)" + DomainModifierCaa, ++ DomainModifierFoo, + DomainModifierLoc, ... - ) - 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). + }, + } +``` +{% 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` +* 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 @@ -355,13 +356,13 @@ example we removed `providers.CanUseCAA` from the capabilities_test.go:66: ok: providers.CanUseNAPTR (3) is checked for with "NAPTR" ``` -## Step 4: Search for `#rtype_variations` +### 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 +### 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. @@ -385,7 +386,7 @@ 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 +### 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 @@ -454,7 +455,7 @@ If you find places that haven't been marked Every time you fail to do this, God kills a cute little kitten. Please do it for the kittens. -## Step 7: Support more providers +### 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: @@ -480,13 +481,13 @@ course). This will help all future contributors. If you need help with adding tests, please ask! -## Step 8: Write documentation +### 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. +* `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. @@ -519,7 +520,7 @@ Add the new file `FOO.md` to the documentation table of contents [`documentation ``` {% endcode %} -## Step 9: "go generate" +### Step 9: "go generate" Re-generate the documentation: diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 5a485bf832..784801ed1c 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -503,7 +503,7 @@ func cfSingleRedirectEnabled() bool { } func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { - r, err := create.SingleRedirect(300, "@", name, []any{code, when, then}) + r, err := create.CloudFlareSingleRedirect(300, "@", name, []any{code, when, then}) if err != nil { panic(err) } diff --git a/models/casts.go b/models/casts.go index b530330cbe..47ed388164 100644 --- a/models/casts.go +++ b/models/casts.go @@ -1,13 +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. -import ( - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" -) - -func (rc *RecordConfig) AsSingleRedirect() *rtypesingleredirect.SingleRedirect { - return rc.Rdata.(*rtypesingleredirect.SingleRedirect) +func (rc *RecordConfig) AsCloudflareSingleRedirect() *rtypecfsingleredirect.SingleRedirect { + return rc.Rdata.(*rtypecfsingleredirect.SingleRedirect) } diff --git a/models/rawrecord.go b/models/rawrecord.go index 8fe9a58799..acab8dff89 100644 --- a/models/rawrecord.go +++ b/models/rawrecord.go @@ -3,7 +3,7 @@ package models import ( "fmt" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" ) // RawRecordConfig stores the user-input from dnsconfig.js for a DNS @@ -49,8 +49,8 @@ func ConvertRawRecords(domains []*DomainConfig) error { args := rawRec.Args[1:] switch rawRec.Type { - case rtypesingleredirect.Name: - rdata, error := rtypesingleredirect.FromRawArgs(args, label) + case rtypecfsingleredirect.Name: + rdata, error := rtypecfsingleredirect.FromRawArgs(args, label) if error != nil { return err } diff --git a/pkg/create/create.go b/pkg/create/create.go index acc9810792..84b28e8e3a 100644 --- a/pkg/create/create.go +++ b/pkg/create/create.go @@ -2,17 +2,17 @@ package create import ( "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" + "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. -// SingleRedirect creates a RecordConfig of type SingleRedirect. -func SingleRedirect(ttl uint32, origin, label string, items []any) (*models.RecordConfig, error) { +// 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 := rtypesingleredirect.FromRawArgs(items, label) + rd, err := rtypecfsingleredirect.FromRawArgs(items, label) if err != nil { return nil, err } diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index c49c591862..40ad556842 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -18,7 +18,7 @@ import ( "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/rtypesingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" "github.com/cloudflare/cloudflare-go" "github.com/fatih/color" ) @@ -316,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.AsSingleRedirect()) }, + 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 rtypesingleredirect.Name: + case rtypecfsingleredirect.Name: return []*models.Correction{{ Msg: msg, F: func() error { - return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect()) + return c.createSingleRedirect(domainID, *newrec.AsCloudflareSingleRedirect()) }, }} default: @@ -343,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 rtypesingleredirect.Name: - idTxt = oldrec.AsSingleRedirect().SRRRulesetID + case rtypecfsingleredirect.Name: + idTxt = oldrec.AsCloudflareSingleRedirect().SRRRulesetID default: idTxt = oldrec.Original.(cloudflare.DNSRecord).ID } @@ -355,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.AsSingleRedirect()) + return c.updatePageRule(idTxt, domainID, *newrec.AsCloudflareSingleRedirect()) }, }} - case rtypesingleredirect.Name: + case rtypecfsingleredirect.Name: return []*models.Correction{{ Msg: msg, F: func() error { @@ -391,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 rtypesingleredirect.Name: + case rtypecfsingleredirect.Name: idTxt = origRec.Original.(cloudflare.RulesetRule).ID default: idTxt = origRec.Original.(cloudflare.DNSRecord).ID @@ -406,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 rtypesingleredirect.Name: - return c.deleteSingleRedirects(domainID, *origRec.AsSingleRedirect()) + case rtypecfsingleredirect.Name: + return c.deleteSingleRedirects(domainID, *origRec.AsCloudflareSingleRedirect()) default: return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID) } @@ -558,7 +558,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } else if !c.manageRedirects && c.manageSingleRedirects { // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT. TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect()); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsCloudflareSingleRedirect()); err != nil { return err } @@ -573,7 +573,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { } // The copy becomes the CF SingleRedirect TranscodePRtoSR(rec) - if err := c.LogTranscode(dc.Name, rec.AsSingleRedirect()); err != nil { + if err := c.LogTranscode(dc.Name, rec.AsCloudflareSingleRedirect()); err != nil { return err } // Append the copy to the end of the list. @@ -582,7 +582,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { // The original PAGE_RULE remains untouched. } - } else if rec.Type == rtypesingleredirect.Name { + } 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") @@ -624,7 +624,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error { return nil } -func (c *cloudflareProvider) LogTranscode(zone string, redirect *rtypesingleredirect.SingleRedirect) 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/convert.go b/providers/cloudflare/convert.go index d3ab8e2f65..77a73702a4 100644 --- a/providers/cloudflare/convert.go +++ b/providers/cloudflare/convert.go @@ -7,15 +7,15 @@ import ( "strings" "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" + "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 = rtypesingleredirect.Name // 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.AsSingleRedirect() + sr := rec.AsCloudflareSingleRedirect() code := sr.Code prWhen := sr.PRWhen prThen := sr.PRThen @@ -46,9 +46,9 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, srDisplay := targetFromConverted(priority, code, prWhen, prThen, srWhen, srThen) - rc.Type = rtypesingleredirect.Name + rc.Type = rtypecfsingleredirect.Name rc.TTL = 1 - sr := rc.AsSingleRedirect() + sr := rc.AsCloudflareSingleRedirect() sr.Code = code sr.SRName = srName @@ -56,7 +56,7 @@ func makeSingleRedirectFromConvert(rc *models.RecordConfig, sr.SRThen = srThen sr.SRDisplay = srDisplay - rc.SetTarget(rc.AsSingleRedirect().SRDisplay) + rc.SetTarget(rc.AsCloudflareSingleRedirect().SRDisplay) } // targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE. diff --git a/providers/cloudflare/makers.go b/providers/cloudflare/makers.go index 8cb572180d..0496326437 100644 --- a/providers/cloudflare/makers.go +++ b/providers/cloudflare/makers.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" ) // makePageRule updates a RecordConfig to be a PAGE_RULE using PAGE_RULE data. @@ -13,7 +13,7 @@ func makePageRule(rc *models.RecordConfig, priority int, code uint16, when, then rc.Type = "PAGE_RULE" rc.TTL = 1 - rc.Rdata = &rtypesingleredirect.SingleRedirect{ + rc.Rdata = &rtypecfsingleredirect.SingleRedirect{ Code: code, // PRWhen: when, diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 56ae9cec31..5c6f7dd7e3 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/rtypesingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" "github.com/cloudflare/cloudflare-go" ) @@ -301,12 +301,12 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo srThen := pr.ActionParameters.FromValue.TargetURL.Expression code := uint16(pr.ActionParameters.FromValue.StatusCode) - MakeSingleRedirectFromAPI(r, code, srName, srWhen, srThen) + makeSingleRedirectFromAPI(r, code, srName, srWhen, srThen) r.ReSeal() r.SetLabel("@", domain) // Store the IDs - sr := r.AsSingleRedirect() + sr := r.AsCloudflareSingleRedirect() sr.SRRRulesetID = rules.ID sr.SRRRulesetRuleID = pr.ID @@ -316,7 +316,7 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo return recs, nil } -func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr rtypesingleredirect.SingleRedirect) error { +func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr rtypecfsingleredirect.SingleRedirect) error { newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{} newSingleRedirectRule := cloudflare.RulesetRule{} @@ -360,7 +360,7 @@ func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr rtypesing return err } -func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr rtypesingleredirect.SingleRedirect) 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 +402,10 @@ func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr rtypesin } func (c *cloudflareProvider) updateSingleRedirect(domainID string, oldrec, newrec *models.RecordConfig) error { - if err := c.deleteSingleRedirects(domainID, *oldrec.AsSingleRedirect()); err != nil { + if err := c.deleteSingleRedirects(domainID, *oldrec.AsCloudflareSingleRedirect()); err != nil { return err } - return c.createSingleRedirect(domainID, *newrec.AsSingleRedirect()) + return c.createSingleRedirect(domainID, *newrec.AsCloudflareSingleRedirect()) } func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { @@ -446,7 +446,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 rtypesingleredirect.SingleRedirect) 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 +455,7 @@ func (c *cloudflareProvider) updatePageRule(recordID, domainID string, cfr rtype return c.createPageRule(domainID, cfr) } -func (c *cloudflareProvider) createPageRule(domainID string, cfr rtypesingleredirect.SingleRedirect) 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/rtypesingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go deleted file mode 100644 index 0a28d9e7e9..0000000000 --- a/providers/cloudflare/rtypes/rtypesingleredirect/cfsingleredirect.go +++ /dev/null @@ -1,107 +0,0 @@ -package rtypesingleredirect - -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 index 7f9dee3b46..f3bd8936f3 100644 --- a/providers/cloudflare/singleredirect.go +++ b/providers/cloudflare/singleredirect.go @@ -4,17 +4,17 @@ import ( "fmt" "github.com/StackExchange/dnscontrol/v4/models" - "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypesingleredirect" + "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/rtypecfsingleredirect" ) -// 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) { +// 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 = rtypesingleredirect.Name + rc.Type = rtypecfsingleredirect.Name rc.TTL = 1 - rc.Rdata = &rtypesingleredirect.SingleRedirect{ + rc.Rdata = &rtypecfsingleredirect.SingleRedirect{ Code: code, // PRWhen: "UNKNOWABLE", From 54e3676224df401c1bbf4e16776eb20a24be6ca6 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 16 Aug 2024 11:02:00 -0400 Subject: [PATCH 11/15] wip! --- documentation/adding-new-rtypes-rdata.md | 81 +++++++++++++----------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/documentation/adding-new-rtypes-rdata.md b/documentation/adding-new-rtypes-rdata.md index bfe360562a..0c965841cd 100644 --- a/documentation/adding-new-rtypes-rdata.md +++ b/documentation/adding-new-rtypes-rdata.md @@ -1,27 +1,27 @@ # Adding DNS Resource Types the "Rdata" way -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. - 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 +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, +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 considerably easier to add a new rtype.** +* **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.** @@ -29,7 +29,7 @@ Goals of Rdata-style records: helpers.js. * Solution: Rdata-style only uses Go (with 1 minor exception) and permits the use of the standard Go unit testing framework. -* **Goal: Stop increasing the size of models.RecordConfig.** +* **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 @@ -37,9 +37,9 @@ Goals of Rdata-style records: 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. +* **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. + * Rdata-style: Code is isolated to a specific directory with many exceptions. The list of exceptions should shrink over time. ## Conceptual design @@ -59,10 +59,9 @@ JavaScript environment, has no test coverage. The new way: -In RData-style, the helpers.js function simply collects all the parameters and -delivers them 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. +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: @@ -70,11 +69,12 @@ For example, `CF_SINGLE_REDIRECT()`'s implementation in helpers.js is one line: var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); ``` -This creates a function called `CF_SINGLE_REDIRECT()` which users can use in `dnsconfig.js`. +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. +(`$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. @@ -94,35 +94,39 @@ var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT'); function name rtype token ``` -* function name: This is the name that appears in `dnsconfig.js`. +* 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 name of the type as it would appear in a zone file. + * 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 that apply to any provider, it should be exactly the same as the function name. + * For pseudo-types this should be the same as the function name. ### Step X: Implement the rtype's functions -General form: +Complete the "Rdataer" interface (which is Go-speak for "write all the functions required for that interface). + +Create the file in: ```text -providers/cloudflare/rtypes/rtype$NAME/$NAME.go +providers/$PROVIDER/rtypes/rtype$name/$name.go # Provider-specific +rtypes/rtype$name/$name.go # Global ``` -Example: +Examples: ```text providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go +rtypes/rtypeloc/loc.go ``` -Implement: +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 +* `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 verbatium +* 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. @@ -178,20 +182,22 @@ If you are new to go's "type assertions", here's a simple explanation: Here's the longer version: -Here's how they work: - * 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, since Go can't - guess it for us. + 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. 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 it is wrong? Well, 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. Most projects make you do all the work yourself. + 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 @@ -200,7 +206,8 @@ Here's how they work: } ``` -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. +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 From cf2f0f229260dd686140620a5133b4d06cc2180d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 16 Aug 2024 11:02:35 -0400 Subject: [PATCH 12/15] fixup! --- .../rtypecfsingleredirect/cfsingleredirect.go | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 providers/cloudflare/rtypes/rtypecfsingleredirect/cfsingleredirect.go 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, + ) +} From 226c9519436624ff2d2ab0badf39a8938f4c96bd Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 19 Aug 2024 17:17:24 -0400 Subject: [PATCH 13/15] Fix TOC --- documentation/SUMMARY.md | 3 ++- documentation/adding-new-rtypes-rdata.md | 2 +- documentation/adding-new-rtypes.md | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index f96c362273..7aaab6b826 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -180,7 +180,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 (rtypes) (Rdata-style)](adding-new-rtypes-rdata.md) +* [Creating new DNS Resource Types (rtypes) (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 index 0c965841cd..9220430102 100644 --- a/documentation/adding-new-rtypes-rdata.md +++ b/documentation/adding-new-rtypes-rdata.md @@ -1,4 +1,4 @@ -# Adding DNS Resource Types the "Rdata" way +# 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 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. From dace25d8000ffa8957b9bf03e3642958581521b5 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 19 Aug 2024 17:18:18 -0400 Subject: [PATCH 14/15] fix SUMMARY --- documentation/SUMMARY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 7aaab6b826..192d3965cb 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -180,8 +180,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) (Rdata-style)](adding-new-rtypes-rdata.md) -* [Creating new DNS Resource Types (rtypes) (RC-Style)](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) From 84f8e48460b406379decf851c0a1f539d1982557 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 18 Dec 2024 16:04:40 -0500 Subject: [PATCH 15/15] empty