external-dns/provider/ibmcloud/ibmcloud.go
2023-09-03 11:23:03 -07:00

1008 lines
37 KiB
Go

/*
Copyright 2022 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 ibmcloud
import (
"context"
"fmt"
"os"
"reflect"
"strconv"
"strings"
"github.com/IBM-Cloud/ibm-cloud-cli-sdk/bluemix/crn"
"github.com/IBM/go-sdk-core/v5/core"
"github.com/IBM/networking-go-sdk/dnsrecordsv1"
"github.com/IBM/networking-go-sdk/dnssvcsv1"
"github.com/IBM/networking-go-sdk/zonesv1"
"gopkg.in/yaml.v2"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/source"
)
var proxyTypeNotSupported = map[string]bool{
"LOC": true,
"MX": true,
"NS": true,
"SPF": true,
"TXT": true,
"SRV": true,
}
var privateTypeSupported = map[string]bool{
"A": true,
"CNAME": true,
"TXT": true,
}
const (
// recordCreate is a ChangeAction enum value
recordCreate = "CREATE"
// recordDelete is a ChangeAction enum value
recordDelete = "DELETE"
// recordUpdate is a ChangeAction enum value
recordUpdate = "UPDATE"
// defaultPublicRecordTTL 1 = automatic
defaultPublicRecordTTL = 1
proxyFilter = "ibmcloud-proxied"
vpcFilter = "ibmcloud-vpc"
zoneStatePendingNetwork = "PENDING_NETWORK_ADD"
zoneStateActive = "ACTIVE"
)
// Source shadow the interface source.Source. used primarily for unit testing.
type Source interface {
Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
AddEventHandler(context.Context, func())
}
// ibmcloudClient is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
type ibmcloudClient interface {
ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error)
CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error)
DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error)
UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error)
ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error)
GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error)
CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error)
ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error)
CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error)
DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error)
UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error)
NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error)
NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error)
NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error)
NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error)
NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error)
NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error)
}
type ibmcloudService struct {
publicZonesService *zonesv1.ZonesV1
publicRecordsService *dnsrecordsv1.DnsRecordsV1
privateDNSService *dnssvcsv1.DnsSvcsV1
}
func (i ibmcloudService) ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) {
return i.publicRecordsService.ListAllDnsRecordsWithContext(ctx, listAllDNSRecordsOptions)
}
func (i ibmcloudService) CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) {
return i.publicRecordsService.CreateDnsRecordWithContext(ctx, createDNSRecordOptions)
}
func (i ibmcloudService) DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) {
return i.publicRecordsService.DeleteDnsRecordWithContext(ctx, deleteDNSRecordOptions)
}
func (i ibmcloudService) UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) {
return i.publicRecordsService.UpdateDnsRecordWithContext(ctx, updateDNSRecordOptions)
}
func (i ibmcloudService) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) {
return i.privateDNSService.ListDnszonesWithContext(ctx, listDnszonesOptions)
}
func (i ibmcloudService) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) {
return i.privateDNSService.GetDnszoneWithContext(ctx, getDnszoneOptions)
}
func (i ibmcloudService) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) {
return i.privateDNSService.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions)
}
func (i ibmcloudService) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) {
return i.privateDNSService.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions)
}
func (i ibmcloudService) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) {
return i.privateDNSService.CreateResourceRecordWithContext(ctx, createResourceRecordOptions)
}
func (i ibmcloudService) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) {
return i.privateDNSService.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions)
}
func (i ibmcloudService) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) {
return i.privateDNSService.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions)
}
func (i ibmcloudService) NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) {
return i.privateDNSService.NewResourceRecordInputRdataRdataARecord(ip)
}
func (i ibmcloudService) NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) {
return i.privateDNSService.NewResourceRecordInputRdataRdataCnameRecord(cname)
}
func (i ibmcloudService) NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) {
return i.privateDNSService.NewResourceRecordInputRdataRdataTxtRecord(text)
}
func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) {
return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataARecord(ip)
}
func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) {
return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataCnameRecord(cname)
}
func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) {
return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataTxtRecord(text)
}
// IBMCloudProvider is an implementation of Provider for IBM Cloud DNS.
type IBMCloudProvider struct {
provider.BaseProvider
source Source
Client ibmcloudClient
// only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
instanceID string
privateZone bool
proxiedByDefault bool
DryRun bool
}
type ibmcloudConfig struct {
Endpoint string `json:"endpoint" yaml:"endpoint"`
APIKey string `json:"apiKey" yaml:"apiKey"`
CRN string `json:"instanceCrn" yaml:"instanceCrn"`
IAMURL string `json:"iamUrl" yaml:"iamUrl"`
InstanceID string `json:"-" yaml:"-"`
}
// ibmcloudChange differentiates between ChangActions
type ibmcloudChange struct {
Action string
PublicResourceRecord dnsrecordsv1.DnsrecordDetails
PrivateResourceRecord dnssvcsv1.ResourceRecord
}
func getConfig(configFile string) (*ibmcloudConfig, error) {
contents, err := os.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err)
}
cfg := &ibmcloudConfig{}
err = yaml.Unmarshal(contents, &cfg)
if err != nil {
return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err)
}
return cfg, nil
}
func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter) (ibmcloudService, bool, error) {
var service ibmcloudService
isPrivate := false
log.Debugf("filters: %v, %v", domainFilter.Filters, zoneIDFilter.ZoneIDs)
if (len(domainFilter.Filters) == 0 || domainFilter.Filters[0] == "") && zoneIDFilter.ZoneIDs[0] == "" {
return service, isPrivate, fmt.Errorf("at lease one of filters: 'domain-filter', 'zone-id-filter' needed")
}
crn, err := crn.Parse(c.CRN)
if err != nil {
return service, isPrivate, err
}
log.Infof("IBM Cloud Service: %s", crn.ServiceName)
c.InstanceID = crn.ServiceInstance
switch {
case strings.Contains(crn.ServiceName, "internet-svcs"):
if len(domainFilter.Filters) > 1 || len(zoneIDFilter.ZoneIDs) > 1 {
return service, isPrivate, fmt.Errorf("for public zone, only one domain id filter or domain name filter allowed")
}
var zoneID string
// Public DNS service
service.publicZonesService, err = zonesv1.NewZonesV1(&zonesv1.ZonesV1Options{
Authenticator: authenticator,
Crn: core.StringPtr(c.CRN),
})
if err != nil {
return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public zones client: %v", err)
}
if c.Endpoint != "" {
service.publicZonesService.SetServiceURL(c.Endpoint)
}
zonesResp, _, err := service.publicZonesService.ListZones(&zonesv1.ListZonesOptions{})
if err != nil {
return service, isPrivate, fmt.Errorf("failed to list ibmcloud public zones: %v", err)
}
for _, zone := range zonesResp.Result {
log.Debugf("zoneName: %s, zoneID: %s", *zone.Name, *zone.ID)
if len(domainFilter.Filters) > 0 && domainFilter.Filters[0] != "" && domainFilter.Match(*zone.Name) {
log.Debugf("zone %s found.", *zone.ID)
zoneID = *zone.ID
break
}
if len(zoneIDFilter.ZoneIDs[0]) != 0 && zoneIDFilter.Match(*zone.ID) {
log.Debugf("zone %s found.", *zone.ID)
zoneID = *zone.ID
break
}
}
if len(zoneID) == 0 {
return service, isPrivate, fmt.Errorf("no matched zone found")
}
service.publicRecordsService, err = dnsrecordsv1.NewDnsRecordsV1(&dnsrecordsv1.DnsRecordsV1Options{
Authenticator: authenticator,
Crn: core.StringPtr(c.CRN),
ZoneIdentifier: core.StringPtr(zoneID),
})
if err != nil {
return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public records client: %v", err)
}
if c.Endpoint != "" {
service.publicRecordsService.SetServiceURL(c.Endpoint)
}
case strings.Contains(crn.ServiceName, "dns-svcs"):
isPrivate = true
// Private DNS service
service.privateDNSService, err = dnssvcsv1.NewDnsSvcsV1(&dnssvcsv1.DnsSvcsV1Options{
Authenticator: authenticator,
})
if err != nil {
return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud private records client: %v", err)
}
if c.Endpoint != "" {
service.privateDNSService.SetServiceURL(c.Endpoint)
}
default:
return service, isPrivate, fmt.Errorf("IBM Cloud instance crn is not provided or invalid dns crn : %s", c.CRN)
}
return service, isPrivate, nil
}
// NewIBMCloudProvider creates a new IBMCloud provider.
//
// Returns the provider or an error if a provider could not be created.
func NewIBMCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, source source.Source, proxiedByDefault bool, dryRun bool) (*IBMCloudProvider, error) {
cfg, err := getConfig(configFile)
if err != nil {
return nil, err
}
authenticator := &core.IamAuthenticator{
ApiKey: cfg.APIKey,
}
if cfg.IAMURL != "" {
authenticator = &core.IamAuthenticator{
ApiKey: cfg.APIKey,
URL: cfg.IAMURL,
}
}
client, isPrivate, err := cfg.Validate(authenticator, domainFilter, zoneIDFilter)
if err != nil {
return nil, err
}
provider := &IBMCloudProvider{
Client: client,
source: source,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
instanceID: cfg.InstanceID,
privateZone: isPrivate,
proxiedByDefault: proxiedByDefault,
DryRun: dryRun,
}
return provider, nil
}
// Records gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *IBMCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
if p.privateZone {
endpoints, err = p.privateRecords(ctx)
} else {
endpoints, err = p.publicRecords(ctx)
}
return endpoints, err
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *IBMCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugln("applying change...")
ibmcloudChanges := []*ibmcloudChange{}
for _, endpoint := range changes.Create {
for _, target := range endpoint.Targets {
ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, endpoint, target))
}
}
for i, desired := range changes.UpdateNew {
current := changes.UpdateOld[i]
add, remove, leave := provider.Difference(current.Targets, desired.Targets)
log.Debugf("add: %v, remove: %v, leave: %v", add, remove, leave)
for _, a := range add {
ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, desired, a))
}
for _, a := range leave {
ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordUpdate, desired, a))
}
for _, a := range remove {
ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, current, a))
}
}
for _, endpoint := range changes.Delete {
for _, target := range endpoint.Targets {
ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, endpoint, target))
}
}
return p.submitChanges(ctx, ibmcloudChanges)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (p *IBMCloudProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
adjustedEndpoints := []*endpoint.Endpoint{}
for _, e := range endpoints {
log.Debugf("adjusting endpont: %v", *e)
proxied := shouldBeProxied(e, p.proxiedByDefault)
if proxied {
e.RecordTTL = 0
}
e.SetProviderSpecificProperty(proxyFilter, strconv.FormatBool(proxied))
adjustedEndpoints = append(adjustedEndpoints, e)
}
return adjustedEndpoints, nil
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *IBMCloudProvider) submitChanges(ctx context.Context, changes []*ibmcloudChange) error {
// return early if there is nothing to change
if len(changes) == 0 {
return nil
}
log.Debugln("submmiting change...")
if p.privateZone {
return p.submitChangesForPrivateDNS(ctx, changes)
}
return p.submitChangesForPublicDNS(ctx, changes)
}
// submitChangesForPublicDNS takes a zone and a collection of Changes and sends them as a single transaction on public dns.
func (p *IBMCloudProvider) submitChangesForPublicDNS(ctx context.Context, changes []*ibmcloudChange) error {
records, err := p.listAllPublicRecords(ctx)
if err != nil {
return err
}
for _, change := range changes {
logFields := log.Fields{
"record": *change.PublicResourceRecord.Name,
"type": *change.PublicResourceRecord.Type,
"ttl": *change.PublicResourceRecord.TTL,
"action": change.Action,
}
if p.DryRun {
continue
}
log.WithFields(logFields).Info("Changing record.")
if change.Action == recordUpdate {
recordID := p.getPublicRecordID(records, change.PublicResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name)
continue
}
p.updateRecord(ctx, "", recordID, change)
} else if change.Action == recordDelete {
recordID := p.getPublicRecordID(records, change.PublicResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name)
continue
}
p.deleteRecord(ctx, "", recordID)
} else if change.Action == recordCreate {
p.createRecord(ctx, "", change)
}
}
return nil
}
// submitChangesForPrivateDNS takes a zone and a collection of Changes and sends them as a single transaction on private dns.
func (p *IBMCloudProvider) submitChangesForPrivateDNS(ctx context.Context, changes []*ibmcloudChange) error {
zones, err := p.privateZones(ctx)
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByPrivateZone := p.changesByPrivateZone(ctx, zones, changes)
for zoneID, changes := range changesByPrivateZone {
records, err := p.listAllPrivateRecords(ctx, zoneID)
if err != nil {
return err
}
for _, change := range changes {
logFields := log.Fields{
"record": *change.PrivateResourceRecord.Name,
"type": *change.PrivateResourceRecord.Type,
"ttl": *change.PrivateResourceRecord.TTL,
"action": change.Action,
}
log.WithFields(logFields).Info("Changing record.")
if p.DryRun {
continue
}
if change.Action == recordUpdate {
recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord)
continue
}
p.updateRecord(ctx, zoneID, recordID, change)
} else if change.Action == recordDelete {
recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord)
if recordID == "" {
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord)
continue
}
p.deleteRecord(ctx, zoneID, recordID)
} else if change.Action == recordCreate {
p.createRecord(ctx, zoneID, change)
}
}
}
return nil
}
// privateZones return zones in private dns
func (p *IBMCloudProvider) privateZones(ctx context.Context) ([]dnssvcsv1.Dnszone, error) {
result := []dnssvcsv1.Dnszone{}
// if there is a zoneIDfilter configured
// && if the filter isn't just a blank string (used in tests)
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
log.Debugf("looking up zone %s", zoneID)
detailResponse, _, err := p.Client.GetDnszoneWithContext(ctx, &dnssvcsv1.GetDnszoneOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
})
if err != nil {
log.Errorf("zone %s lookup failed, %v", zoneID, err)
continue
}
log.WithFields(log.Fields{
"zoneName": *detailResponse.Name,
"zoneID": *detailResponse.ID,
}).Debugln("adding zone for consideration")
result = append(result, *detailResponse)
}
return result, nil
}
log.Debugln("no zoneIDFilter configured, looking at all zones")
zonesResponse, _, err := p.Client.ListDnszonesWithContext(ctx, &dnssvcsv1.ListDnszonesOptions{
InstanceID: core.StringPtr(p.instanceID),
})
if err != nil {
return nil, err
}
for _, zone := range zonesResponse.Dnszones {
if !p.domainFilter.Match(*zone.Name) {
log.Debugf("zone %s not in domain filter", *zone.Name)
continue
}
result = append(result, zone)
}
return result, nil
}
// activePrivateZone active zone with new records add if not active
func (p *IBMCloudProvider) activePrivateZone(ctx context.Context, zoneID, vpc string) {
permittedNetworkVpc := &dnssvcsv1.PermittedNetworkVpc{
VpcCrn: core.StringPtr(vpc),
}
createPermittedNetworkOptions := &dnssvcsv1.CreatePermittedNetworkOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
PermittedNetwork: permittedNetworkVpc,
Type: core.StringPtr("vpc"),
}
_, _, err := p.Client.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions)
if err != nil {
log.Errorf("failed to active zone %s in VPC %s with error: %v", zoneID, vpc, err)
}
}
// changesByPrivateZone separates a multi-zone change into a single change per zone.
func (p *IBMCloudProvider) changesByPrivateZone(ctx context.Context, zones []dnssvcsv1.Dnszone, changeSet []*ibmcloudChange) map[string][]*ibmcloudChange {
changes := make(map[string][]*ibmcloudChange)
zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(*z.ID, *z.Name)
changes[*z.ID] = []*ibmcloudChange{}
}
for _, c := range changeSet {
zoneID, _ := zoneNameIDMapper.FindZone(*c.PrivateResourceRecord.Name)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", *c.PrivateResourceRecord.Name)
continue
}
changes[zoneID] = append(changes[zoneID], c)
}
return changes
}
func (p *IBMCloudProvider) publicRecords(ctx context.Context) ([]*endpoint.Endpoint, error) {
log.Debugf("Listing records on public zone")
dnsRecords, err := p.listAllPublicRecords(ctx)
if err != nil {
return nil, err
}
return p.groupPublicRecords(dnsRecords), nil
}
func (p *IBMCloudProvider) listAllPublicRecords(ctx context.Context) ([]dnsrecordsv1.DnsrecordDetails, error) {
var dnsRecords []dnsrecordsv1.DnsrecordDetails
page := 1
GETRECORDS:
listAllDNSRecordsOptions := &dnsrecordsv1.ListAllDnsRecordsOptions{
Page: core.Int64Ptr(int64(page)),
}
records, _, err := p.Client.ListAllDDNSRecordsWithContext(ctx, listAllDNSRecordsOptions)
if err != nil {
return dnsRecords, err
}
dnsRecords = append(dnsRecords, records.Result...)
// Loop if more records exist
if *records.ResultInfo.TotalCount > int64(page*100) {
page = page + 1
log.Debugf("More than one pages records found, page: %d", page)
goto GETRECORDS
}
return dnsRecords, nil
}
func (p *IBMCloudProvider) groupPublicRecords(records []dnsrecordsv1.DnsrecordDetails) []*endpoint.Endpoint {
endpoints := []*endpoint.Endpoint{}
// group supported records by name and type
groups := map[string][]dnsrecordsv1.DnsrecordDetails{}
for _, r := range records {
if !provider.SupportedRecordType(*r.Type) {
continue
}
groupBy := *r.Name + *r.Type
if _, ok := groups[groupBy]; !ok {
groups[groupBy] = []dnsrecordsv1.DnsrecordDetails{}
}
groups[groupBy] = append(groups[groupBy], r)
}
// create single endpoint with all the targets for each name/type
for _, records := range groups {
targets := make([]string, len(records))
for i, record := range records {
targets[i] = *record.Content
}
ep := endpoint.NewEndpointWithTTL(
*records[0].Name,
*records[0].Type,
endpoint.TTL(*records[0].TTL),
targets...).WithProviderSpecific(proxyFilter, strconv.FormatBool(*records[0].Proxied))
log.Debugf(
"Found %s record for '%s' with target '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
)
endpoints = append(endpoints, ep)
}
return endpoints
}
func (p *IBMCloudProvider) privateRecords(ctx context.Context) ([]*endpoint.Endpoint, error) {
log.Debugf("Listing records on private zone")
var vpc string
zones, err := p.privateZones(ctx)
if err != nil {
return nil, err
}
sources, err := p.source.Endpoints(ctx)
if err != nil {
return nil, err
}
// Filter VPC annoation for private zone active
for _, source := range sources {
vpc = checkVPCAnnotation(source)
if len(vpc) > 0 {
log.Debugf("VPC found: %s", vpc)
break
}
}
endpoints := []*endpoint.Endpoint{}
for _, zone := range zones {
if len(vpc) > 0 && *zone.State == zoneStatePendingNetwork {
log.Debugf("active zone: %s", *zone.ID)
p.activePrivateZone(ctx, *zone.ID, vpc)
}
dnsRecords, err := p.listAllPrivateRecords(ctx, *zone.ID)
if err != nil {
return nil, err
}
endpoints = append(endpoints, p.groupPrivateRecords(dnsRecords)...)
}
return endpoints, nil
}
func (p *IBMCloudProvider) listAllPrivateRecords(ctx context.Context, zoneID string) ([]dnssvcsv1.ResourceRecord, error) {
var dnsRecords []dnssvcsv1.ResourceRecord
offset := 0
GETRECORDS:
listResourceRecordsOptions := &dnssvcsv1.ListResourceRecordsOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
Offset: core.Int64Ptr(int64(offset)),
}
records, _, err := p.Client.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions)
if err != nil {
return dnsRecords, err
}
oRecords := records.ResourceRecords
dnsRecords = append(dnsRecords, oRecords...)
// Loop if more records exist
if int64(offset+1) < *records.TotalCount && int64(offset+200) < *records.TotalCount {
offset = offset + 200
log.Debugf("More than one pages records found, page: %d", offset/200+1)
goto GETRECORDS
}
return dnsRecords, nil
}
func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecord) []*endpoint.Endpoint {
endpoints := []*endpoint.Endpoint{}
// group supported records by name and type
groups := map[string][]dnssvcsv1.ResourceRecord{}
for _, r := range records {
if !provider.SupportedRecordType(*r.Type) || !privateTypeSupported[*r.Type] {
continue
}
rname := *r.Name
rtype := *r.Type
groupBy := rname + rtype
if _, ok := groups[groupBy]; !ok {
groups[groupBy] = []dnssvcsv1.ResourceRecord{}
}
groups[groupBy] = append(groups[groupBy], r)
}
// create single endpoint with all the targets for each name/type
for _, records := range groups {
targets := make([]string, len(records))
for i, record := range records {
data := record.Rdata
log.Debugf("record data: %v", data)
switch *record.Type {
case "A":
if !isNil(data["ip"]) {
targets[i] = data["ip"].(string)
}
case "CNAME":
if !isNil(data["cname"]) {
targets[i] = data["cname"].(string)
}
case "TXT":
if !isNil(data["text"]) {
targets[i] = data["text"].(string)
}
log.Debugf("text record data: %v", targets[i])
}
}
ep := endpoint.NewEndpointWithTTL(
*records[0].Name,
*records[0].Type,
endpoint.TTL(*records[0].TTL), targets...)
log.Debugf(
"Found %s record for '%s' with target '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
)
endpoints = append(endpoints, ep)
}
return endpoints
}
func (p *IBMCloudProvider) getPublicRecordID(records []dnsrecordsv1.DnsrecordDetails, record dnsrecordsv1.DnsrecordDetails) string {
for _, zoneRecord := range records {
if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type && *zoneRecord.Content == *record.Content {
return *zoneRecord.ID
}
}
return ""
}
func (p *IBMCloudProvider) getPrivateRecordID(records []dnssvcsv1.ResourceRecord, record dnssvcsv1.ResourceRecord) string {
for _, zoneRecord := range records {
if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type {
return *zoneRecord.ID
}
}
return ""
}
func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.Endpoint, target string) *ibmcloudChange {
ttl := defaultPublicRecordTTL
proxied := shouldBeProxied(endpoint, p.proxiedByDefault)
if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL)
}
if p.privateZone {
rData := make(map[string]interface{})
switch endpoint.RecordType {
case "A":
rData[dnssvcsv1.CreateResourceRecordOptions_Type_A] = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{
Ip: core.StringPtr(target),
}
case "CNAME":
rData[dnssvcsv1.CreateResourceRecordOptions_Type_Cname] = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{
Cname: core.StringPtr(target),
}
case "TXT":
rData[dnssvcsv1.CreateResourceRecordOptions_Type_Txt] = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{
Text: core.StringPtr(target),
}
}
return &ibmcloudChange{
Action: action,
PrivateResourceRecord: dnssvcsv1.ResourceRecord{
Name: core.StringPtr(endpoint.DNSName),
TTL: core.Int64Ptr(int64(ttl)),
Type: core.StringPtr(endpoint.RecordType),
Rdata: rData,
},
}
}
return &ibmcloudChange{
Action: action,
PublicResourceRecord: dnsrecordsv1.DnsrecordDetails{
Name: core.StringPtr(endpoint.DNSName),
TTL: core.Int64Ptr(int64(ttl)),
Proxied: core.BoolPtr(proxied),
Type: core.StringPtr(endpoint.RecordType),
Content: core.StringPtr(target),
},
}
}
func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, change *ibmcloudChange) {
if p.privateZone {
createResourceRecordOptions := &dnssvcsv1.CreateResourceRecordOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
Name: change.PrivateResourceRecord.Name,
Type: change.PrivateResourceRecord.Type,
TTL: change.PrivateResourceRecord.TTL,
}
switch *change.PrivateResourceRecord.Type {
case "A":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip)
createResourceRecordOptions.SetRdata(aData)
case "CNAME":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname)
createResourceRecordOptions.SetRdata(cnameData)
case "TXT":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text)
createResourceRecordOptions.SetRdata(txtData)
}
_, _, err := p.Client.CreateResourceRecordWithContext(ctx, createResourceRecordOptions)
if err != nil {
log.Errorf("failed to create %s type record named %s: %v", *change.PrivateResourceRecord.Type, *change.PrivateResourceRecord.Name, err)
}
} else {
createDNSRecordOptions := &dnsrecordsv1.CreateDnsRecordOptions{
Name: change.PublicResourceRecord.Name,
Type: change.PublicResourceRecord.Type,
TTL: change.PublicResourceRecord.TTL,
Content: change.PublicResourceRecord.Content,
}
_, _, err := p.Client.CreateDNSRecordWithContext(ctx, createDNSRecordOptions)
if err != nil {
log.Errorf("failed to create %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err)
}
}
}
func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID string, change *ibmcloudChange) {
if p.privateZone {
updateResourceRecordOptions := &dnssvcsv1.UpdateResourceRecordOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
RecordID: core.StringPtr(recordID),
Name: change.PrivateResourceRecord.Name,
TTL: change.PrivateResourceRecord.TTL,
}
switch *change.PrivateResourceRecord.Type {
case "A":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip)
updateResourceRecordOptions.SetRdata(aData)
case "CNAME":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname)
updateResourceRecordOptions.SetRdata(cnameData)
case "TXT":
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text)
updateResourceRecordOptions.SetRdata(txtData)
}
_, _, err := p.Client.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions)
if err != nil {
log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err)
}
} else {
updateDNSRecordOptions := &dnsrecordsv1.UpdateDnsRecordOptions{
DnsrecordIdentifier: &recordID,
Name: change.PublicResourceRecord.Name,
Type: change.PublicResourceRecord.Type,
TTL: change.PublicResourceRecord.TTL,
Content: change.PublicResourceRecord.Content,
Proxied: change.PublicResourceRecord.Proxied,
}
_, _, err := p.Client.UpdateDNSRecordWithContext(ctx, updateDNSRecordOptions)
if err != nil {
log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err)
}
}
}
func (p *IBMCloudProvider) deleteRecord(ctx context.Context, zoneID, recordID string) {
if p.privateZone {
deleteResourceRecordOptions := &dnssvcsv1.DeleteResourceRecordOptions{
InstanceID: core.StringPtr(p.instanceID),
DnszoneID: core.StringPtr(zoneID),
RecordID: core.StringPtr(recordID),
}
_, err := p.Client.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions)
if err != nil {
log.Errorf("failed to delete record %s: %v", recordID, err)
}
} else {
deleteDNSRecordOptions := &dnsrecordsv1.DeleteDnsRecordOptions{
DnsrecordIdentifier: &recordID,
}
_, _, err := p.Client.DeleteDNSRecordWithContext(ctx, deleteDNSRecordOptions)
if err != nil {
log.Errorf("failed to delete record %s: %v", recordID, err)
}
}
}
func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool {
proxied := proxiedByDefault
for _, v := range endpoint.ProviderSpecific {
if v.Name == proxyFilter {
b, err := strconv.ParseBool(v.Value)
if err != nil {
log.Errorf("Failed to parse annotation [%s]: %v", proxyFilter, err)
} else {
proxied = b
}
break
}
}
if proxyTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") {
proxied = false
}
return proxied
}
func checkVPCAnnotation(endpoint *endpoint.Endpoint) string {
var vpc string
for _, v := range endpoint.ProviderSpecific {
if v.Name == vpcFilter {
vpcCrn, err := crn.Parse(v.Value)
if vpcCrn.ResourceType != "vpc" || err != nil {
log.Errorf("Failed to parse vpc [%s]: %v", v.Value, err)
} else {
vpc = v.Value
}
break
}
}
return vpc
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}