1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

CLOUDFLAREAPI: Adds CF_WORKER_ROUTE (#1243)

* CLOUDFLAREAPI: Adds CF_WORKER_ROUTE.

- CLOUDFLAREAPI: Initial support for CF_WORKER_ROUTE.
- Put CF_WORKER_ROUTE behind a per-domain feature-flag.
- Adds Integration Test.
- Create Cloudflare workers for tests.
- Updates documentation.

* Workaround for updateWorkerRoute causing stack overflow.

* Sets AccountID.

* Updates to latest master (Thanks @tresni!).

* Removes $CLOUDFLAREAPI_ACCOUNTNAME.

* Add alltrue() filter and -cfworkers flag

* Final workers changes

* alltrue() should be implemented like other filters.
* Update documentation.

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
F.D.Castel
2021-10-11 17:04:49 -03:00
committed by GitHub
parent 156c684be6
commit 0d12eb7dc6
17 changed files with 370 additions and 21 deletions

View File

@ -94,6 +94,7 @@ jobs:
CLOUDFLAREAPI_KEY: ${{ secrets.CLOUDFLAREAPI_KEY }} CLOUDFLAREAPI_KEY: ${{ secrets.CLOUDFLAREAPI_KEY }}
CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }} CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
CLOUDFLAREAPI_USER: ${{ secrets.CLOUDFLAREAPI_USER }} CLOUDFLAREAPI_USER: ${{ secrets.CLOUDFLAREAPI_USER }}
CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
# #
CLOUDNS_DOMAIN: ${{ secrets.CLOUDNS_DOMAIN }} CLOUDNS_DOMAIN: ${{ secrets.CLOUDNS_DOMAIN }}
CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }} CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}

View File

@ -0,0 +1,29 @@
---
name: CF_WORKER_ROUTE
parameters:
- pattern
- script
---
`CF_WORKER_ROUTE` uses the [Cloudflare Workers](https://developers.cloudflare.com/workers/)
API to manage [worker routes](https://developers.cloudflare.com/workers/platform/routes)
for a given domain.
If _any_ `CF_WORKER_ROUTE` function is used then `dnscontrol` will manage _all_
Worker Routes for the domain. To be clear: this means it will delete existing routes that
were created outside of DNSControl.
WARNING: This interface is not extensively tested. Take precautions such as making
backups and manually verifying `dnscontrol preview` output before running
`dnscontrol push`.
This example assigns the patterns `api.foo.com/*` and `foo.com/api/*` to a `my-worker` script:
{% include startExample.html %}
{% highlight js %}
D("foo.com", .... ,
CF_WORKER_ROUTE("api.foo.com/*", "my-worker"),
CF_WORKER_ROUTE("foo.com/api/*", "my-worker"),
);
{%endhighlight%}
{% include endExample.html %}

View File

@ -11,32 +11,58 @@ jsId: CLOUDFLAREAPI
* When using `SPF()` or the `SPF_BUILDER()` the records are converted to RecordType `TXT` as Cloudflare API fails otherwise. See more [here](https://github.com/StackExchange/dnscontrol/issues/446). * When using `SPF()` or the `SPF_BUILDER()` the records are converted to RecordType `TXT` as Cloudflare API fails otherwise. See more [here](https://github.com/StackExchange/dnscontrol/issues/446).
## Configuration ## Configuration
In the credentials file you must provide a [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens):
The Cloudflare API supports two different authentication methods.
The recommended (newer) method is to
provide a [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens).
This method is enabled by setting the "apitoken" value in `creds.json`:
{% highlight json %} {% highlight json %}
{ {
"cloudflare": { "cloudflare": {
"apitoken": "your-cloudflare-api-token" "apitoken": "your-cloudflare-api-token",
"accountid": "your-cloudflare-account-id"
} }
} }
{% endhighlight %} {% endhighlight %}
Make sure the token has at least the right read zones and edit DNS records (i.e. `Zone → Zone → Read` and `Zone → DNS → Edit`; to modify Page Rules additionally requires `Zone → Page Rules → Edit`); See [Cloudflare's documentation](https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys) for instructions on how to generate and configure permissions on API tokens.
checkout [Cloudflare's documentation](https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys) for instructions on how to generate and configure permissions on API tokens.
A token can be granded rights (authorization to do certain tasks) at a very granular level. DNSControl requires the token to have the following rights:
Or you can provide your Cloudflare API username and access key instead (but it isn't recommended because those credentials give DNSControl access to the complete Cloudflare API): * Read zones (`Zone → Zone → Read`)
* Edit DNS records (`Zone → DNS → Edit`)
* Edit Page Rules (`Zone → Page Rules → Edit`) (Only required if `manage_redirects` is true for any dommain.)
* If Cloudflare Workers are being managed: (if `manage_workers`: set to `true` or `CF_WORKER_ROUTE()` is in use.)
* Edit Worker Scripts (`Account → Workers Scripts → Edit`)
* Edit Worker Scripts (`Zone → Workers Routes → Edit`)
* FYI: [An example permissions configuration](https://user-images.githubusercontent.com/210250/136301050-1fd430bf-21b6-428b-aa54-f6009964031d.png)
The other (older, not recommended) method is to
provide your Cloudflare API username and access key.
This key is available under "My Settings".
This method is not recommended because these credentials give DNSControl access to the entire Cloudflare API.
This method is enabled by setting the "apikey" and "apiuser" values in `creds.json`:
{% highlight json %} {% highlight json %}
{ {
"cloudflare": { "cloudflare": {
"apikey": "your-cloudflare-api-key", "apikey": "your-cloudflare-api-key",
"apiuser": "your-cloudflare-email-address" "apiuser": "your-cloudflare-email-address",
"accountid": "your-cloudflare-account-id"
} }
} }
{% endhighlight %} {% endhighlight %}
If your Cloudflare account has access to multiple Cloudflare accounts, you can specify which Cloudflare account should be used when adding new domains: You can not mix apikey/apiuser and apitoken. If all three values are set, you will receive an error.
You should also set the "accountid" value. This is optional but may become required some day therefore we recommend setting it.
The Account ID is used to disambiguate when API key has access to multiple Cloudflare accounts. For example, when creating domains this key is used to determine which account to place the new domain. It is also required when using Workers.
The "accountid" is found in the Cloudflare portal ("Account ID") on the DNS page. Set it in `creds.json`:
{% highlight json %} {% highlight json %}
{ {
@ -47,6 +73,8 @@ If your Cloudflare account has access to multiple Cloudflare accounts, you can s
} }
{% endhighlight %} {% endhighlight %}
Older `creds.json` files that do not have accountid set may work for now, but not in the future.
## Metadata ## Metadata
Record level metadata available: Record level metadata available:
* `cloudflare_proxy` ("on", "off", or "full") * `cloudflare_proxy` ("on", "off", or "full")
@ -58,6 +86,7 @@ Domain level metadata available:
Provider level metadata available: Provider level metadata available:
* `ip_conversions` * `ip_conversions`
* `manage_redirects`: set to `true` to manage page-rule based redirects * `manage_redirects`: set to `true` to manage page-rule based redirects
* `manage_workers`: set to `true` to manage cloud workers (`CF_WORKER_ROUTE`)
What does on/off/full mean? What does on/off/full mean?
@ -101,7 +130,7 @@ The following example shows how to set meta variables with and without aliases:
D('example.tld', REG_NONE, DnsProvider(CLOUDFLARE), D('example.tld', REG_NONE, DnsProvider(CLOUDFLARE),
A('www1','1.2.3.11', CF_PROXY_ON), // turn proxy ON. A('www1','1.2.3.11', CF_PROXY_ON), // turn proxy ON.
A('www2','1.2.3.12', CF_PROXY_OFF), // default is OFF, this is a no-op. A('www2','1.2.3.12', CF_PROXY_OFF), // default is OFF, this is a no-op.
A('www3','1.2.3.13', {'cloudflare_proxy': 'on'}) // why would anyone do this? A('www3','1.2.3.13', {'cloudflare_proxy': 'on'}) // Old format.
); );
{% endhighlight %} {% endhighlight %}
@ -132,8 +161,6 @@ D('example2.tld', REG_NONE, DnsProvider(CLOUDFLARE),
); );
{%endhighlight%} {%endhighlight%}
## Activation
DNSControl depends on a Cloudflare Global API Key that's available under "My Settings".
## New domains ## New domains
If a domain does not exist in your Cloudflare account, DNSControl If a domain does not exist in your Cloudflare account, DNSControl
@ -169,3 +196,43 @@ Notice a few details:
2. The IP address in those A records may be mostly irrelevant, as cloudflare should handle all requests (assuming some page rule matches). 2. The IP address in those A records may be mostly irrelevant, as cloudflare should handle all requests (assuming some page rule matches).
3. Ordering matters for priority. CF_REDIRECT records will be added in the order they appear in your js. So put catch-alls at the bottom. 3. Ordering matters for priority. CF_REDIRECT records will be added in the order they appear in your js. So put catch-alls at the bottom.
4. if _any_ `CF_REDIRECT` or `CF_TEMP_REDIRECT` functions are used then `dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain. Page Rule types other than "Forwarding URL” will be left alone. 4. if _any_ `CF_REDIRECT` or `CF_TEMP_REDIRECT` functions are used then `dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain. Page Rule types other than "Forwarding URL” will be left alone.
## Worker routes
The Cloudflare provider can manage Worker Routes for your domains. Simply use the `CF_WORKER_ROUTE` function passing the route pattern and the worker name:
{% highlight js %}
var CLOUDFLARE = NewDnsProvider('cloudflare','CLOUDFLAREAPI');
D("foo.com", REG_NONE, DnsProvider(CLOUDFLARE),
{ manage_workers: true}, // Enable editing workers.
// Assign the patterns `api.foo.com/*` and `foo.com/api/*` to `my-worker` script.
CF_WORKER_ROUTE("api.foo.com/*", "my-worker"),
CF_WORKER_ROUTE("foo.com/api/*", "my-worker"),
);
{%endhighlight%}
The API key you use must be enabled to edit workers. In the portal, edit the API key,
under "Permissions" add "Account", "Workers Scripts", "Edit". Without this permission you may see errors that mention "failed fetching worker route list from cloudflare: bad status code from cloudflare: 403 not 200"
Please notice that if _any_ `CF_WORKER_ROUTE` function is used then `dnscontrol` will manage _all_
Worker Routes for the domain. To be clear: this means it will delete existing routes that
were created outside of DNSControl.
## Integration testing
The integration tests assume that Cloudflare Workers are enabled and the credentials used
have the required permissions listed above. The flag `-cfworkers=false` will disable tests related to Workers.
This flag is intended for use with legacy domains where the integration test credentials do not
have access to read/edit Workers. This flag will eventually go away.
{% highlight bash %}
go test -v -verbose -provider CLOUDFLAREAPI -cfworkers=false
{%endhighlight%}
When `-cfworkers=false` is set, tests related to Workers are skipped. The Account ID is not required.

View File

@ -15,6 +15,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/pkg/normalize" "github.com/StackExchange/dnscontrol/v3/pkg/normalize"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
_ "github.com/StackExchange/dnscontrol/v3/providers/_all" _ "github.com/StackExchange/dnscontrol/v3/providers/_all"
"github.com/StackExchange/dnscontrol/v3/providers/cloudflare"
"github.com/StackExchange/dnscontrol/v3/providers/config" "github.com/StackExchange/dnscontrol/v3/providers/config"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
) )
@ -24,6 +25,7 @@ var startIdx = flag.Int("start", 0, "Test number to begin with")
var endIdx = flag.Int("end", 0, "Test index to stop after") var endIdx = flag.Int("end", 0, "Test index to stop after")
var verbose = flag.Bool("verbose", false, "Print corrections as you run them") var verbose = flag.Bool("verbose", false, "Print corrections as you run them")
var printElapsed = flag.Bool("elapsed", false, "Print elapsed time for each testgroup") var printElapsed = flag.Bool("elapsed", false, "Print elapsed time for each testgroup")
var enableCFWorkers = flag.Bool("cfworkers", true, "Set false to disable CF worker tests")
func init() { func init() {
testing.Init() testing.Init()
@ -52,7 +54,11 @@ func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bo
// use this feature. Maybe because we didn't have the capabilities // use this feature. Maybe because we didn't have the capabilities
// feature at the time? // feature at the time?
if name == "CLOUDFLAREAPI" { if name == "CLOUDFLAREAPI" {
metadata = []byte(`{ "manage_redirects": true }`) if *enableCFWorkers {
metadata = []byte(`{ "manage_redirects": true, "manage_workers": true }`)
} else {
metadata = []byte(`{ "manage_redirects": true }`)
}
} }
provider, err := providers.CreateDNSProvider(name, cfg, metadata) provider, err := providers.CreateDNSProvider(name, cfg, metadata)
@ -69,6 +75,13 @@ func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bo
} }
} }
if name == "CLOUDFLAREAPI" && *enableCFWorkers {
// Cloudflare only. Will do nothing if provider != *cloudflareProvider.
if err := cloudflare.PrepareCloudflareTestWorkers(provider); err != nil {
t.Fatal(err)
}
}
return provider, cfg["domain"], fails, cfg return provider, cfg["domain"], fails, cfg
} }
@ -118,6 +131,13 @@ func testPermitted(t *testing.T, p string, f TestGroup) error {
// TODO(tlim): Have a separate validation pass so that such mistakes // TODO(tlim): Have a separate validation pass so that such mistakes
// are more visible? // are more visible?
// If there are any trueflags, make sure they are all true.
for _, c := range f.trueflags {
if !c {
return fmt.Errorf("excluded by alltrue(%v)", f.trueflags)
}
}
// If there are any required capabilities, make sure they all exist. // If there are any required capabilities, make sure they all exist.
if len(f.required) != 0 { if len(f.required) != 0 {
for _, c := range f.required { for _, c := range f.required {
@ -326,11 +346,12 @@ func TestDualProviders(t *testing.T) {
} }
type TestGroup struct { type TestGroup struct {
Desc string Desc string
required []providers.Capability required []providers.Capability
only []string only []string
not []string not []string
tests []*TestCase trueflags []bool
tests []*TestCase
} }
type TestCase struct { type TestCase struct {
@ -399,6 +420,12 @@ func cfProxyCNAME(name, target, status string) *models.RecordConfig {
return r return r
} }
func cfWorkerRoute(pattern, target string) *models.RecordConfig {
t := fmt.Sprintf("%s,%s", pattern, target)
r := makeRec("@", t, "CF_WORKER_ROUTE")
return r
}
func ns(name, target string) *models.RecordConfig { func ns(name, target string) *models.RecordConfig {
return makeRec(name, target, "NS") return makeRec(name, target, "NS")
} }
@ -553,6 +580,12 @@ func testgroup(desc string, items ...interface{}) *TestGroup {
os.Exit(1) os.Exit(1)
} }
group.only = append(group.only, v.names...) group.only = append(group.only, v.names...)
case alltrueFilter:
if len(group.tests) != 0 {
fmt.Printf("ERROR: alltrue() must be before all tc(): %v\n", desc)
os.Exit(1)
}
group.trueflags = append(group.trueflags, v.flags...)
case *TestCase: case *TestCase:
group.tests = append(group.tests, v) group.tests = append(group.tests, v)
default: default:
@ -615,6 +648,14 @@ func only(n ...string) onlyFilter {
return onlyFilter{names: n} return onlyFilter{names: n}
} }
type alltrueFilter struct {
flags []bool
}
func alltrue(f ...bool) alltrueFilter {
return alltrueFilter{flags: f}
}
// //
func makeTests(t *testing.T) []*TestGroup { func makeTests(t *testing.T) []*TestGroup {
@ -635,6 +676,8 @@ func makeTests(t *testing.T) []*TestGroup {
// only("ROUTE53", "GANDI_V5") // only("ROUTE53", "GANDI_V5")
// Only apply to all providers except ROUTE53 + GANDI_V5: // Only apply to all providers except ROUTE53 + GANDI_V5:
// not("ROUTE53", "GANDI_V5"), // not("ROUTE53", "GANDI_V5"),
// Only run this test if all these bool flags are true:
// alltrue(*enableCFWorkers, *anotherFlag, myBoolValue)
// NOTE: You can't mix not() and only() // NOTE: You can't mix not() and only()
// reset(not("ROUTE53"), only("GCLOUD")), // ERROR! // reset(not("ROUTE53"), only("GCLOUD")), // ERROR!
// NOTE: All requires()/not()/only() must appear before any tc(). // NOTE: All requires()/not()/only() must appear before any tc().
@ -1384,6 +1427,34 @@ func makeTests(t *testing.T) []*TestGroup {
tc("proxycnamechange", cfProxyCNAME("anewproxy", "example.com.", "off")), tc("proxycnamechange", cfProxyCNAME("anewproxy", "example.com.", "off")),
clear(), clear(),
), ),
testgroup("CF_WORKER_ROUTE",
only("CLOUDFLAREAPI"),
alltrue(*enableCFWorkers),
// TODO(fdcastel): Add worker scripts via api call before test execution
tc("simple", cfWorkerRoute("cnn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_cnn")),
tc("changeScript", cfWorkerRoute("cnn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc")),
tc("changePattern", cfWorkerRoute("cable.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc")),
clear(),
tc("createMultiple",
cfWorkerRoute("cnn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_cnn"),
cfWorkerRoute("msnbc.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc"),
),
tc("addOne",
cfWorkerRoute("msnbc.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc"),
cfWorkerRoute("cnn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_cnn"),
cfWorkerRoute("api.**current-domain-no-trailing**/cnn/*", "dnscontrol_integrationtest_cnn"),
),
tc("changeOne",
cfWorkerRoute("msn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc"),
cfWorkerRoute("cnn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_cnn"),
cfWorkerRoute("api.**current-domain-no-trailing**/cnn/*", "dnscontrol_integrationtest_cnn"),
),
tc("deleteOne",
cfWorkerRoute("msn.**current-domain-no-trailing**/*", "dnscontrol_integrationtest_msnbc"),
cfWorkerRoute("api.**current-domain-no-trailing**/cnn/*", "dnscontrol_integrationtest_cnn"),
),
),
} }
return tests return tests

View File

@ -25,7 +25,8 @@
"apikey": "$CLOUDFLAREAPI_KEY", "apikey": "$CLOUDFLAREAPI_KEY",
"apitoken": "$CLOUDFLAREAPI_TOKEN", "apitoken": "$CLOUDFLAREAPI_TOKEN",
"apiuser": "$CLOUDFLAREAPI_USER", "apiuser": "$CLOUDFLAREAPI_USER",
"domain": "$CLOUDFLAREAPI_DOMAIN" "domain": "$CLOUDFLAREAPI_DOMAIN",
"accountid": "$CLOUDFLAREAPI_ACCOUNTID"
}, },
"CLOUDFLAREAPI_OLD": { "CLOUDFLAREAPI_OLD": {
"apikey": "$CF_KEY", "apikey": "$CF_KEY",

View File

@ -84,7 +84,7 @@ func (dc *DomainConfig) Punycode() error {
return err return err
} }
rec.SetTarget(t) rec.SetTarget(t)
case "CF_REDIRECT", "CF_TEMP_REDIRECT": case "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
rec.SetTarget(rec.GetTargetField()) rec.SetTarget(rec.GetTargetField())
case "A", "AAAA", "CAA", "DS", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS": case "A", "AAAA", "CAA", "DS", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
// Nothing to do. // Nothing to do.

View File

@ -34,12 +34,14 @@ import (
// ALIAS // ALIAS
// CF_REDIRECT // CF_REDIRECT
// CF_TEMP_REDIRECT // CF_TEMP_REDIRECT
// CF_WORKER_ROUTE
// FRAME // FRAME
// IMPORT_TRANSFORM // IMPORT_TRANSFORM
// NAMESERVER // NAMESERVER
// NO_PURGE // NO_PURGE
// NS1_URLFWD // NS1_URLFWD
// PAGE_RULE // PAGE_RULE
// WORKER_ROUTE
// PURGE // PURGE
// URL // URL
// URL301 // URL301
@ -512,7 +514,7 @@ func downcase(recs []*RecordConfig) {
case "ANAME", "CNAME", "DS", "MX", "NS", "PTR", "NAPTR", "SRV", "TLSA", "AKAMAICDN": case "ANAME", "CNAME", "DS", "MX", "NS", "PTR", "NAPTR", "SRV", "TLSA", "AKAMAICDN":
// These record types have a target that is case insensitive, so we downcase it. // These record types have a target that is case insensitive, so we downcase it.
r.target = strings.ToLower(r.target) r.target = strings.ToLower(r.target)
case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT": case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
// These record types have a target that is case sensitive, or is an IP address. We leave them alone. // These record types have a target that is case sensitive, or is an IP address. We leave them alone.
// Do nothing. // Do nothing.
case "SOA": case "SOA":

View File

@ -722,7 +722,8 @@ function recordBuilder(type, opts) {
// Handle D_EXTEND() with subdomains. // Handle D_EXTEND() with subdomains.
if (d.subdomain && if (d.subdomain &&
record.type != 'CF_REDIRECT' && record.type != 'CF_REDIRECT' &&
record.type != 'CF_TEMP_REDIRECT') { record.type != 'CF_TEMP_REDIRECT' &&
record.type != 'CF_WORKER_ROUTE') {
fqdn = [d.subdomain, d.name].join(".") fqdn = [d.subdomain, d.name].join(".")
record.subdomain = d.subdomain; record.subdomain = d.subdomain;
@ -856,6 +857,18 @@ var CF_TEMP_REDIRECT = recordBuilder('CF_TEMP_REDIRECT', {
}, },
}); });
var CF_WORKER_ROUTE = recordBuilder('CF_WORKER_ROUTE', {
args: [
['pattern', _validateCloudflareRedirect],
['script', _validateCloudflareRedirect],
],
transform: function(record, args, modifiers) {
record.name = '@';
record.target = args.pattern + ',' + args.script;
},
});
var URL = recordBuilder('URL'); var URL = recordBuilder('URL');
var URL301 = recordBuilder('URL301'); var URL301 = recordBuilder('URL301');
var FRAME = recordBuilder('FRAME'); var FRAME = recordBuilder('FRAME');

View File

@ -132,6 +132,7 @@ func TestErrors(t *testing.T) {
{"MX reversed", `D("foo.com","reg",MX("@","test.", 5))`}, {"MX reversed", `D("foo.com","reg",MX("@","test.", 5))`},
{"CF_REDIRECT With comma", `D("foo.com","reg",CF_REDIRECT("foo.com,","baaa"))`}, {"CF_REDIRECT With comma", `D("foo.com","reg",CF_REDIRECT("foo.com,","baaa"))`},
{"CF_TEMP_REDIRECT With comma", `D("foo.com","reg",CF_TEMP_REDIRECT("foo.com","baa,a"))`}, {"CF_TEMP_REDIRECT With comma", `D("foo.com","reg",CF_TEMP_REDIRECT("foo.com","baa,a"))`},
{"CF_WORKER_ROUTE With comma", `D("foo.com","reg",CF_WORKER_ROUTE("foo.com","baa,a"))`},
{"Bad cidr", `D(reverse("foo.com"), "reg")`}, {"Bad cidr", `D(reverse("foo.com"), "reg")`},
{"Dup domains", `D("example.org", "reg"); D("example.org", "reg")`}, {"Dup domains", `D("example.org", "reg"); D("example.org", "reg")`},
{"Bad NAMESERVER", `D("example.com","reg", NAMESERVER("@","ns1.foo.com."))`}, {"Bad NAMESERVER", `D("example.com","reg", NAMESERVER("@","ns1.foo.com."))`},

View File

@ -5,6 +5,8 @@ D("foo.com", REG, DnsProvider(CF));
D_EXTEND("sub.foo.com", D_EXTEND("sub.foo.com",
A("test1.foo.com","10.2.3.1"), A("test1.foo.com","10.2.3.1"),
A("test2.foo.com","10.2.3.2"), A("test2.foo.com","10.2.3.2"),
A("test3.foo.com","10.2.3.3"),
CF_REDIRECT("test1.foo.com","https://goo.com/$1"), CF_REDIRECT("test1.foo.com","https://goo.com/$1"),
CF_TEMP_REDIRECT("test2.foo.com","https://goo.com/$1") CF_TEMP_REDIRECT("test2.foo.com","https://goo.com/$1"),
CF_WORKER_ROUTE("test3.foo.com","test-worker")
); );

View File

@ -24,6 +24,12 @@
"target": "10.2.3.2", "target": "10.2.3.2",
"type": "A" "type": "A"
}, },
{
"name": "test3.foo.com.sub",
"subdomain": "sub",
"target": "10.2.3.3",
"type": "A"
},
{ {
"name": "@", "name": "@",
"target": "test1.foo.com,https://goo.com/$1", "target": "test1.foo.com,https://goo.com/$1",
@ -33,6 +39,11 @@
"name": "@", "name": "@",
"target": "test2.foo.com,https://goo.com/$1", "target": "test2.foo.com,https://goo.com/$1",
"type": "CF_TEMP_REDIRECT" "type": "CF_TEMP_REDIRECT"
},
{
"name": "@",
"target": "test3.foo.com,test-worker",
"type": "CF_WORKER_ROUTE"
} }
], ],
"registrar": "Third-Party" "registrar": "Third-Party"

View File

@ -1,5 +1,7 @@
$TTL 300 $TTL 300
;@ IN CF_REDIRECT test1.foo.com,https://goo.com/$1 ;@ IN CF_REDIRECT test1.foo.com,https://goo.com/$1
;@ IN CF_TEMP_REDIRECT test2.foo.com,https://goo.com/$1 ;@ IN CF_TEMP_REDIRECT test2.foo.com,https://goo.com/$1
;@ IN CF_WORKER_ROUTE test3.foo.com,test-worker
test1.foo.com.sub IN A 10.2.3.1 test1.foo.com.sub IN A 10.2.3.1
test2.foo.com.sub IN A 10.2.3.2 test2.foo.com.sub IN A 10.2.3.2
test3.foo.com.sub IN A 10.2.3.3

View File

@ -0,0 +1,3 @@
D("foo.com","none",
CF_WORKER_ROUTE("test.foo.com","test-worker")
);

View File

@ -0,0 +1,18 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "none",
"dnsProviders": {},
"records": [
{
"type": "CF_WORKER_ROUTE",
"name": "@",
"target": "test.foo.com,test-worker"
}
]
}
]
}

View File

@ -0,0 +1,2 @@
$TTL 300
;@ IN CF_WORKER_ROUTE test.foo.com,test-worker

View File

@ -58,6 +58,7 @@ func init() {
providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features) providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features)
providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "") providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "") providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
providers.RegisterCustomRecordType("CF_WORKER_ROUTE", "CLOUDFLAREAPI", "")
} }
// cloudflareProvider is the handle for API calls. // cloudflareProvider is the handle for API calls.
@ -67,6 +68,7 @@ type cloudflareProvider struct {
ipConversions []transform.IPConversion ipConversions []transform.IPConversion
ignoredLabels []string ignoredLabels []string
manageRedirects bool manageRedirects bool
manageWorkers bool
cfClient *cloudflare.API cfClient *cloudflare.API
} }
@ -185,6 +187,14 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
records = append(records, prs...) records = append(records, prs...)
} }
if c.manageWorkers {
wrs, err := c.getWorkerRoutes(id, dc.Name)
if err != nil {
return nil, err
}
records = append(records, wrs...)
}
for _, rec := range dc.Records { for _, rec := range dc.Records {
if rec.Type == "ALIAS" { if rec.Type == "ALIAS" {
rec.Type = "CNAME" rec.Type = "CNAME"
@ -220,6 +230,11 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
Msg: d.String(), Msg: d.String(),
F: func() error { return c.deletePageRule(ex.Original.(cloudflare.PageRule).ID, id) }, F: func() error { return c.deletePageRule(ex.Original.(cloudflare.PageRule).ID, id) },
}) })
} else if ex.Type == "WORKER_ROUTE" {
corrections = append(corrections, &models.Correction{
Msg: d.String(),
F: func() error { return c.deleteWorkerRoute(ex.Original.(cloudflare.WorkerRoute).ID, id) },
})
} else { } else {
corr := c.deleteRec(ex.Original.(cloudflare.DNSRecord), id) corr := c.deleteRec(ex.Original.(cloudflare.DNSRecord), id)
// DS records must always have a corresponding NS record. // DS records must always have a corresponding NS record.
@ -238,6 +253,11 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
Msg: d.String(), Msg: d.String(),
F: func() error { return c.createPageRule(id, des.GetTargetField()) }, F: func() error { return c.createPageRule(id, des.GetTargetField()) },
}) })
} else if des.Type == "WORKER_ROUTE" {
corrections = append(corrections, &models.Correction{
Msg: d.String(),
F: func() error { return c.createWorkerRoute(id, des.GetTargetField()) },
})
} else { } else {
corr := c.createRec(des, id) corr := c.createRec(des, id)
// DS records must always have a corresponding NS record. // DS records must always have a corresponding NS record.
@ -258,6 +278,13 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
Msg: d.String(), Msg: d.String(),
F: func() error { return c.updatePageRule(ex.Original.(cloudflare.PageRule).ID, id, rec.GetTargetField()) }, F: func() error { return c.updatePageRule(ex.Original.(cloudflare.PageRule).ID, id, rec.GetTargetField()) },
}) })
} else if rec.Type == "WORKER_ROUTE" {
corrections = append(corrections, &models.Correction{
Msg: d.String(),
F: func() error {
return c.updateWorkerRoute(ex.Original.(cloudflare.WorkerRoute).ID, id, rec.GetTargetField())
},
})
} else { } else {
e := ex.Original.(cloudflare.DNSRecord) e := ex.Original.(cloudflare.DNSRecord)
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off" proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
@ -416,6 +443,16 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
rec.TTL = 1 rec.TTL = 1
rec.Type = "PAGE_RULE" rec.Type = "PAGE_RULE"
} }
// CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
if rec.Type == "CF_WORKER_ROUTE" {
parts := strings.Split(rec.GetTargetField(), ",")
if len(parts) != 2 {
return fmt.Errorf("invalid data specified for cloudflare worker record")
}
rec.TTL = 1
rec.Type = "WORKER_ROUTE"
}
} }
// look for ip conversions and transform records // look for ip conversions and transform records
@ -473,12 +510,14 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
IPConversions string `json:"ip_conversions"` IPConversions string `json:"ip_conversions"`
IgnoredLabels []string `json:"ignored_labels"` IgnoredLabels []string `json:"ignored_labels"`
ManageRedirects bool `json:"manage_redirects"` ManageRedirects bool `json:"manage_redirects"`
ManageWorkers bool `json:"manage_workers"`
}{} }{}
err := json.Unmarshal([]byte(metadata), parsedMeta) err := json.Unmarshal([]byte(metadata), parsedMeta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
api.manageRedirects = parsedMeta.ManageRedirects api.manageRedirects = parsedMeta.ManageRedirects
api.manageWorkers = parsedMeta.ManageWorkers
// ignored_labels: // ignored_labels:
api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...) api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...)
if len(api.ignoredLabels) > 0 { if len(api.ignoredLabels) > 0 {
@ -630,3 +669,20 @@ func (c *cloudflareProvider) EnsureDomainExists(domain string) error {
fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id) fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id)
return err return err
} }
// PrepareCloudflareWorkers creates Cloudflare Workers required for CF_WORKER_ROUTE tests.
func PrepareCloudflareTestWorkers(prv providers.DNSServiceProvider) error {
cf, ok := prv.(*cloudflareProvider)
if ok {
err := cf.createTestWorker("dnscontrol_integrationtest_cnn")
if err != nil {
return err
}
err = cf.createTestWorker("dnscontrol_integrationtest_msnbc")
if err != nil {
return err
}
}
return nil
}

View File

@ -282,6 +282,76 @@ func (c *cloudflareProvider) createPageRule(domainID string, target string) erro
return err return err
} }
func (c *cloudflareProvider) getWorkerRoutes(id string, domain string) ([]*models.RecordConfig, error) {
res, err := c.cfClient.ListWorkerRoutes(context.Background(), id)
if err != nil {
return nil, fmt.Errorf("failed fetching worker route list cloudflare: %s", err)
}
recs := []*models.RecordConfig{}
for _, pr := range res.Routes {
var thisPr = pr
r := &models.RecordConfig{
Type: "WORKER_ROUTE",
Original: thisPr,
TTL: 1,
}
r.SetLabel("@", domain)
r.SetTarget(fmt.Sprintf("%s,%s", // $PATTERN,$SCRIPT
pr.Pattern,
pr.Script))
recs = append(recs, r)
}
return recs, nil
}
func (c *cloudflareProvider) deleteWorkerRoute(recordID, domainID string) error {
_, err := c.cfClient.DeleteWorkerRoute(context.Background(), domainID, recordID)
return err
}
func (c *cloudflareProvider) updateWorkerRoute(recordID, domainID string, target string) error {
// Causing stack overflow (!?)
// return c.updateWorkerRoute(recordID, domainID, target)
if err := c.deleteWorkerRoute(recordID, domainID); err != nil {
return err
}
return c.createWorkerRoute(domainID, target)
}
func (c *cloudflareProvider) createWorkerRoute(domainID string, target string) error {
// $PATTERN,$SCRIPT
parts := strings.Split(target, ",")
if len(parts) != 2 {
return fmt.Errorf("unexpected target: '%s' (expected: 'PATTERN,SCRIPT')", target)
}
wr := cloudflare.WorkerRoute{
Pattern: parts[0],
Script: parts[1],
}
_, err := c.cfClient.CreateWorkerRoute(context.Background(), domainID, wr)
return err
}
func (c *cloudflareProvider) createTestWorker(workerName string) error {
wrp := cloudflare.WorkerRequestParams{
ZoneID: "",
ScriptName: workerName,
}
script := `
addEventListener("fetch", (event) => {
event.respondWith(
new Response("Ok.", { status: 200 })
);
});`
_, err := c.cfClient.UploadWorker(context.Background(), &wrp, script)
return err
}
// go-staticcheck lies! // go-staticcheck lies!
type pageRuleConstraint struct { type pageRuleConstraint struct {
Operator string `json:"operator"` Operator string `json:"operator"`