Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudflare): support cloudflare region #4646

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a38476b
feat: add cloudflare host name
AndrewCharlesHay Jul 31, 2024
a6590de
add test
AndrewCharlesHay Aug 1, 2024
bbe914b
add flag declaration
AndrewCharlesHay Aug 5, 2024
d8ec935
fix: add flag declaration
AndrewCharlesHay Aug 5, 2024
6ef25bf
fix: add RegionalHostname
AndrewCharlesHay Aug 6, 2024
4386cbb
cloudflare: changes to plan
AndrewCharlesHay Aug 6, 2024
2227d22
Merge branch 'master' into cloudflare/region
AndrewCharlesHay Sep 6, 2024
70406fc
fix: resolve syntax errors
AndrewCharlesHay Sep 6, 2024
e0cc262
fix: add region to changes function
AndrewCharlesHay Sep 9, 2024
c18bb8b
test: add tests for cloudflare region
AndrewCharlesHay Sep 9, 2024
797629f
chore: remove extra comments
AndrewCharlesHay Sep 13, 2024
e49c40d
chore: remove extra comments
AndrewCharlesHay Sep 13, 2024
fe7f6f0
feat: add codespace definition
AndrewCharlesHay Sep 13, 2024
e8d55ba
ci: move go to vscode setings
AndrewCharlesHay Sep 13, 2024
d704d7d
chore: remove devcontainer
AndrewCharlesHay Sep 14, 2024
15d0813
style: tabs vs spaces
AndrewCharlesHay Sep 14, 2024
3b4568e
fix: update failing tests
AndrewCharlesHay Oct 7, 2024
438b0bc
fix: add UpdateDataLocalizationRegionalHostname to test framework
AndrewCharlesHay Oct 11, 2024
1e35134
fix: add remove default from minimal config
AndrewCharlesHay Oct 11, 2024
806cf81
fix: update function name to resemble crud
AndrewCharlesHay Oct 29, 2024
3b5ef97
Merge branch 'master' into cloudflare/region
AndrewCharlesHay Nov 1, 2024
6f6e714
docs: notes on how to use cloudflare-region-key
AndrewCharlesHay Nov 1, 2024
10d383c
Update docs/tutorials/cloudflare.md
AndrewCharlesHay Nov 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/tutorials/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ spec:
- --provider=cloudflare
- --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
- --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
- --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
env:
- name: CF_API_KEY
valueFrom:
Expand Down Expand Up @@ -204,6 +205,7 @@ spec:
- --provider=cloudflare
- --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
- --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
- --cloudflare-region-key="eu" # (optional) configure which region can decrypt HTTPS requests
env:
- name: CF_API_KEY
valueFrom:
Expand Down Expand Up @@ -299,3 +301,9 @@ $ kubectl delete -f externaldns.yaml
## Setting cloudflare-proxied on a per-ingress basis

Using the `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"` annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global `--cloudflare-proxied` setting.

## Setting cloudflare-region-key to configure regional services

Using the `external-dns.alpha.kubernetes.io/cloudflare-region-key` annotation on your ingress, you can restrict which data centers can decrypt and service HTTPS traffic. A list of aviable options can be see [here](https://developers.cloudflare.com/data-localization/regional-services/get-started/).
AndrewCharlesHay marked this conversation as resolved.
Show resolved Hide resolved

If not set the value will default to `global`.
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func main() {
case "civo":
p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun)
case "cloudflare":
p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage)
p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage, cfg.CloudflareRegionKey)
case "google":
p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
case "digitalocean":
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type Config struct {
AzureZonesCacheDuration time.Duration
CloudflareProxied bool
CloudflareDNSRecordsPerPage int
CloudflareRegionKey string
CoreDNSPrefix string
AkamaiServiceConsumerDomain string
AkamaiClientToken string
Expand Down Expand Up @@ -267,6 +268,7 @@ var defaultConfig = &Config{
AzureZonesCacheDuration: 0 * time.Second,
CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100,
CloudflareRegionKey: "earth",
CoreDNSPrefix: "/skydns/",
AkamaiServiceConsumerDomain: "",
AkamaiClientToken: "",
Expand Down Expand Up @@ -492,6 +494,7 @@ func (cfg *Config) ParseFlags(args []string) error {

app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey)
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var (
AzureSubscriptionID: "",
CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100,
CloudflareRegionKey: "",
CoreDNSPrefix: "/skydns/",
AkamaiServiceConsumerDomain: "",
AkamaiClientToken: "",
Expand Down Expand Up @@ -175,6 +176,7 @@ var (
AzureSubscriptionID: "arg",
CloudflareProxied: true,
CloudflareDNSRecordsPerPage: 5000,
CloudflareRegionKey: "us",
CoreDNSPrefix: "/coredns/",
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
Expand Down Expand Up @@ -277,6 +279,7 @@ func TestParseFlags(t *testing.T) {
"--azure-subscription-id=arg",
"--cloudflare-proxied",
"--cloudflare-dns-records-per-page=5000",
"--cloudflare-region-key=us",
"--coredns-prefix=/coredns/",
"--akamai-serviceconsumerdomain=oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
Expand Down Expand Up @@ -396,6 +399,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
Expand Down
40 changes: 36 additions & 4 deletions provider/cloudflare/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"strconv"
"strings"
"time"

cloudflare "github.com/cloudflare/cloudflare-go"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -73,6 +74,7 @@ type cloudFlareDNS interface {
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error
}

type zoneService struct {
Expand Down Expand Up @@ -104,6 +106,11 @@ func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.Resourc
return err
}

func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
_, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp)
return err
}

func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
return z.service.DeleteDNSRecord(ctx, rc, recordID)
}
Expand All @@ -126,12 +133,14 @@ type CloudFlareProvider struct {
proxiedByDefault bool
DryRun bool
DNSRecordsPerPage int
RegionKey string
}

// cloudFlareChange differentiates between ChangActions
type cloudFlareChange struct {
Action string
ResourceRecord cloudflare.DNSRecord
Action string
ResourceRecord cloudflare.DNSRecord
RegionalHostname cloudflare.RegionalHostname
}

// RecordParamsTypes is a typeset of the possible Record Params that can be passed to cloudflare-go library
Expand All @@ -150,6 +159,14 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams
}
}

// updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in
func updateDataLocalizationRegionalHostnameParams(cfc cloudFlareChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams {
return cloudflare.UpdateDataLocalizationRegionalHostnameParams{
Hostname: cfc.RegionalHostname.Hostname,
RegionKey: cfc.RegionalHostname.RegionKey,
}
}

// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams {
return cloudflare.CreateDNSRecordParams{
Expand All @@ -162,7 +179,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar
}

// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider.
func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, dnsRecordsPerPage int) (*CloudFlareProvider, error) {
func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, proxiedByDefault bool, dryRun bool, dnsRecordsPerPage int, regionKey string) (*CloudFlareProvider, error) {
// initialize via chosen auth method and returns new API object
var (
config *cloudflare.API
Expand Down Expand Up @@ -192,6 +209,7 @@ func NewCloudFlareProvider(domainFilter endpoint.DomainFilter, zoneIDFilter prov
proxiedByDefault: proxiedByDefault,
DryRun: dryRun,
DNSRecordsPerPage: dnsRecordsPerPage,
RegionKey: regionKey,
}
return provider, nil
}
Expand Down Expand Up @@ -351,12 +369,18 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
continue
}
recordParam := updateDNSRecordParam(*change)
regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(*change)
recordParam.ID = recordID
err := p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam)
if err != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to update record: %v", err)
}
regionalHostnameErr := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam)
if regionalHostnameErr != nil {
failedChange = true
log.WithFields(logFields).Errorf("failed to update record: %v", regionalHostnameErr)
}
} else if change.Action == cloudFlareDelete {
recordID := p.getRecordID(records, change.ResourceRecord)
if recordID == "" {
Expand Down Expand Up @@ -443,7 +467,7 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL)
}

dt := time.Now()
return &cloudFlareChange{
Action: action,
ResourceRecord: cloudflare.DNSRecord{
Expand All @@ -452,6 +476,14 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
Proxied: &proxied,
Type: endpoint.RecordType,
Content: target,
Meta: map[string]interface{}{
"region": p.RegionKey,
},
},
RegionalHostname: cloudflare.RegionalHostname{
Hostname: endpoint.DNSName,
RegionKey: p.RegionKey,
CreatedOn: &dt,
},
}
}
Expand Down
89 changes: 82 additions & 7 deletions provider/cloudflare/cloudflare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudfla
return nil
}

func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error {
m.Actions = append(m.Actions, MockAction{
Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: rc.Identifier,
RecordId: "",
RecordData: cloudflare.DNSRecord{
Name: rp.Hostname,
},
})
return nil
}

func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
m.Actions = append(m.Actions, MockAction{
Name: "Delete",
Expand Down Expand Up @@ -706,7 +718,8 @@ func TestCloudflareProvider(t *testing.T) {
provider.NewZoneIDFilter([]string{""}),
false,
true,
5000)
5000,
"")
if err != nil {
t.Errorf("should not fail, %s", err)
}
Expand All @@ -722,7 +735,8 @@ func TestCloudflareProvider(t *testing.T) {
provider.NewZoneIDFilter([]string{""}),
false,
true,
5000)
5000,
"")
if err != nil {
t.Errorf("should not fail, %s", err)
}
Expand All @@ -735,7 +749,8 @@ func TestCloudflareProvider(t *testing.T) {
provider.NewZoneIDFilter([]string{""}),
false,
true,
5000)
5000,
"")
if err != nil {
t.Errorf("should not fail, %s", err)
}
Expand All @@ -747,7 +762,8 @@ func TestCloudflareProvider(t *testing.T) {
provider.NewZoneIDFilter([]string{""}),
false,
true,
5000)
5000,
"")
if err == nil {
t.Errorf("expected to fail")
}
Expand Down Expand Up @@ -1225,7 +1241,6 @@ func TestCloudflareComplexUpdate(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": ExampleDomain,
})

provider := &CloudFlareProvider{
Client: client,
}
Expand Down Expand Up @@ -1267,7 +1282,7 @@ func TestCloudflareComplexUpdate(t *testing.T) {
t.Errorf("should not fail, %s", err)
}

td.CmpDeeply(t, client.Actions, []MockAction{
mockAction := []MockAction{
{
Name: "Delete",
ZoneId: "001",
Expand Down Expand Up @@ -1296,7 +1311,17 @@ func TestCloudflareComplexUpdate(t *testing.T) {
Proxied: proxyEnabled,
},
},
})
{
Name: "UpdateDataLocalizationRegionalHostname",
ZoneId: "001",
RecordData: cloudflare.DNSRecord{
Name: "foobar.bar.com",
TTL: 0,
Proxiable: false,
},
},
}
td.CmpDeeply(t, client.Actions, mockAction)
}

func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
Expand Down Expand Up @@ -1355,3 +1380,53 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
assert.Equal(t, 0, len(planned.Changes.UpdateOld), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.Delete), "no new changes should be here")
}

func TestCloudFlareProvider_Region(t *testing.T) {
_ = os.Setenv("CF_API_TOKEN", "abc123def")
_ = os.Setenv("CF_API_EMAIL", "test@test.com")
provider, err := NewCloudFlareProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.ZoneIDFilter{}, true, false, 50, "us")
if err != nil {
t.Fatal(err)
}

if provider.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", provider.RegionKey)
}
}

func TestCloudFlareProvider_updateDataLocalizationRegionalHostnameParams(t *testing.T) {
change := &cloudFlareChange{
RegionalHostname: cloudflare.RegionalHostname{
Hostname: "example.com",
RegionKey: "us",
},
}

params := updateDataLocalizationRegionalHostnameParams(*change)
if params.Hostname != "example.com" {
t.Errorf("expected hostname to be 'example.com', but got '%s'", params.Hostname)
}

if params.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", params.RegionKey)
}
}

func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) {
_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
_ = os.Setenv("CF_API_EMAIL", "test@test.com")
provider, err := NewCloudFlareProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.ZoneIDFilter{}, true, false, 50, "us")
if err != nil {
t.Fatal(err)
}

endpoint := &endpoint.Endpoint{
DNSName: "example.com",
Targets: []string{"192.0.2.1"},
}

change := provider.newCloudFlareChange(cloudFlareCreate, endpoint, endpoint.Targets[0])
if change.RegionalHostname.RegionKey != "us" {
t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey)
}
}