Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added Exoscale as Provider #625

Merged
merged 16 commits into from
Jul 13, 2018
14 changes: 13 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/nesv/go-dynect"
version = "0.6.0"

[[constraint]]
name = "github.com/exoscale/egoscale"
version = "~0.9.31"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected
* [OpenStack Designate](https://docs.openstack.org/designate/latest/)
* [PowerDNS](https://www.powerdns.com/)
* [CoreDNS](https://coredns.io/)
* [Exoscale](https://www.exoscale.com/dns/)

From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.

Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ func main() {
)
case "coredns", "skydns":
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.DryRun)
case "exoscale":
p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil
case "inmemory":
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
case "designate":
Expand Down
9 changes: 8 additions & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ type Config struct {
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
ExoscaleEndpoint string
ExoscaleAPIKey string
ExoscaleAPISecret string
}

var defaultConfig = &Config{
Expand Down Expand Up @@ -170,7 +173,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)

// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "exoscale", "inmemory", "pdns")
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("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
Expand All @@ -194,6 +197,10 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)

app.Flag("exoscale-endpoint", "Provide the endpoint for the Exoscale provider").Default("https://api.exoscale.ch/dns").StringVar(&cfg.ExoscaleEndpoint)
app.Flag("exoscale-apikey", "Provide your API Key for the Exoscale provider").Default("").StringVar(&cfg.ExoscaleAPIKey)
app.Flag("exoscale-apisecret", "Provide your API Secret for the Exoscale provider").Default("").StringVar(&cfg.ExoscaleAPISecret)

// Flags related to policies
app.Flag("policy", "Modify how DNS records are sychronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only")

Expand Down
201 changes: 201 additions & 0 deletions provider/exoscale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package provider

import (
"strings"

"github.com/exoscale/egoscale"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
log "github.com/sirupsen/logrus"
)

// EgoscaleClientI for replaceable implementation
type EgoscaleClientI interface {
GetRecords(string) ([]egoscale.DNSRecord, error)
GetDomains() ([]egoscale.DNSDomain, error)
CreateRecord(string, egoscale.DNSRecord) (*egoscale.DNSRecord, error)
DeleteRecord(string, int64) error
}

// ExoscaleProvider initialized as dns provider with no records
type ExoscaleProvider struct {
domain DomainFilter
client EgoscaleClientI
filter *zoneFilter
OnApplyChanges func(changes *plan.Changes)
}

// ExoscaleOption for Provider options
type ExoscaleOption func(*ExoscaleProvider)

// NewExoscaleProvider returns ExoscaleProvider DNS provider interface implementation
func NewExoscaleProvider(endpoint, apiKey, apiSecret string, opts ...ExoscaleOption) *ExoscaleProvider {
client := egoscale.NewClient(endpoint, apiKey, apiSecret)
return NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret, client, opts...)
}

// NewExoscaleProviderWithClient returns ExoscaleProvider DNS provider interface implementation (Client provided)
func NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret string, client EgoscaleClientI, opts ...ExoscaleOption) *ExoscaleProvider {
ep := &ExoscaleProvider{
filter: &zoneFilter{},
OnApplyChanges: func(changes *plan.Changes) {},
domain: NewDomainFilter([]string{""}),
client: client,
}
for _, opt := range opts {
opt(ep)
}
return ep
}

func (ep *ExoscaleProvider) getZones() (map[int64]string, error) {
dom, err := ep.client.GetDomains()
if err != nil {
return nil, err
}

zones := map[int64]string{}
for _, d := range dom {
zones[d.ID] = d.Name
}
return zones, nil
}

// ApplyChanges simply modifies DNS via exoscale API
func (ep *ExoscaleProvider) ApplyChanges(changes *plan.Changes) error {
ep.OnApplyChanges(changes)

zones, err := ep.getZones()
if err != nil {
return err
}

for _, epoint := range changes.Create {
if ep.domain.Match(epoint.DNSName) {
if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 {
rec := egoscale.DNSRecord{
Name: name,
RecordType: epoint.RecordType,
TTL: int(epoint.RecordTTL),
Content: epoint.Targets[0],
}
_, err := ep.client.CreateRecord(zones[zoneID], rec)
if err != nil {
return err
}
}
}
}
for _, epoint := range changes.UpdateNew {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to understand it a bit better, why do you ignore the updates? I just only see that you're looping over update changes even if you would use a logging level which is not debug?

Copy link
Contributor Author

@FaKod FaKod Jul 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit that for my use case it seems to be sufficient to only support create and delete. Is update mandatory?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just only asking because I never saw that someone wouldn't want to update their records. Could there people which might need it?

If you don't use it at all I'm not seeing any point looping over updates which doesn't do anything execept logging.

log.Debugf("UPDATE-NEW (ignored) for epoint: %+v", epoint)
}
for _, epoint := range changes.UpdateOld {
log.Debugf("UPDATE-OLD (ignored) for epoint: %+v", epoint)
}
for _, epoint := range changes.Delete {
if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 {
records, err := ep.client.GetRecords(zones[zoneID])
if err != nil {
return err
}

for _, r := range records {
if r.Name == name {
if err := ep.client.DeleteRecord(zones[zoneID], r.ID); err != nil {
return err
}
break
}
}
}
}

return nil
}

// Records returns the list of endpoints
func (ep *ExoscaleProvider) Records() ([]*endpoint.Endpoint, error) {
endpoints := make([]*endpoint.Endpoint, 0)

dom, err := ep.client.GetDomains()
if err != nil {
return nil, err
}

for _, d := range dom {
record, err := ep.client.GetRecords(d.Name)
if err != nil {
return nil, err
}
for _, r := range record {
switch r.RecordType {
case "A", "AAAA", "CNAME", "TXT":
break
default:
continue
}
ep := endpoint.NewEndpointWithTTL(r.Name+"."+d.Name, r.RecordType, endpoint.TTL(r.TTL), r.Content)
endpoints = append(endpoints, ep)
}
}

log.Infof("called Records() with %d items", len(endpoints))
return endpoints, nil
}

// ExoscaleWithDomain modifies the domain on which dns zones are filtered
func ExoscaleWithDomain(domainFilter DomainFilter) ExoscaleOption {
return func(p *ExoscaleProvider) {
p.domain = domainFilter
}
}

// ExoscaleWithLogging injects logging when ApplyChanges is called
func ExoscaleWithLogging() ExoscaleOption {
return func(p *ExoscaleProvider) {
p.OnApplyChanges = func(changes *plan.Changes) {
for _, v := range changes.Create {
log.Infof("CREATE: %v", v)
}
for _, v := range changes.UpdateOld {
log.Infof("UPDATE (old): %v", v)
}
for _, v := range changes.UpdateNew {
log.Infof("UPDATE (new): %v", v)
}
for _, v := range changes.Delete {
log.Infof("DELETE: %v", v)
}
}
}
}

type zoneFilter struct {
domain string
}

// Zones filters map[zoneID]zoneName for names having f.domain as suffix
func (f *zoneFilter) Zones(zones map[int64]string) map[int64]string {
result := map[int64]string{}
for zoneID, zoneName := range zones {
if strings.HasSuffix(zoneName, f.domain) {
result[zoneID] = zoneName
}
}
return result
}

// EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName
// returns 0 if no match found
func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64]string) (zoneID int64, name string) {
var matchZoneID int64
var matchZoneName string
for zoneID, zoneName := range zones {
if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) {
matchZoneName = zoneName
matchZoneID = zoneID
name = strings.TrimSuffix(endpoint.DNSName, "."+zoneName)
}
}
return matchZoneID, name
}
Loading