mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
341 lines
9.3 KiB
Go
341 lines
9.3 KiB
Go
/*
|
|
Copyright 2020 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 ovh
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/golang/sync/errgroup"
|
|
"github.com/ovh/go-ovh/ovh"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"sigs.k8s.io/external-dns/endpoint"
|
|
"sigs.k8s.io/external-dns/plan"
|
|
"sigs.k8s.io/external-dns/provider"
|
|
)
|
|
|
|
const (
|
|
ovhDefaultTTL = 0
|
|
ovhCreate = iota
|
|
ovhDelete
|
|
)
|
|
|
|
var (
|
|
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
|
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
|
// ErrNoDryRun No dry run support for the moment
|
|
ErrNoDryRun = errors.New("dry run not supported")
|
|
)
|
|
|
|
// OVHProvider is an implementation of Provider for OVH DNS.
|
|
type OVHProvider struct {
|
|
client ovhClient
|
|
|
|
domainFilter endpoint.DomainFilter
|
|
DryRun bool
|
|
}
|
|
|
|
type ovhClient interface {
|
|
Post(string, interface{}, interface{}) error
|
|
Get(string, interface{}) error
|
|
Delete(string, interface{}) error
|
|
}
|
|
|
|
type ovhRecordFields struct {
|
|
FieldType string `json:"fieldType"`
|
|
SubDomain string `json:"subDomain"`
|
|
TTL int64 `json:"ttl"`
|
|
Target string `json:"target"`
|
|
}
|
|
|
|
type ovhRecord struct {
|
|
ovhRecordFields
|
|
ID uint64 `json:"id"`
|
|
Zone string `json:"zone"`
|
|
}
|
|
|
|
type ovhChange struct {
|
|
ovhRecord
|
|
Action int
|
|
}
|
|
|
|
// NewOVHProvider initializes a new OVH DNS based Provider.
|
|
func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, endpoint string, dryRun bool) (*OVHProvider, error) {
|
|
client, err := ovh.NewEndpointClient(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: Add Dry Run support
|
|
if dryRun {
|
|
return nil, ErrNoDryRun
|
|
}
|
|
return &OVHProvider{
|
|
client: client,
|
|
domainFilter: domainFilter,
|
|
DryRun: dryRun,
|
|
}, nil
|
|
}
|
|
|
|
// Records returns the list of records in all relevant zones.
|
|
func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
|
|
|
_, records, err := p.zonesRecords(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoints := ovhGroupByNameAndType(records)
|
|
log.Infof("OVH: %d endpoints have been found", len(endpoints))
|
|
return endpoints, nil
|
|
}
|
|
|
|
// ApplyChanges applies a given set of changes in a given zone.
|
|
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
|
zones, records, err := p.zonesRecords(ctx)
|
|
zonesChangeUniques := map[string]bool{}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allChanges := make([]ovhChange, 0, countTargets(changes.Create, changes.UpdateNew, changes.UpdateOld, changes.Delete))
|
|
|
|
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.Create, zones, records)...)
|
|
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.UpdateNew, zones, records)...)
|
|
|
|
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.UpdateOld, zones, records)...)
|
|
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.Delete, zones, records)...)
|
|
|
|
log.Infof("OVH: %d changes will be done", len(allChanges))
|
|
|
|
eg, _ := errgroup.WithContext(ctx)
|
|
for _, change := range allChanges {
|
|
change := change
|
|
zonesChangeUniques[change.Zone] = true
|
|
eg.Go(func() error { return p.change(change) })
|
|
}
|
|
if err := eg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infof("OVH: %d zones will be refreshed", len(zonesChangeUniques))
|
|
|
|
eg, _ = errgroup.WithContext(ctx)
|
|
for zone := range zonesChangeUniques {
|
|
zone := zone
|
|
eg.Go(func() error { return p.refresh(zone) })
|
|
}
|
|
if err := eg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *OVHProvider) refresh(zone string) error {
|
|
log.Debugf("OVH: Refresh %s zone", zone)
|
|
return p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
|
|
}
|
|
|
|
func (p *OVHProvider) change(change ovhChange) error {
|
|
switch change.Action {
|
|
case ovhCreate:
|
|
log.Debugf("OVH: Add an entry to %s", change.String())
|
|
return p.client.Post(fmt.Sprintf("/domain/zone/%s/record", change.Zone), change.ovhRecordFields, nil)
|
|
case ovhDelete:
|
|
if change.ID == 0 {
|
|
return ErrRecordToMutateNotFound
|
|
}
|
|
log.Debugf("OVH: Delete an entry to %s", change.String())
|
|
return p.client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", change.Zone, change.ID), nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord, error) {
|
|
var allRecords []ovhRecord
|
|
zones, err := p.zones()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
chRecords := make(chan []ovhRecord, len(zones))
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
for _, zone := range zones {
|
|
zone := zone
|
|
eg.Go(func() error { return p.records(&ctx, &zone, chRecords) })
|
|
}
|
|
if err := eg.Wait(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
close(chRecords)
|
|
for records := range chRecords {
|
|
allRecords = append(allRecords, records...)
|
|
}
|
|
return zones, allRecords, nil
|
|
}
|
|
|
|
func (p *OVHProvider) zones() ([]string, error) {
|
|
zones := []string{}
|
|
filteredZones := []string{}
|
|
|
|
if err := p.client.Get("/domain/zone", &zones); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, zoneName := range zones {
|
|
if p.domainFilter.Match(zoneName) {
|
|
filteredZones = append(filteredZones, zoneName)
|
|
}
|
|
}
|
|
log.Infof("OVH: %d zones found", len(filteredZones))
|
|
return filteredZones, nil
|
|
}
|
|
|
|
func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<- []ovhRecord) error {
|
|
var recordsIds []uint64
|
|
ovhRecords := make([]ovhRecord, len(recordsIds))
|
|
eg, _ := errgroup.WithContext(*ctx)
|
|
|
|
log.Debugf("OVH: Getting records for %s", *zone)
|
|
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record", *zone), &recordsIds); err != nil {
|
|
return err
|
|
}
|
|
chRecords := make(chan ovhRecord, len(recordsIds))
|
|
for _, id := range recordsIds {
|
|
id := id
|
|
eg.Go(func() error { return p.record(zone, id, chRecords) })
|
|
}
|
|
if err := eg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
close(chRecords)
|
|
for record := range chRecords {
|
|
ovhRecords = append(ovhRecords, record)
|
|
}
|
|
records <- ovhRecords
|
|
return nil
|
|
}
|
|
|
|
func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord) error {
|
|
record := ovhRecord{}
|
|
|
|
log.Debugf("OVH: Getting record %d for %s", id, *zone)
|
|
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
|
|
return err
|
|
}
|
|
if provider.SupportedRecordType(record.FieldType) {
|
|
log.Debugf("OVH: Record %d for %s is %+v", id, *zone, record)
|
|
records <- record
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ovhGroupByNameAndType(records []ovhRecord) []*endpoint.Endpoint {
|
|
endpoints := []*endpoint.Endpoint{}
|
|
|
|
// group supported records by name and type
|
|
groups := map[string][]ovhRecord{}
|
|
|
|
for _, r := range records {
|
|
groupBy := r.Zone + r.SubDomain + r.FieldType
|
|
if _, ok := groups[groupBy]; !ok {
|
|
groups[groupBy] = []ovhRecord{}
|
|
}
|
|
|
|
groups[groupBy] = append(groups[groupBy], r)
|
|
}
|
|
|
|
// create single endpoint with all the targets for each name/type
|
|
for _, records := range groups {
|
|
targets := []string{}
|
|
for _, record := range records {
|
|
targets = append(targets, record.Target)
|
|
}
|
|
endpoint := endpoint.NewEndpointWithTTL(
|
|
strings.TrimPrefix(records[0].SubDomain+"."+records[0].Zone, "."),
|
|
records[0].FieldType,
|
|
endpoint.TTL(records[0].TTL),
|
|
targets...,
|
|
)
|
|
endpoints = append(endpoints, endpoint)
|
|
}
|
|
|
|
return endpoints
|
|
}
|
|
|
|
func newOvhChange(action int, endpoints []*endpoint.Endpoint, zones []string, records []ovhRecord) []ovhChange {
|
|
zoneNameIDMapper := provider.ZoneIDName{}
|
|
ovhChanges := make([]ovhChange, 0, countTargets(endpoints))
|
|
for _, zone := range zones {
|
|
zoneNameIDMapper.Add(zone, zone)
|
|
}
|
|
|
|
for _, e := range endpoints {
|
|
zone, _ := zoneNameIDMapper.FindZone(e.DNSName)
|
|
if zone == "" {
|
|
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", e.DNSName)
|
|
continue
|
|
}
|
|
for _, target := range e.Targets {
|
|
if e.RecordType == endpoint.RecordTypeCNAME {
|
|
target = target + "."
|
|
}
|
|
change := ovhChange{
|
|
Action: action,
|
|
ovhRecord: ovhRecord{
|
|
Zone: zone,
|
|
ovhRecordFields: ovhRecordFields{
|
|
FieldType: e.RecordType,
|
|
SubDomain: strings.TrimSuffix(e.DNSName, "."+zone),
|
|
TTL: ovhDefaultTTL,
|
|
Target: target,
|
|
},
|
|
},
|
|
}
|
|
if e.RecordTTL.IsConfigured() {
|
|
change.TTL = int64(e.RecordTTL)
|
|
}
|
|
for _, record := range records {
|
|
if record.Zone == change.Zone && record.SubDomain == change.SubDomain && record.FieldType == change.FieldType && record.Target == change.Target {
|
|
change.ID = record.ID
|
|
}
|
|
}
|
|
ovhChanges = append(ovhChanges, change)
|
|
}
|
|
}
|
|
|
|
return ovhChanges
|
|
}
|
|
|
|
func countTargets(allEndpoints ...[]*endpoint.Endpoint) int {
|
|
count := 0
|
|
for _, endpoints := range allEndpoints {
|
|
for _, endpoint := range endpoints {
|
|
count += len(endpoint.Targets)
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (c *ovhChange) String() string {
|
|
if c.ID != 0 {
|
|
return fmt.Sprintf("%s zone (ID : %d) : %s %d IN %s %s", c.Zone, c.ID, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
|
}
|
|
return fmt.Sprintf("%s zone : %s %d IN %s %s", c.Zone, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
|
}
|