diff --git a/go.mod b/go.mod index eb260eb76..06b632e7a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/Azure/go-autorest/autorest/adal v0.9.5 github.com/Azure/go-autorest/autorest/azure/auth v0.5.3 github.com/Azure/go-autorest/autorest/to v0.4.0 - github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11 + github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0 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 diff --git a/go.sum b/go.sum index 48ee1381e..12e341d3b 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq github.com/ahmetb/gen-crd-api-reference-docs v0.1.5/go.mod h1:P/XzJ+c2+khJKNKABcm2biRwk2QAuwbLf8DlXuaL7WM= github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11 h1:QGjNHMwoPYxE5NpOAc8kpd2KTY293/oFk5BWdjkza+k= github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11/go.mod h1:L+HB2uBoDgi3+r1pJEJcbGwyyHhd2QXaGsKLbDwtm8Q= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0 h1:FJF58TWBaQnGqTIcziIP5/z3TTqWUn8fh26z03oZE2c= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8= 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= diff --git a/main.go b/main.go index 867de5da9..a3c18a2fd 100644 --- a/main.go +++ b/main.go @@ -151,7 +151,7 @@ func main() { var p provider.Provider switch cfg.Provider { case "akamai": - p = akamai.NewAkamaiProvider( + p, err = akamai.NewAkamaiProvider( akamai.AkamaiConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, @@ -159,9 +159,10 @@ func main() { ClientToken: cfg.AkamaiClientToken, ClientSecret: cfg.AkamaiClientSecret, AccessToken: cfg.AkamaiAccessToken, + EdgercPath: cfg.AkamaiEdgercPath, + EdgercSection: cfg.AkamaiEdgercSection, DryRun: cfg.DryRun, - }, - ) + }, nil) case "alibabacloud": p, err = alibabacloud.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 c6f96ae1a..621818f36 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -88,6 +88,8 @@ type Config struct { AkamaiClientToken string AkamaiClientSecret string AkamaiAccessToken string + AkamaiEdgercPath string + AkamaiEdgercSection string InfobloxGridHost string InfobloxWapiPort int InfobloxWapiUsername string @@ -194,6 +196,8 @@ var defaultConfig = &Config{ AkamaiClientToken: "", AkamaiClientSecret: "", AkamaiAccessToken: "", + AkamaiEdgercSection: "", + AkamaiEdgercPath: "", InfobloxGridHost: "", InfobloxWapiPort: 443, InfobloxWapiUsername: "admin", @@ -353,10 +357,12 @@ 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("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) + app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret) + app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken) + app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath) + app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection) 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 02fd16ffd..6eec94302 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -66,6 +66,8 @@ var ( AkamaiClientToken: "", AkamaiClientSecret: "", AkamaiAccessToken: "", + AkamaiEdgercPath: "", + AkamaiEdgercSection: "", InfobloxGridHost: "", InfobloxWapiPort: 443, InfobloxWapiUsername: "admin", @@ -144,6 +146,8 @@ var ( AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46", AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46", + AkamaiEdgercPath: "/home/test/.edgerc", + AkamaiEdgercSection: "default", InfobloxGridHost: "127.0.0.1", InfobloxWapiPort: 8443, InfobloxWapiUsername: "infoblox", @@ -235,6 +239,8 @@ func TestParseFlags(t *testing.T) { "--akamai-client-token=o184671d5307a388180fbf7f11dbdf46", "--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46", "--akamai-access-token=o184671d5307a388180fbf7f11dbdf46", + "--akamai-edgerc-path=/home/test/.edgerc", + "--akamai-edgerc-section=default", "--infoblox-grid-host=127.0.0.1", "--infoblox-wapi-port=8443", "--infoblox-wapi-username=infoblox", @@ -328,6 +334,8 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46", + "EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc", + "EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default", "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 58371ed6a..517c0ea64 100644 --- a/pkg/apis/externaldns/validation/validation.go +++ b/pkg/apis/externaldns/validation/validation.go @@ -45,16 +45,20 @@ func ValidateConfig(cfg *externaldns.Config) error { // Akamai provider specific validations if cfg.Provider == "akamai" { - if cfg.AkamaiServiceConsumerDomain == "" { + edgerc := false + if cfg.AkamaiEdgercPath != "" { + edgerc = true + } + if cfg.AkamaiServiceConsumerDomain == "" && !edgerc { return errors.New("no Akamai ServiceConsumerDomain specified") } - if cfg.AkamaiClientToken == "" { + if cfg.AkamaiClientToken == "" && !edgerc { return errors.New("no Akamai client token specified") } - if cfg.AkamaiClientSecret == "" { + if cfg.AkamaiClientSecret == "" && !edgerc { return errors.New("no Akamai client secret specified") } - if cfg.AkamaiAccessToken == "" { + if cfg.AkamaiAccessToken == "" && !edgerc { return errors.New("no Akamai access token specified") } } diff --git a/provider/akamai/akamai.go b/provider/akamai/akamai.go index 15acd1ed1..61dee2eac 100644 --- a/provider/akamai/akamai.go +++ b/provider/akamai/akamai.go @@ -17,15 +17,13 @@ limitations under the License. package akamai import ( - "bytes" "context" - "encoding/json" "fmt" - "io" - "net/http" + "os" + "strconv" "strings" - c "github.com/akamai/AkamaiOPEN-edgegrid-golang/client-v1" + dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" log "github.com/sirupsen/logrus" @@ -34,22 +32,23 @@ import ( "sigs.k8s.io/external-dns/provider" ) -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) +const ( + // Default Record TTL + edgeDNSRecordTTL = 600 + maxUint = ^uint(0) + maxInt = int(maxUint >> 1) +) + +// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing. +type AkamaiDNSService interface { + ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) + GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) + GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error) + DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error + UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error + CreateRecordsets(recordsets *dns.Recordsets, zone string, recLock bool) 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 endpoint.DomainFilter ZoneIDFilter provider.ZoneIDFilter @@ -57,17 +56,25 @@ type AkamaiConfig struct { ClientToken string ClientSecret string AccessToken string + EdgercPath string + EdgercSection string + MaxBody int + AccountKey string DryRun bool } // AkamaiProvider implements the DNS provider for Akamai. type AkamaiProvider struct { provider.BaseProvider + // Edgedns zones to filter on domainFilter endpoint.DomainFilter + // Contract Ids to filter on zoneIDFilter provider.ZoneIDFilter - config edgegrid.Config - dryRun bool - client akamaiClient + // Edgegrid library configuration + config *edgegrid.Config + dryRun bool + // Defines client. Allows for mocking. + client AkamaiDNSService } type akamaiZones struct { @@ -79,84 +86,126 @@ type akamaiZone struct { 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, +func NewAkamaiProvider(akamaiConfig AkamaiConfig, akaService AkamaiDNSService) (provider.Provider, error) { + var edgeGridConfig edgegrid.Config + + /* + log.Debugf("Host: %s", akamaiConfig.ServiceConsumerDomain) + log.Debugf("ClientToken: %s", akamaiConfig.ClientToken) + log.Debugf("ClientSecret: %s", akamaiConfig.ClientSecret) + log.Debugf("AccessToken: %s", akamaiConfig.AccessToken) + log.Debugf("EdgePath: %s", akamaiConfig.EdgercPath) + log.Debugf("EdgeSection: %s", akamaiConfig.EdgercSection) + */ + // environment overrides edgerc file but config needs to be complete + if akamaiConfig.ServiceConsumerDomain == "" || akamaiConfig.ClientToken == "" || akamaiConfig.ClientSecret == "" || akamaiConfig.AccessToken == "" { + // Kubernetes config incomplete or non existent. Can't mix and match. + // Look for Akamai environment or .edgerd creds + var err error + edgeGridConfig, err = edgegrid.Init(akamaiConfig.EdgercPath, akamaiConfig.EdgercSection) // use default .edgerc location and section + if err != nil { + log.Errorf("Edgegrid Init Failed") + return &AkamaiProvider{}, err // return empty provider for backward compatibility + } + edgeGridConfig.HeaderToSign = append(edgeGridConfig.HeaderToSign, "X-External-DNS") + } else { + // Use external-dns config + edgeGridConfig = edgegrid.Config{ + Host: akamaiConfig.ServiceConsumerDomain, + ClientToken: akamaiConfig.ClientToken, + ClientSecret: akamaiConfig.ClientSecret, + AccessToken: akamaiConfig.AccessToken, + MaxBody: 131072, // same default val as used by Edgegrid + HeaderToSign: []string{ + "X-External-DNS", + }, + Debug: false, + } + // Check for edgegrid overrides + if envval, ok := os.LookupEnv("AKAMAI_MAX_BODY"); ok { + if i, err := strconv.Atoi(envval); err == nil { + edgeGridConfig.MaxBody = i + log.Debugf("Edgegrid maxbody set to %s", envval) + } + } + if envval, ok := os.LookupEnv("AKAMAI_ACCOUNT_KEY"); ok { + edgeGridConfig.AccountKey = envval + log.Debugf("Edgegrid applying account key %s", envval) + } + if envval, ok := os.LookupEnv("AKAMAI_DEBUG"); ok { + if dbgval, err := strconv.ParseBool(envval); err == nil { + edgeGridConfig.Debug = dbgval + log.Debugf("Edgegrid debug set to %s", envval) + } + } } provider := &AkamaiProvider{ domainFilter: akamaiConfig.DomainFilter, zoneIDFilter: akamaiConfig.ZoneIDFilter, - config: edgeGridConfig, + config: &edgeGridConfig, dryRun: akamaiConfig.DryRun, - client: &akamaiOpenClient{}, } - return provider + if akaService != nil { + log.Debugf("Using STUB") + provider.client = akaService + } else { + provider.client = provider + } + + // Init library for direct endpoint calls + dns.Init(edgeGridConfig) + + return provider, nil } -func (p *AkamaiProvider) request(method, path string, body io.Reader) (*http.Response, 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 - } - if !c.IsSuccess(resp) { - return nil, c.NewAPIError(resp) - } - - return resp, err +func (p AkamaiProvider) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) { + return dns.ListZones(queryArgs) } -//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") - resp, err := p.request("GET", "config-dns/v2/zones?showAll=true&types=primary%2Csecondary", nil) +func (p AkamaiProvider) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) { + return dns.GetRecordsets(zone, queryArgs) +} + +func (p AkamaiProvider) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error { + return recordsets.Save(zone, reclock) +} + +func (p AkamaiProvider) GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error) { + return dns.GetRecord(zone, name, recordtype) +} + +func (p AkamaiProvider) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error { + return record.Delete(zone, recLock) +} + +func (p AkamaiProvider) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error { + return record.Update(zone, recLock) +} + +// Fetch zones using Edgegrid DNS v2 API +func (p AkamaiProvider) fetchZones() (akamaiZones, error) { + log.Debugf("Fetching Akamai Edge DNS zones") + filteredZones := akamaiZones{Zones: make([]akamaiZone, 0)} + queryArgs := dns.ZoneListQueryArgs{Types: "primary", ShowAll: true} + // filter based on contractIds + if len(p.zoneIDFilter.ZoneIDs) > 0 { + queryArgs.ContractIds = strings.Join(p.zoneIDFilter.ZoneIDs, ",") + } + resp, err := p.client.ListZones(queryArgs) // don't worry about paged results + if err != nil { log.Errorf("Failed to fetch zones from Akamai") - return zones, err + return filteredZones, err } - err = json.NewDecoder(resp.Body).Decode(&zones) - if err != nil { - log.Errorf("Could not decode json response from Akamai on zone request") - return zones, err - } - defer resp.Body.Close() - - 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 + for _, zone := range resp.Zones { + //log.Debugf("Evaluating zone: %s", zone.Zone) + if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() { + filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone}) + log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId) } - 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 { @@ -168,53 +217,45 @@ func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) { 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) - resp, 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 - } - defer resp.Body.Close() - - err = json.NewDecoder(resp.Body).Decode(&recordSet) - if err != nil { - log.Errorf("Could not decode 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() +func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) { + log.Debugf("Entering Records function") + if p.config == nil { + log.Errorf("Akamai provider failed initialization!") + return endpoints, fmt.Errorf("edge dns provider is not initialized") + } + + zones, err := p.fetchZones() // returns a filtered set of zones if err != nil { - log.Warnf("No zones to fetch endpoints from!") + log.Warnf("Failed to identify target zones! Error: %s", err.Error()) return endpoints, err } for _, zone := range zones.Zones { - records, err := p.fetchRecordSet(zone.Zone) + recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{}) if err != nil { - log.Warnf("No recordsets could be fetched for zone: '%s'!", zone.Zone) + log.Errorf("Recordsets retrieval for zone: '%s' failed! %s", zone.Zone, err.Error()) continue } + if len(recordsets.Recordsets) == 0 { + log.Warnf("Zone %s contains no recordsets", 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) + for _, recordset := range recordsets.Recordsets { + if !provider.SupportedRecordType(recordset.Type) { + log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s'. Record type not supported.", recordset.Name, recordset.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) + if !p.domainFilter.Match(recordset.Name) { + log.Debugf("Skipping endpoint. Record name %s doesn't match containing zone %s.", recordset.Name, zone) + continue + } + var temp interface{} = int64(recordset.TTL) + var ttl endpoint.TTL = endpoint.TTL(temp.(int64)) //endpoint.TTL) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(recordset.Name, + recordset.Type, + ttl, + trimTxtRdata(recordset.Rdata, recordset.Type)...)) + log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", recordset.Name, recordset.Type, recordset.Rdata) } } lenEndpoints := len(endpoints) @@ -222,161 +263,247 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin log.Warnf("No endpoints could be fetched") } else { log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints) + log.Debugf("Endpoints [%v]", endpoints) } 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 { +func (p AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + log.Debugf("Entering ApplyChanges") + + if p.config == nil { + log.Errorf("Akamai provider failed initialization!") + return fmt.Errorf("edge dns provider failed initialization") + } + zoneNameIDMapper := provider.ZoneIDName{} zones, err := p.fetchZones() if err != nil { - log.Warnf("No zones to fetch endpoints from!") - return nil + log.Errorf("Failed to fetch zones from Akamai") + return err } for _, z := range zones.Zones { zoneNameIDMapper[z.Zone] = z.Zone } + log.Debugf("Processing zones: [%v]", zoneNameIDMapper) - _, 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) + // Create recodsets + log.Debugf("Create Changes requested [%v]", changes.Create) + if err := p.createRecordsets(zoneNameIDMapper, changes.Create); err != nil { + return err + } + // Delete recordsets + log.Debugf("Delete Changes requested [%v]", changes.Delete) + if err := p.deleteRecordsets(zoneNameIDMapper, changes.Delete); err != nil { + return err + } + // Update recordsets + log.Debugf("Update Changes requested [%v]", changes.UpdateNew) + if err := p.updateNewRecordsets(zoneNameIDMapper, changes.UpdateNew); err != nil { + return err + } + // Check that all old endpoints were accounted for + revRecs := changes.Delete + revRecs = append(revRecs, changes.UpdateNew...) + for _, rec := range changes.UpdateOld { + found := false + for _, r := range revRecs { + if rec.DNSName == r.DNSName { + found = true + break } } - } - - _, 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) + if !found { + log.Warnf("UpdateOld endpoint '%s' is not accounted for in UpdateNew|Delete endpoint list", rec.DNSName) } } 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{ +// Create DNS Recordset +func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) dns.Recordset { + return dns.Recordset{ Name: strings.TrimSuffix(dnsName, "."), - Rdata: cleanTargets, + Rdata: targets, Type: recordType, - TTL: 300, + TTL: ttl, } } -func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.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) +// cleanTargets preps recordset rdata if necessary for EdgeDNS +func cleanTargets(rtype string, targets ...string) []string { + log.Debugf("Targets to clean: [%v]", targets) + //var targets []string = tgts + if rtype == "CNAME" || rtype == "SRV" { + for idx, target := range targets { + targets[idx] = strings.TrimSuffix(target, ".") + } + } else if rtype == "TXT" { + for idx, target := range targets { + log.Debugf("TXT data to clean: [%s]", target) + // need to embed text data in quotes. Make sure not piling on + target = strings.Trim(target, "\"") + // bug in DNS API with embedded quotes. + if strings.Contains(target, "owner") && strings.Contains(target, "\"") { + target = strings.ReplaceAll(target, "\"", "`") + } + targets[idx] = "\"" + target + "\"" + } + } + log.Debugf("Clean targets: [%v]", targets) + + return targets +} + +// trimTxtRdata removes surrounding quotes for received TXT rdata +func trimTxtRdata(rdata []string, rtype string) []string { + if rtype == "TXT" { + for idx, d := range rdata { + //rdata[idx] = strings.Trim(d, "\"") + if strings.Contains(d, "`") { + rdata[idx] = strings.ReplaceAll(d, "`", "\"") + } + } + } + log.Debugf("Trimmed data: [%v]", rdata) + + return rdata +} + +func ttlAsInt(src endpoint.TTL) int { + var temp interface{} = int64(src) + var temp64 = temp.(int64) + var ttl int = edgeDNSRecordTTL // int + if temp64 > 0 && temp64 <= int64(maxInt) { + ttl = int(temp64) + } + + return ttl +} + +// Create Endpoint Recordsets +func (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error { + if len(endpoints) == 0 { + log.Info("No endpoints to create") + return nil + } + + endpointsByZone := edgeChangesByZone(zoneNameIDMapper, endpoints) + + // create all recordsets by zone + for zone, endpoints := range endpointsByZone { + recordsets := &dns.Recordsets{Recordsets: make([]dns.Recordset, 0)} + for _, endpoint := range endpoints { + newrec := newAkamaiRecordset(endpoint.DNSName, + endpoint.RecordType, + ttlAsInt(endpoint.RecordTTL), + cleanTargets(endpoint.RecordType, endpoint.Targets...)) + logfields := log.Fields{ + "record": newrec.Name, + "type": newrec.Type, + "ttl": newrec.TTL, + "target": fmt.Sprintf("%v", newrec.Rdata), + "zone": zone, + } + log.WithFields(logfields).Info("Creating recordsets") + recordsets.Recordsets = append(recordsets.Recordsets, newrec) + } + + if p.dryRun { 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) + // Create recordsets all at once + err := p.client.CreateRecordsets(recordsets, zone, true) + if err != nil { + log.Errorf("Failed to create endpoints for DNS zone %s. Error: %s", zone, err.Error()) + return err } } - return created, failed + + return nil } -func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) { +func (p AkamaiProvider) deleteRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error { 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) + zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName) + if zoneName == "" { + log.Debugf("Skipping Akamai Edge DNS endpoint deletion: '%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) + log.Infof("Akamai Edge DNS recordset deletion- 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 provider.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) + if p.dryRun { 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 + recName := strings.TrimSuffix(endpoint.DNSName, ".") + rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType) + if err != nil { + // error not found? + if _, ok := err.(*dns.RecordError); !ok { + return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error()) } - - _, 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) + log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType) + continue + } + if err := p.client.DeleteRecord(rec, zoneName, true); err != nil { + log.Errorf("edge dns recordset deletion failed. error: %s", err.Error()) + return err } } - return updated, failed + + return nil +} + +// Update endpoint recordsets +func (p AkamaiProvider) updateNewRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error { + for _, endpoint := range endpoints { + zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName) + if zoneName == "" { + log.Debugf("Skipping Akamai Edge DNS endpoint update: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType) + continue + } + log.Infof("Akamai Edge DNS recordset update - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets) + + if p.dryRun { + continue + } + + recName := strings.TrimSuffix(endpoint.DNSName, ".") + rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType) + if err != nil { + log.Errorf("Endpoint update. Record validation failed. Error: %s", err.Error()) + return err + } + rec.TTL = ttlAsInt(endpoint.RecordTTL) + rec.Target = cleanTargets(endpoint.RecordType, endpoint.Targets...) + if err := p.client.UpdateRecord(rec, zoneName, true); err != nil { + log.Errorf("Akamai Edge DNS recordset update failed. Error: %s", err.Error()) + return err + } + } + + return nil +} + +// edgeChangesByZone separates a multi-zone change into a single change per zone. +func edgeChangesByZone(zoneMap provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint { + createsByZone := make(map[string][]*endpoint.Endpoint, len(zoneMap)) + for _, z := range zoneMap { + createsByZone[z] = make([]*endpoint.Endpoint, 0) + } + for _, ep := range endpoints { + zone, _ := zoneMap.FindZone(ep.DNSName) + if zone != "" { + createsByZone[zone] = append(createsByZone[zone], ep) + continue + } + log.Debugf("Skipping Akamai Edge DNS creation of endpoint: '%s' type: '%s', it does not match against Domain filters", ep.DNSName, ep.RecordType) + } + + return createsByZone } diff --git a/provider/akamai/akamai_test.go b/provider/akamai/akamai_test.go index 7d0bd7a5e..a865ec7d9 100644 --- a/provider/akamai/akamai_test.go +++ b/provider/akamai/akamai_test.go @@ -17,148 +17,208 @@ limitations under the License. package akamai import ( - "bytes" "context" "encoding/json" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" + log "github.com/sirupsen/logrus" "testing" - "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" + dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) -type mockAkamaiClient struct { - mock.Mock +type edgednsStubData struct { + objType string // zone, record, recordsets + output []interface{} + updateRecords []interface{} + createRecords []interface{} } -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\"}")) - case met == "ERROR": - b = bytes.NewReader([]byte("{\"status\": 404 }")) - } - req := httptest.NewRequest(met, p, b) - return req, nil +type edgednsStub struct { + stubData map[string]edgednsStubData } -func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) { - handler := func(w http.ResponseWriter, r *http.Request) (isError bool) { - b, _ := ioutil.ReadAll(r.Body) - io.WriteString(w, string(b)) - return string(b) == "{\"status\": 404 }" +func newStub() *edgednsStub { + return &edgednsStub{ + stubData: make(map[string]edgednsStubData), } - w := httptest.NewRecorder() - err := handler(w, req) - resp := w.Result() +} - if err == true { - resp.StatusCode = 400 +func createAkamaiStubProvider(stub *edgednsStub, domfilter endpoint.DomainFilter, idfilter provider.ZoneIDFilter) (*AkamaiProvider, error) { + + akamaiConfig := AkamaiConfig{ + DomainFilter: domfilter, + ZoneIDFilter: idfilter, + ServiceConsumerDomain: "testzone.com", + ClientToken: "test_token", + ClientSecret: "test_client_secret", + AccessToken: "test_access_token", } + prov, err := NewAkamaiProvider(akamaiConfig, stub) + aprov := prov.(*AkamaiProvider) + return aprov, err +} + +func (r *edgednsStub) createStubDataEntry(objtype string) { + + log.Debugf("Creating stub data entry") + if _, exists := r.stubData[objtype]; !exists { + r.stubData[objtype] = edgednsStubData{objType: objtype} + } + + return +} + +func (r *edgednsStub) setOutput(objtype string, output []interface{}) { + + log.Debugf("Setting output to %v", output) + r.createStubDataEntry(objtype) + stubdata := r.stubData[objtype] + stubdata.output = output + r.stubData[objtype] = stubdata + + return +} + +func (r *edgednsStub) setUpdateRecords(objtype string, records []interface{}) { + + log.Debugf("Setting updaterecords to %v", records) + r.createStubDataEntry(objtype) + stubdata := r.stubData[objtype] + stubdata.updateRecords = records + r.stubData[objtype] = stubdata + + return +} + +func (r *edgednsStub) setCreateRecords(objtype string, records []interface{}) { + + log.Debugf("Setting createrecords to %v", records) + r.createStubDataEntry(objtype) + stubdata := r.stubData[objtype] + stubdata.createRecords = records + r.stubData[objtype] = stubdata + + return +} + +func (r *edgednsStub) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) { + + log.Debugf("Entering ListZones") + // Ignore Metadata` + resp := &dns.ZoneListResponse{} + zones := make([]*dns.ZoneResponse, 0) + for _, zname := range r.stubData["zone"].output { + log.Debugf("Processing output: %v", zname) + zn := &dns.ZoneResponse{Zone: zname.(string), ContractId: "contract"} + log.Debugf("Created Zone Object: %v", zn) + zones = append(zones, zn) + } + resp.Zones = zones + return resp, nil +} + +func (r *edgednsStub) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) { + + log.Debugf("Entering GetRecordsets") + // Ignore Metadata` + + resp := &dns.RecordSetResponse{} + sets := make([]dns.Recordset, 0) + for _, rec := range r.stubData["recordset"].output { + rset := rec.(dns.Recordset) + sets = append(sets, rset) + } + resp.Recordsets = sets + return resp, nil } -func TestRequestError(t *testing.T) { - config := AkamaiConfig{} +func (r *edgednsStub) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error { - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client - - m := "ERROR" - p := "" - b := "" - x, err := c.request(m, p, bytes.NewReader([]byte(b))) - assert.Nil(t, x) - assert.NotNil(t, err) + return nil } +func (r *edgednsStub) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) { + + resp := &dns.RecordBody{} + + return resp, nil +} + +func (r *edgednsStub) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error { + + return nil +} + +func (r *edgednsStub) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error { + + return nil +} + +// Test FetchZones func TestFetchZonesZoneIDFilter(t *testing.T) { - config := AkamaiConfig{ - ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.NewZoneIDFilter([]string{"Test"}) + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{"test1.testzone.com", "test2.testzone.com"}) x, _ := c.fetchZones() y, _ := json.Marshal(x) if assert.NotNil(t, y) { - assert.Equal(t, "{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"}]}", string(y)) + assert.Equal(t, "{\"zones\":[{\"contractId\":\"contract\",\"zone\":\"test1.testzone.com\"},{\"contractId\":\"contract\",\"zone\":\"test2.testzone.com\"}]}", string(y)) } } +// func TestFetchZonesEmpty(t *testing.T) { - config := AkamaiConfig{ - DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}), - ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.NewDomainFilter([]string{"Nonexistent"}) + idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"}) + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{}) 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)) + assert.Equal(t, "{\"zones\":[]}", string(y)) } } +// TestAkamaiRecords tests record endpoint func TestAkamaiRecords(t *testing.T) { - config := AkamaiConfig{} - - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{"test1.testzone.com"}) + recordsets := make([]interface{}, 0) + recordsets = append(recordsets, dns.Recordset{ + Name: "www.example.com", + Type: endpoint.RecordTypeA, + Rdata: []string{"10.0.0.2", "10.0.0.3"}, + }) + recordsets = append(recordsets, dns.Recordset{ + Name: "www.example.com", + Type: endpoint.RecordTypeTXT, + Rdata: []string{"heritage=external-dns,external-dns/owner=default"}, + }) + recordsets = append(recordsets, dns.Recordset{ + Name: "www.exclude.me", + Type: endpoint.RecordTypeA, + Rdata: []string{"192.168.0.1", "192.168.0.2"}, + }) + stub.setOutput("recordset", recordsets) 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")) @@ -170,29 +230,43 @@ func TestAkamaiRecords(t *testing.T) { } } +// func TestAkamaiRecordsEmpty(t *testing.T) { - config := AkamaiConfig{ - ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"}) + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{"test1.testzone.com"}) + recordsets := make([]interface{}, 0) + stub.setOutput("recordset", recordsets) x, _ := c.Records(context.Background()) assert.Nil(t, x) } +// func TestAkamaiRecordsFilters(t *testing.T) { - config := AkamaiConfig{ - DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}), - ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}), - } - - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.NewDomainFilter([]string{"www.exclude.me"}) + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{"www.exclude.me"}) + recordsets := make([]interface{}, 0) + recordsets = append(recordsets, dns.Recordset{ + Name: "www.example.com", + Type: endpoint.RecordTypeA, + Rdata: []string{"10.0.0.2", "10.0.0.3"}, + }) + recordsets = append(recordsets, dns.Recordset{ + Name: "www.exclude.me", + Type: endpoint.RecordTypeA, + Rdata: []string{"192.168.0.1", "192.168.0.2"}, + }) + stub.setOutput("recordset", recordsets) endpoints := make([]*endpoint.Endpoint, 0) endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2")) @@ -202,32 +276,32 @@ func TestAkamaiRecordsFilters(t *testing.T) { } } +// TestCreateRecords tests create function +// (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error func TestCreateRecords(t *testing.T) { - config := AkamaiConfig{} - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.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) - } + err = c.createRecordsets(zoneNameIDMapper, endpoints) + assert.Nil(t, err) } func TestCreateRecordsDomainFilter(t *testing.T) { - config := AkamaiConfig{ - DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) @@ -235,38 +309,36 @@ func TestCreateRecordsDomainFilter(t *testing.T) { 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) - } + err = c.createRecordsets(zoneNameIDMapper, exclude) + assert.Nil(t, err) } +// TestDeleteRecords validate delete func TestDeleteRecords(t *testing.T) { - config := AkamaiConfig{} - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.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) - } + err = c.deleteRecordsets(zoneNameIDMapper, endpoints) + assert.Nil(t, err) } +// func TestDeleteRecordsDomainFilter(t *testing.T) { - config := AkamaiConfig{ - DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.NewDomainFilter([]string{"example.com"}) + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) @@ -274,38 +346,36 @@ func TestDeleteRecordsDomainFilter(t *testing.T) { 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) - } + err = c.deleteRecordsets(zoneNameIDMapper, exclude) + assert.Nil(t, err) } +// Test record update func func TestUpdateRecords(t *testing.T) { - config := AkamaiConfig{} - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.DomainFilter{} + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.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) - } + err = c.updateNewRecordsets(zoneNameIDMapper, endpoints) + assert.Nil(t, err) } +// func TestUpdateRecordsDomainFilter(t *testing.T) { - config := AkamaiConfig{ - DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}), - } - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.NewDomainFilter([]string{"example.com"}) + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"} endpoints := make([]*endpoint.Endpoint, 0) @@ -313,19 +383,19 @@ func TestUpdateRecordsDomainFilter(t *testing.T) { 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) - } + err = c.updateNewRecordsets(zoneNameIDMapper, exclude) + assert.Nil(t, err) } func TestAkamaiApplyChanges(t *testing.T) { - config := AkamaiConfig{} - client := &mockAkamaiClient{} - c := NewAkamaiProvider(config) - c.client = client + stub := newStub() + domfilter := endpoint.NewDomainFilter([]string{"example.com"}) + idfilter := provider.ZoneIDFilter{} + c, err := createAkamaiStubProvider(stub, domfilter, idfilter) + assert.Nil(t, err) + stub.setOutput("zone", []interface{}{"example.com"}) changes := &plan.Changes{} changes.Create = []*endpoint.Endpoint{ {DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},