From 3d821d74cebe3d6f39bbb9a536043b08e128d0bb Mon Sep 17 00:00:00 2001 From: "k.siemer" Date: Tue, 21 Jan 2020 10:11:54 +0100 Subject: [PATCH] Added new provider: Akamai FastDNS --- go.mod | 2 + go.sum | 12 + main.go | 12 + pkg/apis/externaldns/types.go | 14 +- pkg/apis/externaldns/types_test.go | 278 ++++++------ pkg/apis/externaldns/validation/validation.go | 16 + provider/akamai.go | 414 ++++++++++++++++++ provider/akamai_test.go | 336 ++++++++++++++ 8 files changed, 952 insertions(+), 132 deletions(-) create mode 100644 provider/akamai.go create mode 100644 provider/akamai_test.go diff --git a/go.mod b/go.mod index 3a093f75a..f2edf2568 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Azure/go-autorest/autorest/adal v0.6.0 github.com/Azure/go-autorest/autorest/azure/auth v0.0.0-00010101000000-000000000000 github.com/Azure/go-autorest/autorest/to v0.3.0 + github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.5 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/kingpin v2.2.5+incompatible @@ -64,6 +65,7 @@ replace ( github.com/Azure/go-autorest/autorest/adal => github.com/Azure/go-autorest/autorest/adal v0.6.0 github.com/Azure/go-autorest/autorest/azure/auth => github.com/Azure/go-autorest/autorest/azure/auth v0.3.0 github.com/golang/glog => github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d + github.com/h2non/gock => gopkg.in/h2non/gock.v1 v1.0.14 istio.io/api => istio.io/api v0.0.0-20190820204432-483f2547d882 istio.io/istio => istio.io/istio v0.0.0-20190911205955-c2bd59595ce6 k8s.io/api => k8s.io/api v0.0.0-20190817221950-ebce17126a01 diff --git a/go.sum b/go.sum index 5863891e6..c3c39d69d 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPE github.com/SermoDigital/jose v0.9.1/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.5 h1:6/ofsc2djpS+bsyOG10cDJI1ftigCiulAfIZpxSua6k= +github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.5/go.mod h1:L3YOfsK9Dzvjxj1/Q5OG58SDCGGOwtzgi734Kpdc03c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= @@ -192,6 +194,8 @@ github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.33.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.44.0 h1:8+SRbfpRFlIunpSum4BEf1ClTtVjOgKzgBv9pHFkI6w= +github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -292,6 +296,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20171214222146-0e7658f8ee99/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul v1.3.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -413,6 +418,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4= github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs= github.com/nic-at/rc0go v1.1.0 h1:k6/Bru/npTjmCSFw65ulYRw/b3ycIS30t6/YM4r42V4= @@ -549,6 +555,9 @@ github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 h1:Q76MzqJu++vAfhj0mVf7t0F4xHUbg+V/d/Uk5PBQjRU= github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92/go.mod h1:AZuEfReFWdvtU0LatbLpo70t3lqdLvph2D5mqFP0bkA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -726,9 +735,12 @@ gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn+ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/logfmt.v0 v0.3.0/go.mod h1:mRLMcMLrml5h2Ux/H+4zccFOlVCiRvOvndsolsJoU8Q= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= diff --git a/main.go b/main.go index 5a4a6ae15..eca48b351 100644 --- a/main.go +++ b/main.go @@ -111,6 +111,18 @@ func main() { var p provider.Provider switch cfg.Provider { + case "akamai": + p = provider.NewAkamaiProvider( + provider.AkamaiConfig{ + DomainFilter: domainFilter, + ZoneIDFilter: zoneIDFilter, + ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain, + ClientToken: cfg.AkamaiClientToken, + ClientSecret: cfg.AkamaiClientSecret, + AccessToken: cfg.AkamaiAccessToken, + DryRun: cfg.DryRun, + }, + ) case "alibabacloud": p, err = provider.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) case "aws": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index bc9953bd9..c24761287 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -77,6 +77,10 @@ type Config struct { CloudflareZonesPerPage int CoreDNSPrefix string RcodezeroTXTEncrypt bool + AkamaiServiceConsumerDomain string + AkamaiClientToken string + AkamaiClientSecret string + AkamaiAccessToken string InfobloxGridHost string InfobloxWapiPort int InfobloxWapiUsername string @@ -169,6 +173,10 @@ var defaultConfig = &Config{ CloudflareZonesPerPage: 50, CoreDNSPrefix: "/skydns/", RcodezeroTXTEncrypt: false, + AkamaiServiceConsumerDomain: "", + AkamaiClientToken: "", + AkamaiClientSecret: "", + AkamaiAccessToken: "", InfobloxGridHost: "", InfobloxWapiPort: 443, InfobloxWapiUsername: "admin", @@ -289,7 +297,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) @@ -313,6 +321,10 @@ 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-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage) 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)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain) + app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken) + app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret) + app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken) app.Flag("infoblox-grid-host", "When using the Infoblox provider, specify the Grid Manager host (required when --provider=infoblox)").Default(defaultConfig.InfobloxGridHost).StringVar(&cfg.InfobloxGridHost) app.Flag("infoblox-wapi-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort) app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 278df11de..6a2b89fb2 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -29,140 +29,148 @@ import ( var ( minimalConfig = &Config{ - Master: "", - KubeConfig: "", - RequestTimeout: time.Second * 30, - ContourLoadBalancerService: "heptio-contour/contour", - Sources: []string{"service"}, - Namespace: "", - FQDNTemplate: "", - Compatibility: "", - Provider: "google", - GoogleProject: "", - GoogleBatchChangeSize: 1000, - GoogleBatchChangeInterval: time.Second, - DomainFilter: []string{""}, - ExcludeDomains: []string{""}, - ZoneIDFilter: []string{""}, - AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", - AWSZoneType: "", - AWSZoneTagFilter: []string{""}, - AWSAssumeRole: "", - AWSBatchChangeSize: 1000, - AWSBatchChangeInterval: time.Second, - AWSEvaluateTargetHealth: true, - AWSAPIRetries: 3, - AWSPreferCNAME: false, - AzureConfigFile: "/etc/kubernetes/azure.json", - AzureResourceGroup: "", - AzureSubscriptionID: "", - CloudflareProxied: false, - CloudflareZonesPerPage: 50, - CoreDNSPrefix: "/skydns/", - InfobloxGridHost: "", - InfobloxWapiPort: 443, - InfobloxWapiUsername: "admin", - InfobloxWapiPassword: "", - InfobloxWapiVersion: "2.3.1", - InfobloxView: "", - InfobloxSSLVerify: true, - InfobloxMaxResults: 0, - OCIConfigFile: "/etc/kubernetes/oci.yaml", - InMemoryZones: []string{""}, - PDNSServer: "http://localhost:8081", - PDNSAPIKey: "", - Policy: "sync", - Registry: "txt", - TXTOwnerID: "default", - TXTPrefix: "", - TXTCacheInterval: 0, - Interval: time.Minute, - Once: false, - DryRun: false, - LogFormat: "text", - MetricsAddress: ":7979", - LogLevel: logrus.InfoLevel.String(), - ConnectorSourceServer: "localhost:8080", - ExoscaleEndpoint: "https://api.exoscale.ch/dns", - ExoscaleAPIKey: "", - ExoscaleAPISecret: "", - CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", - CRDSourceKind: "DNSEndpoint", - RcodezeroTXTEncrypt: false, - TransIPAccountName: "", - TransIPPrivateKeyFile: "", + Master: "", + KubeConfig: "", + RequestTimeout: time.Second * 30, + ContourLoadBalancerService: "heptio-contour/contour", + Sources: []string{"service"}, + Namespace: "", + FQDNTemplate: "", + Compatibility: "", + Provider: "google", + GoogleProject: "", + GoogleBatchChangeSize: 1000, + GoogleBatchChangeInterval: time.Second, + DomainFilter: []string{""}, + ExcludeDomains: []string{""}, + ZoneIDFilter: []string{""}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", + AWSZoneType: "", + AWSZoneTagFilter: []string{""}, + AWSAssumeRole: "", + AWSBatchChangeSize: 1000, + AWSBatchChangeInterval: time.Second, + AWSEvaluateTargetHealth: true, + AWSAPIRetries: 3, + AWSPreferCNAME: false, + AzureConfigFile: "/etc/kubernetes/azure.json", + AzureResourceGroup: "", + AzureSubscriptionID: "", + CloudflareProxied: false, + CloudflareZonesPerPage: 50, + CoreDNSPrefix: "/skydns/", + AkamaiServiceConsumerDomain: "", + AkamaiClientToken: "", + AkamaiClientSecret: "", + AkamaiAccessToken: "", + InfobloxGridHost: "", + InfobloxWapiPort: 443, + InfobloxWapiUsername: "admin", + InfobloxWapiPassword: "", + InfobloxWapiVersion: "2.3.1", + InfobloxView: "", + InfobloxSSLVerify: true, + InfobloxMaxResults: 0, + OCIConfigFile: "/etc/kubernetes/oci.yaml", + InMemoryZones: []string{""}, + PDNSServer: "http://localhost:8081", + PDNSAPIKey: "", + Policy: "sync", + Registry: "txt", + TXTOwnerID: "default", + TXTPrefix: "", + TXTCacheInterval: 0, + Interval: time.Minute, + Once: false, + DryRun: false, + LogFormat: "text", + MetricsAddress: ":7979", + LogLevel: logrus.InfoLevel.String(), + ConnectorSourceServer: "localhost:8080", + ExoscaleEndpoint: "https://api.exoscale.ch/dns", + ExoscaleAPIKey: "", + ExoscaleAPISecret: "", + CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", + CRDSourceKind: "DNSEndpoint", + RcodezeroTXTEncrypt: false, + TransIPAccountName: "", + TransIPPrivateKeyFile: "", } overriddenConfig = &Config{ - Master: "http://127.0.0.1:8080", - KubeConfig: "/some/path", - RequestTimeout: time.Second * 77, - ContourLoadBalancerService: "heptio-contour-other/contour-other", - Sources: []string{"service", "ingress", "connector"}, - Namespace: "namespace", - IgnoreHostnameAnnotation: true, - FQDNTemplate: "{{.Name}}.service.example.com", - Compatibility: "mate", - Provider: "google", - GoogleProject: "project", - GoogleBatchChangeSize: 100, - GoogleBatchChangeInterval: time.Second * 2, - DomainFilter: []string{"example.org", "company.com"}, - ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, - ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, - AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", - AWSZoneType: "private", - AWSZoneTagFilter: []string{"tag=foo"}, - AWSAssumeRole: "some-other-role", - AWSBatchChangeSize: 100, - AWSBatchChangeInterval: time.Second * 2, - AWSEvaluateTargetHealth: false, - AWSAPIRetries: 13, - AWSPreferCNAME: true, - AzureConfigFile: "azure.json", - AzureResourceGroup: "arg", - AzureSubscriptionID: "arg", - CloudflareProxied: true, - CloudflareZonesPerPage: 20, - CoreDNSPrefix: "/coredns/", - InfobloxGridHost: "127.0.0.1", - InfobloxWapiPort: 8443, - InfobloxWapiUsername: "infoblox", - InfobloxWapiPassword: "infoblox", - InfobloxWapiVersion: "2.6.1", - InfobloxView: "internal", - InfobloxSSLVerify: false, - InfobloxMaxResults: 2000, - OCIConfigFile: "oci.yaml", - InMemoryZones: []string{"example.org", "company.com"}, - PDNSServer: "http://ns.example.com:8081", - PDNSAPIKey: "some-secret-key", - PDNSTLSEnabled: true, - TLSCA: "/path/to/ca.crt", - TLSClientCert: "/path/to/cert.pem", - TLSClientCertKey: "/path/to/key.pem", - Policy: "upsert-only", - Registry: "noop", - TXTOwnerID: "owner-1", - TXTPrefix: "associated-txt-record", - TXTCacheInterval: 12 * time.Hour, - Interval: 10 * time.Minute, - Once: true, - DryRun: true, - LogFormat: "json", - MetricsAddress: "127.0.0.1:9099", - LogLevel: logrus.DebugLevel.String(), - ConnectorSourceServer: "localhost:8081", - ExoscaleEndpoint: "https://api.foo.ch/dns", - ExoscaleAPIKey: "1", - ExoscaleAPISecret: "2", - CRDSourceAPIVersion: "test.k8s.io/v1alpha1", - CRDSourceKind: "Endpoint", - RcodezeroTXTEncrypt: true, - NS1Endpoint: "https://api.example.com/v1", - NS1IgnoreSSL: true, - TransIPAccountName: "transip", - TransIPPrivateKeyFile: "/path/to/transip.key", + Master: "http://127.0.0.1:8080", + KubeConfig: "/some/path", + RequestTimeout: time.Second * 77, + ContourLoadBalancerService: "heptio-contour-other/contour-other", + Sources: []string{"service", "ingress", "connector"}, + Namespace: "namespace", + IgnoreHostnameAnnotation: true, + FQDNTemplate: "{{.Name}}.service.example.com", + Compatibility: "mate", + Provider: "google", + GoogleProject: "project", + GoogleBatchChangeSize: 100, + GoogleBatchChangeInterval: time.Second * 2, + DomainFilter: []string{"example.org", "company.com"}, + ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, + ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", + AWSZoneType: "private", + AWSZoneTagFilter: []string{"tag=foo"}, + AWSAssumeRole: "some-other-role", + AWSBatchChangeSize: 100, + AWSBatchChangeInterval: time.Second * 2, + AWSEvaluateTargetHealth: false, + AWSAPIRetries: 13, + AWSPreferCNAME: true, + AzureConfigFile: "azure.json", + AzureResourceGroup: "arg", + AzureSubscriptionID: "arg", + CloudflareProxied: true, + CloudflareZonesPerPage: 20, + CoreDNSPrefix: "/coredns/", + AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", + AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46", + AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46", + InfobloxGridHost: "127.0.0.1", + InfobloxWapiPort: 8443, + InfobloxWapiUsername: "infoblox", + InfobloxWapiPassword: "infoblox", + InfobloxWapiVersion: "2.6.1", + InfobloxView: "internal", + InfobloxSSLVerify: false, + InfobloxMaxResults: 2000, + OCIConfigFile: "oci.yaml", + InMemoryZones: []string{"example.org", "company.com"}, + PDNSServer: "http://ns.example.com:8081", + PDNSAPIKey: "some-secret-key", + PDNSTLSEnabled: true, + TLSCA: "/path/to/ca.crt", + TLSClientCert: "/path/to/cert.pem", + TLSClientCertKey: "/path/to/key.pem", + Policy: "upsert-only", + Registry: "noop", + TXTOwnerID: "owner-1", + TXTPrefix: "associated-txt-record", + TXTCacheInterval: 12 * time.Hour, + Interval: 10 * time.Minute, + Once: true, + DryRun: true, + LogFormat: "json", + MetricsAddress: "127.0.0.1:9099", + LogLevel: logrus.DebugLevel.String(), + ConnectorSourceServer: "localhost:8081", + ExoscaleEndpoint: "https://api.foo.ch/dns", + ExoscaleAPIKey: "1", + ExoscaleAPISecret: "2", + CRDSourceAPIVersion: "test.k8s.io/v1alpha1", + CRDSourceKind: "Endpoint", + RcodezeroTXTEncrypt: true, + NS1Endpoint: "https://api.example.com/v1", + NS1IgnoreSSL: true, + TransIPAccountName: "transip", + TransIPPrivateKeyFile: "/path/to/transip.key", } ) @@ -206,6 +214,10 @@ func TestParseFlags(t *testing.T) { "--cloudflare-proxied", "--cloudflare-zones-per-page=20", "--coredns-prefix=/coredns/", + "--akamai-serviceconsumerdomain=oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + "--akamai-client-token=o184671d5307a388180fbf7f11dbdf46", + "--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46", + "--akamai-access-token=o184671d5307a388180fbf7f11dbdf46", "--infoblox-grid-host=127.0.0.1", "--infoblox-wapi-port=8443", "--infoblox-wapi-username=infoblox", @@ -286,6 +298,10 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", "EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20", "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", + "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1", "EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443", "EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox", diff --git a/pkg/apis/externaldns/validation/validation.go b/pkg/apis/externaldns/validation/validation.go index c4e09f9eb..272dc7716 100644 --- a/pkg/apis/externaldns/validation/validation.go +++ b/pkg/apis/externaldns/validation/validation.go @@ -43,6 +43,22 @@ func ValidateConfig(cfg *externaldns.Config) error { } } + // Akamai provider specific validations + if cfg.Provider == "akamai" { + if cfg.AkamaiServiceConsumerDomain == "" { + return errors.New("no Akamai ServiceConsumerDomain specified") + } + if cfg.AkamaiClientToken == "" { + return errors.New("no Akamai client token specified") + } + if cfg.AkamaiClientSecret == "" { + return errors.New("no Akamai client secret specified") + } + if cfg.AkamaiAccessToken == "" { + return errors.New("no Akamai access token specified") + } + } + // Infoblox provider specific validations if cfg.Provider == "infoblox" { if cfg.InfobloxGridHost == "" { diff --git a/provider/akamai.go b/provider/akamai.go new file mode 100644 index 000000000..b6cdc235b --- /dev/null +++ b/provider/akamai.go @@ -0,0 +1,414 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + + c "github.com/akamai/AkamaiOPEN-edgegrid-golang/client-v1" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" + log "github.com/sirupsen/logrus" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +type akamaiClient interface { + NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) + Do(config edgegrid.Config, req *http.Request) (*http.Response, error) +} + +type akamaiOpenClient struct{} + +func (*akamaiOpenClient) NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) { + return c.NewRequest(config, method, path, body) +} + +func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) { + return c.Do(config, req) +} + +// AkamaiConfig clarifies the method signature +type AkamaiConfig struct { + DomainFilter DomainFilter + ZoneIDFilter ZoneIDFilter + ServiceConsumerDomain string + ClientToken string + ClientSecret string + AccessToken string + DryRun bool +} + +// AkamaiProvider implements the DNS provider for Akamai. +type AkamaiProvider struct { + domainFilter DomainFilter + zoneIDFilter ZoneIDFilter + config edgegrid.Config + dryRun bool + client akamaiClient +} + +type akamaiError struct { + Title string `json:"title"` + Status int `json:"status"` + Detail string `json:"detail"` + RequestID string `json:"requestId"` +} + +type akamaiZones struct { + Zones []akamaiZone `json:"zones"` +} + +type akamaiZone struct { + ContractID string `json:"contractId"` + Zone string `json:"zone"` +} + +type akamaiRecordsets struct { + Recordsets []akamaiRecord `json:"recordsets"` +} + +type akamaiRecord struct { + Name string `json:"name"` + Type string `json:"type"` + TTL int64 `json:"ttl"` + Rdata []interface{} `json:"rdata"` +} + +// NewAkamaiProvider initializes a new Akamai DNS based Provider. +func NewAkamaiProvider(akamaiConfig AkamaiConfig) *AkamaiProvider { + edgeGridConfig := edgegrid.Config{ + Host: akamaiConfig.ServiceConsumerDomain, + ClientToken: akamaiConfig.ClientToken, + ClientSecret: akamaiConfig.ClientSecret, + AccessToken: akamaiConfig.AccessToken, + MaxBody: 1024, + HeaderToSign: []string{ + "X-External-DNS", + }, + Debug: false, + } + + provider := &AkamaiProvider{ + domainFilter: akamaiConfig.DomainFilter, + zoneIDFilter: akamaiConfig.ZoneIDFilter, + config: edgeGridConfig, + dryRun: akamaiConfig.DryRun, + client: &akamaiOpenClient{}, + } + return provider +} + +func (p *AkamaiProvider) request(method, path string, body io.Reader) ([]byte, error) { + req, err := p.client.NewRequest(p.config, method, fmt.Sprintf("https://%s/%s", p.config.Host, path), body) + if err != nil { + log.Errorf("Akamai client failed to prepare the request") + return nil, err + } + resp, err := p.client.Do(p.config, req) + if err != nil { + log.Errorf("Akamai client failed to do the request") + return nil, err + } + + //204 means no body on success by choice + if resp.StatusCode == 204 { + return nil, nil + } + + defer resp.Body.Close() + byt, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Errorf("ioutil failed to read the response-body of the request to Akamai") + return nil, err + } + + //catch authentication errors here, they aren't handled very well by the client + akamaiError := akamaiError{} + err = json.Unmarshal([]byte(byt), &akamaiError) + if err != nil { + log.Errorf("Failed to unmarshal the response-body of the request to Akamai") + return nil, err + } + if akamaiError.Status >= 300 { + log.Errorf("Received an error from Akamai!\ntitle: %s \nstatus: %v \ndetail: %s \nrequestId: %s", akamaiError.Title, akamaiError.Status, akamaiError.Detail, akamaiError.RequestID) + return nil, errors.New("AkamaiError") + } + + return byt, err +} + +//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzones +func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) { + log.Debugf("Trying to fetch zones from Akamai") + res, err := p.request("GET", "config-dns/v2/zones?showAll=true&types=primary%2Csecondary", nil) + if err != nil { + log.Errorf("Failed to fetch zones from Akamai") + return zones, err + } + + err = json.Unmarshal([]byte(res), &zones) + if err != nil { + log.Errorf("Could not unmarshal json response from Akamai on zone request") + return zones, err + } + + filteredZones := akamaiZones{} + for _, zone := range zones.Zones { + if !p.zoneIDFilter.Match(zone.ContractID) { + log.Debugf("Skipping zone: '%s' with ZoneID: '%s', it does not match against ZoneID filters", zone.Zone, zone.ContractID) + continue + } + filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractID, Zone: zone.Zone}) + log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractID) + } + lenFilteredZones := len(filteredZones.Zones) + if lenFilteredZones == 0 { + log.Warnf("No zones could be fetched") + } else { + log.Debugf("Fetched '%d' zones from Akamai", lenFilteredZones) + } + + return filteredZones, nil +} + +//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzonerecordsets +func (p *AkamaiProvider) fetchRecordSet(zone string) (recordSet akamaiRecordsets, err error) { + log.Debugf("Trying to fetch endpoints for zone: '%s' from Akamai", zone) + res, err := p.request("GET", "config-dns/v2/zones/"+zone+"/recordsets?showAll=true&types=A%2CTXT%2CCNAME", nil) + if err != nil { + log.Errorf("Failed to fetch records from Akamai for zone: '%s'", zone) + return recordSet, err + } + + err = json.Unmarshal([]byte(res), &recordSet) + if err != nil { + log.Errorf("Could not unmarshal json response from Akamai for zone: '%s' on request", zone) + return recordSet, err + } + + return recordSet, nil +} + +//Records returns the list of records in a given zone. +func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) { + zones, err := p.fetchZones() + if err != nil { + log.Warnf("No zones to fetch endpoints from!") + return endpoints, err + } + for _, zone := range zones.Zones { + records, _ := p.fetchRecordSet(zone.Zone) + for _, record := range records.Recordsets { + rdata := make([]string, len(record.Rdata)) + + for i, v := range record.Rdata { + rdata[i] = v.(string) + } + + if !p.domainFilter.Match(record.Name) { + log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", record.Name, record.Type) + continue + } + + endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, rdata...)) + log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", record.Name, record.Type, rdata) + } + } + lenEndpoints := len(endpoints) + if lenEndpoints == 0 { + log.Warnf("No endpoints could be fetched") + } else { + log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints) + } + + return endpoints, nil +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + zoneNameIDMapper := zoneIDName{} + zones, err := p.fetchZones() + if err != nil { + log.Warnf("No zones to fetch endpoints from!") + return nil + } + + for _, z := range zones.Zones { + zoneNameIDMapper[z.Zone] = z.Zone + } + + _, cf := p.createRecords(zoneNameIDMapper, changes.Create) + if !p.dryRun { + if len(cf) > 0 { + log.Warnf("Not all desired endpoints could be created, retrying next iteration") + for _, f := range cf { + log.Warnf("Not created was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType) + } + } + } + + _, df := p.deleteRecords(zoneNameIDMapper, changes.Delete) + if !p.dryRun { + if len(df) > 0 { + log.Warnf("Not all endpoints that require deletion could be deleted, retrying next iteration") + for _, f := range df { + log.Warnf("Not deleted was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType) + } + } + } + + _, uf := p.updateNewRecords(zoneNameIDMapper, changes.UpdateNew) + if !p.dryRun { + if len(uf) > 0 { + log.Warnf("Not all endpoints that require updating could be updated, retrying next iteration") + for _, f := range uf { + log.Warnf("Not updated was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType) + } + } + } + + for _, uold := range changes.UpdateOld { + if !p.dryRun { + log.Debugf("UpdateOld (ignored) for DNSName: '%s' RecordType: '%s'", uold.DNSName, uold.RecordType) + } + } + + return nil +} + +func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ...string) *akamaiRecord { + cleanTargets := make([]interface{}, len(targets)) + for idx, target := range targets { + cleanTargets[idx] = strings.TrimSuffix(target, ".") + } + return &akamaiRecord{ + Name: strings.TrimSuffix(dnsName, "."), + Rdata: cleanTargets, + Type: recordType, + TTL: 300, + } +} + +func (p *AkamaiProvider) newAkamaiRecordsets(dnsName, recordType string, targets ...string) *akamaiRecordsets { + akamaiRecords := make([]akamaiRecord, 0) + akamaiRecord := p.newAkamaiRecord(dnsName, recordType, targets...) + akamaiRecords = append(akamaiRecords, *akamaiRecord) + + return &akamaiRecordsets{ + Recordsets: akamaiRecords, + } +} + +func (p *AkamaiProvider) createRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) { + for _, endpoint := range endpoints { + + if !p.domainFilter.Match(endpoint.DNSName) { + log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) + continue + } + if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" { + akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...) + body, _ := json.MarshalIndent(akamaiRecord, "", " ") + + log.Infof("Create new Endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets) + + if p.dryRun { + continue + } + _, err := p.request("POST", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body)) + if err != nil { + log.Errorf("Failed to create Akamai endpoint DNSName: '%s' RecordType: '%s' for zone: '%s'", endpoint.DNSName, endpoint.RecordType, zoneName) + failed = append(failed, endpoint) + continue + } + created = append(created, endpoint) + } else { + log.Warnf("No matching zone for endpoint addition DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType) + failed = append(failed, endpoint) + } + } + return created, failed +} + +func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) { + for _, endpoint := range endpoints { + + if !p.domainFilter.Match(endpoint.DNSName) { + log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) + continue + } + if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" { + log.Infof("Deletion at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets) + + if p.dryRun { + continue + } + + _, err := p.request("DELETE", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, nil) + if err != nil { + log.Errorf("Failed to delete Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName) + failed = append(failed, endpoint) + continue + } + deleted = append(deleted, endpoint) + } else { + log.Warnf("No matching zone for endpoint deletion DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType) + failed = append(failed, endpoint) + } + } + return deleted, failed +} + +func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) { + for _, endpoint := range endpoints { + + if !p.domainFilter.Match(endpoint.DNSName) { + log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) + continue + } + if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" { + akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...) + body, _ := json.MarshalIndent(akamaiRecord, "", " ") + + log.Infof("Updating endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets) + + if p.dryRun { + continue + } + + _, err := p.request("PUT", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body)) + if err != nil { + log.Errorf("Failed to update Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName) + failed = append(failed, endpoint) + continue + } + updated = append(updated, endpoint) + } else { + log.Warnf("No matching zone for endpoint update DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType) + failed = append(failed, endpoint) + } + } + return updated, failed +} diff --git a/provider/akamai_test.go b/provider/akamai_test.go new file mode 100644 index 000000000..d61ca5aab --- /dev/null +++ b/provider/akamai_test.go @@ -0,0 +1,336 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +type mockAkamaiClient struct { + mock.Mock +} + +func (m *mockAkamaiClient) NewRequest(config edgegrid.Config, met, p string, b io.Reader) (*http.Request, error) { + switch { + case met == "GET": + switch { + case strings.HasPrefix(p, "https:///config-dns/v2/zones?"): + b = bytes.NewReader([]byte("{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"},{\"contractId\":\"Exclude-Me\",\"zone\":\"exclude.me\"}]}")) + case strings.HasPrefix(p, "https:///config-dns/v2/zones/example.com/"): + b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}")) + case strings.HasPrefix(p, "https:///config-dns/v2/zones/exclude.me/"): + b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}")) + } + case met == "DELETE": + b = bytes.NewReader([]byte("{\"title\": \"Success\", \"status\": 200, \"detail\": \"Record deleted\", \"requestId\": \"4321\"}")) + } + req := httptest.NewRequest(met, p, b) + return req, nil +} + +func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) { + handler := func(w http.ResponseWriter, r *http.Request) { + b, _ := ioutil.ReadAll(r.Body) + io.WriteString(w, string(b)) + } + w := httptest.NewRecorder() + handler(w, req) + + resp := w.Result() + + return resp, nil +} + +func TestRequestError(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + m := "GET" + p := "" + b := "{\"title\": \"MockError\", \"status\": 404, \"detail\": \"MockError\", \"requestId\": \"1234\"}" + x, _ := c.request(m, p, bytes.NewReader([]byte(b))) + assert.Nil(t, x) +} + +func TestFetchZonesZoneIDFilter(t *testing.T) { + config := AkamaiConfig{ + ZoneIDFilter: NewZoneIDFilter([]string{"Test"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + x, _ := c.fetchZones() + y, _ := json.Marshal(x) + if assert.NotNil(t, y) { + assert.Equal(t, "{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"}]}", string(y)) + } +} + +func TestFetchZonesEmpty(t *testing.T) { + config := AkamaiConfig{ + DomainFilter: NewDomainFilter([]string{"Nonexistent"}), + ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + x, _ := c.fetchZones() + y, _ := json.Marshal(x) + if assert.NotNil(t, y) { + assert.Equal(t, "{\"zones\":null}", string(y)) + } +} + +func TestFetchRecordset1(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + x, _ := c.fetchRecordSet("example.com") + y, _ := json.Marshal(x) + if assert.NotNil(t, y) { + assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}", string(y)) + } +} + +func TestFetchRecordset2(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + x, _ := c.fetchRecordSet("exclude.me") + y, _ := json.Marshal(x) + if assert.NotNil(t, y) { + assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}", string(y)) + } +} + +func TestAkamaiRecords(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2")) + + x, _ := c.Records(context.Background()) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestAkamaiRecordsEmpty(t *testing.T) { + config := AkamaiConfig{ + ZoneIDFilter: NewZoneIDFilter([]string{"Nonexistent"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + x, _ := c.Records(context.Background()) + assert.Nil(t, x) +} + +func TestAkamaiRecordsFilters(t *testing.T) { + config := AkamaiConfig{ + DomainFilter: NewDomainFilter([]string{"www.exclude.me"}), + ZoneIDFilter: NewZoneIDFilter([]string{"Exclude-Me"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2")) + + x, _ := c.Records(context.Background()) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestCreateRecords(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + + x, _ := c.createRecords(zoneNameIDMapper, endpoints) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestCreateRecordsDomainFilter(t *testing.T) { + config := AkamaiConfig{ + DomainFilter: NewDomainFilter([]string{"example.com"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + + x, _ := c.createRecords(zoneNameIDMapper, exclude) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestDeleteRecords(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + + x, _ := c.deleteRecords(zoneNameIDMapper, endpoints) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestDeleteRecordsDomainFilter(t *testing.T) { + config := AkamaiConfig{ + DomainFilter: NewDomainFilter([]string{"example.com"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + + x, _ := c.deleteRecords(zoneNameIDMapper, exclude) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestUpdateRecords(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + + x, _ := c.updateNewRecords(zoneNameIDMapper, endpoints) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestUpdateRecordsDomainFilter(t *testing.T) { + config := AkamaiConfig{ + DomainFilter: NewDomainFilter([]string{"example.com"}), + } + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + zoneNameIDMapper := zoneIDName{"example.com": "example.com"} + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default")) + exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + + x, _ := c.updateNewRecords(zoneNameIDMapper, exclude) + if assert.NotNil(t, x) { + assert.Equal(t, endpoints, x) + } +} + +func TestAkamaiApplyChanges(t *testing.T) { + config := AkamaiConfig{} + + client := &mockAkamaiClient{} + c := NewAkamaiProvider(config) + c.client = client + + changes := &plan.Changes{} + changes.Create = []*endpoint.Endpoint{ + {DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300}, + {DNSName: "test.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300}, + {DNSName: "test.this.example.com", RecordType: "A", Targets: endpoint.Targets{"127.0.0.1"}, RecordTTL: 300}, + {DNSName: "www.example.com", RecordType: "TXT", Targets: endpoint.Targets{"heritage=external-dns,external-dns/owner=default"}, RecordTTL: 300}, + {DNSName: "test.example.com", RecordType: "TXT", Targets: endpoint.Targets{"heritage=external-dns,external-dns/owner=default"}, RecordTTL: 300}, + {DNSName: "test.this.example.com", RecordType: "TXT", Targets: endpoint.Targets{"heritage=external-dns,external-dns/owner=default"}, RecordTTL: 300}, + {DNSName: "another.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}}, + } + changes.Delete = []*endpoint.Endpoint{{DNSName: "delete.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300}} + changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "old.example.com", RecordType: "A", Targets: endpoint.Targets{"target-old"}, RecordTTL: 300}} + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "update.example.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 300}} + apply := c.ApplyChanges(context.Background(), changes) + assert.Nil(t, apply) +}