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:
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -94,6 +94,7 @@ jobs:
|
||||
CLOUDFLAREAPI_KEY: ${{ secrets.CLOUDFLAREAPI_KEY }}
|
||||
CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
|
||||
CLOUDFLAREAPI_USER: ${{ secrets.CLOUDFLAREAPI_USER }}
|
||||
CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
|
||||
#
|
||||
CLOUDNS_DOMAIN: ${{ secrets.CLOUDNS_DOMAIN }}
|
||||
CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}
|
||||
|
29
docs/_functions/domain/CF_WORKER_ROUTE.md
Normal file
29
docs/_functions/domain/CF_WORKER_ROUTE.md
Normal 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 %}
|
@ -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).
|
||||
|
||||
## 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 %}
|
||||
{
|
||||
"cloudflare": {
|
||||
"apitoken": "your-cloudflare-api-token"
|
||||
"apitoken": "your-cloudflare-api-token",
|
||||
"accountid": "your-cloudflare-account-id"
|
||||
}
|
||||
}
|
||||
{% 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`);
|
||||
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.
|
||||
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.
|
||||
|
||||
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 %}
|
||||
{
|
||||
"cloudflare": {
|
||||
"apikey": "your-cloudflare-api-key",
|
||||
"apiuser": "your-cloudflare-email-address"
|
||||
"apiuser": "your-cloudflare-email-address",
|
||||
"accountid": "your-cloudflare-account-id"
|
||||
}
|
||||
}
|
||||
{% 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 %}
|
||||
{
|
||||
@ -47,6 +73,8 @@ If your Cloudflare account has access to multiple Cloudflare accounts, you can s
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
Older `creds.json` files that do not have accountid set may work for now, but not in the future.
|
||||
|
||||
## Metadata
|
||||
Record level metadata available:
|
||||
* `cloudflare_proxy` ("on", "off", or "full")
|
||||
@ -58,6 +86,7 @@ Domain level metadata available:
|
||||
Provider level metadata available:
|
||||
* `ip_conversions`
|
||||
* `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?
|
||||
|
||||
@ -101,7 +130,7 @@ The following example shows how to set meta variables with and without aliases:
|
||||
D('example.tld', REG_NONE, DnsProvider(CLOUDFLARE),
|
||||
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('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 %}
|
||||
|
||||
@ -132,8 +161,6 @@ D('example2.tld', REG_NONE, DnsProvider(CLOUDFLARE),
|
||||
);
|
||||
{%endhighlight%}
|
||||
|
||||
## Activation
|
||||
DNSControl depends on a Cloudflare Global API Key that's available under "My Settings".
|
||||
|
||||
## New domains
|
||||
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).
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/_all"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers/cloudflare"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers/config"
|
||||
"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 verbose = flag.Bool("verbose", false, "Print corrections as you run them")
|
||||
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() {
|
||||
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
|
||||
// feature at the time?
|
||||
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)
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
// 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 len(f.required) != 0 {
|
||||
for _, c := range f.required {
|
||||
@ -326,11 +346,12 @@ func TestDualProviders(t *testing.T) {
|
||||
}
|
||||
|
||||
type TestGroup struct {
|
||||
Desc string
|
||||
required []providers.Capability
|
||||
only []string
|
||||
not []string
|
||||
tests []*TestCase
|
||||
Desc string
|
||||
required []providers.Capability
|
||||
only []string
|
||||
not []string
|
||||
trueflags []bool
|
||||
tests []*TestCase
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
@ -399,6 +420,12 @@ func cfProxyCNAME(name, target, status string) *models.RecordConfig {
|
||||
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 {
|
||||
return makeRec(name, target, "NS")
|
||||
}
|
||||
@ -553,6 +580,12 @@ func testgroup(desc string, items ...interface{}) *TestGroup {
|
||||
os.Exit(1)
|
||||
}
|
||||
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:
|
||||
group.tests = append(group.tests, v)
|
||||
default:
|
||||
@ -615,6 +648,14 @@ func only(n ...string) onlyFilter {
|
||||
return onlyFilter{names: n}
|
||||
}
|
||||
|
||||
type alltrueFilter struct {
|
||||
flags []bool
|
||||
}
|
||||
|
||||
func alltrue(f ...bool) alltrueFilter {
|
||||
return alltrueFilter{flags: f}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func makeTests(t *testing.T) []*TestGroup {
|
||||
@ -635,6 +676,8 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
// only("ROUTE53", "GANDI_V5")
|
||||
// Only apply to all providers except 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()
|
||||
// reset(not("ROUTE53"), only("GCLOUD")), // ERROR!
|
||||
// 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")),
|
||||
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
|
||||
|
@ -25,7 +25,8 @@
|
||||
"apikey": "$CLOUDFLAREAPI_KEY",
|
||||
"apitoken": "$CLOUDFLAREAPI_TOKEN",
|
||||
"apiuser": "$CLOUDFLAREAPI_USER",
|
||||
"domain": "$CLOUDFLAREAPI_DOMAIN"
|
||||
"domain": "$CLOUDFLAREAPI_DOMAIN",
|
||||
"accountid": "$CLOUDFLAREAPI_ACCOUNTID"
|
||||
},
|
||||
"CLOUDFLAREAPI_OLD": {
|
||||
"apikey": "$CF_KEY",
|
||||
|
@ -84,7 +84,7 @@ func (dc *DomainConfig) Punycode() error {
|
||||
return err
|
||||
}
|
||||
rec.SetTarget(t)
|
||||
case "CF_REDIRECT", "CF_TEMP_REDIRECT":
|
||||
case "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
|
||||
rec.SetTarget(rec.GetTargetField())
|
||||
case "A", "AAAA", "CAA", "DS", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
|
||||
// Nothing to do.
|
||||
|
@ -34,12 +34,14 @@ import (
|
||||
// ALIAS
|
||||
// CF_REDIRECT
|
||||
// CF_TEMP_REDIRECT
|
||||
// CF_WORKER_ROUTE
|
||||
// FRAME
|
||||
// IMPORT_TRANSFORM
|
||||
// NAMESERVER
|
||||
// NO_PURGE
|
||||
// NS1_URLFWD
|
||||
// PAGE_RULE
|
||||
// WORKER_ROUTE
|
||||
// PURGE
|
||||
// URL
|
||||
// URL301
|
||||
@ -512,7 +514,7 @@ func downcase(recs []*RecordConfig) {
|
||||
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.
|
||||
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.
|
||||
// Do nothing.
|
||||
case "SOA":
|
||||
|
@ -722,7 +722,8 @@ function recordBuilder(type, opts) {
|
||||
// Handle D_EXTEND() with subdomains.
|
||||
if (d.subdomain &&
|
||||
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(".")
|
||||
|
||||
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 URL301 = recordBuilder('URL301');
|
||||
var FRAME = recordBuilder('FRAME');
|
||||
|
@ -132,6 +132,7 @@ func TestErrors(t *testing.T) {
|
||||
{"MX reversed", `D("foo.com","reg",MX("@","test.", 5))`},
|
||||
{"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_WORKER_ROUTE With comma", `D("foo.com","reg",CF_WORKER_ROUTE("foo.com","baa,a"))`},
|
||||
{"Bad cidr", `D(reverse("foo.com"), "reg")`},
|
||||
{"Dup domains", `D("example.org", "reg"); D("example.org", "reg")`},
|
||||
{"Bad NAMESERVER", `D("example.com","reg", NAMESERVER("@","ns1.foo.com."))`},
|
||||
|
@ -5,6 +5,8 @@ D("foo.com", REG, DnsProvider(CF));
|
||||
D_EXTEND("sub.foo.com",
|
||||
A("test1.foo.com","10.2.3.1"),
|
||||
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_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")
|
||||
);
|
||||
|
@ -24,6 +24,12 @@
|
||||
"target": "10.2.3.2",
|
||||
"type": "A"
|
||||
},
|
||||
{
|
||||
"name": "test3.foo.com.sub",
|
||||
"subdomain": "sub",
|
||||
"target": "10.2.3.3",
|
||||
"type": "A"
|
||||
},
|
||||
{
|
||||
"name": "@",
|
||||
"target": "test1.foo.com,https://goo.com/$1",
|
||||
@ -33,6 +39,11 @@
|
||||
"name": "@",
|
||||
"target": "test2.foo.com,https://goo.com/$1",
|
||||
"type": "CF_TEMP_REDIRECT"
|
||||
},
|
||||
{
|
||||
"name": "@",
|
||||
"target": "test3.foo.com,test-worker",
|
||||
"type": "CF_WORKER_ROUTE"
|
||||
}
|
||||
],
|
||||
"registrar": "Third-Party"
|
||||
|
@ -1,5 +1,7 @@
|
||||
$TTL 300
|
||||
;@ IN CF_REDIRECT test1.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
|
||||
test2.foo.com.sub IN A 10.2.3.2
|
||||
test3.foo.com.sub IN A 10.2.3.3
|
||||
|
3
pkg/js/parse_tests/040-cfWorkerRoute.js
Normal file
3
pkg/js/parse_tests/040-cfWorkerRoute.js
Normal file
@ -0,0 +1,3 @@
|
||||
D("foo.com","none",
|
||||
CF_WORKER_ROUTE("test.foo.com","test-worker")
|
||||
);
|
18
pkg/js/parse_tests/040-cfWorkerRoute.json
Normal file
18
pkg/js/parse_tests/040-cfWorkerRoute.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
2
pkg/js/parse_tests/040-cfWorkerRoute/foo.com.zone
Normal file
2
pkg/js/parse_tests/040-cfWorkerRoute/foo.com.zone
Normal file
@ -0,0 +1,2 @@
|
||||
$TTL 300
|
||||
;@ IN CF_WORKER_ROUTE test.foo.com,test-worker
|
@ -58,6 +58,7 @@ func init() {
|
||||
providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features)
|
||||
providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
|
||||
providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
|
||||
providers.RegisterCustomRecordType("CF_WORKER_ROUTE", "CLOUDFLAREAPI", "")
|
||||
}
|
||||
|
||||
// cloudflareProvider is the handle for API calls.
|
||||
@ -67,6 +68,7 @@ type cloudflareProvider struct {
|
||||
ipConversions []transform.IPConversion
|
||||
ignoredLabels []string
|
||||
manageRedirects bool
|
||||
manageWorkers bool
|
||||
cfClient *cloudflare.API
|
||||
}
|
||||
|
||||
@ -185,6 +187,14 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
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 {
|
||||
if rec.Type == "ALIAS" {
|
||||
rec.Type = "CNAME"
|
||||
@ -220,6 +230,11 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
Msg: d.String(),
|
||||
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 {
|
||||
corr := c.deleteRec(ex.Original.(cloudflare.DNSRecord), id)
|
||||
// DS records must always have a corresponding NS record.
|
||||
@ -238,6 +253,11 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
Msg: d.String(),
|
||||
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 {
|
||||
corr := c.createRec(des, id)
|
||||
// DS records must always have a corresponding NS record.
|
||||
@ -258,6 +278,13 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
Msg: d.String(),
|
||||
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 {
|
||||
e := ex.Original.(cloudflare.DNSRecord)
|
||||
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
|
||||
@ -416,6 +443,16 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
||||
rec.TTL = 1
|
||||
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
|
||||
@ -473,12 +510,14 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
|
||||
IPConversions string `json:"ip_conversions"`
|
||||
IgnoredLabels []string `json:"ignored_labels"`
|
||||
ManageRedirects bool `json:"manage_redirects"`
|
||||
ManageWorkers bool `json:"manage_workers"`
|
||||
}{}
|
||||
err := json.Unmarshal([]byte(metadata), parsedMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.manageRedirects = parsedMeta.ManageRedirects
|
||||
api.manageWorkers = parsedMeta.ManageWorkers
|
||||
// ignored_labels:
|
||||
api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
@ -282,6 +282,76 @@ func (c *cloudflareProvider) createPageRule(domainID string, target string) erro
|
||||
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!
|
||||
type pageRuleConstraint struct {
|
||||
Operator string `json:"operator"`
|
||||
|
Reference in New Issue
Block a user