diff --git a/docs/tutorials/infoblox.md b/docs/tutorials/infoblox.md index 71a3cafc3..701e7b35e 100644 --- a/docs/tutorials/infoblox.md +++ b/docs/tutorials/infoblox.md @@ -78,6 +78,7 @@ spec: - --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is "443". - --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is "2.3.1" - --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification. + - --infoblox-create-ptr # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry. env: - name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10". diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 7d7be8e00..9a78ffc97 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -35,6 +35,8 @@ const ( RecordTypeSRV = "SRV" // RecordTypeNS is a RecordType enum value RecordTypeNS = "NS" + // RecordTypePTR is a RecordType enum value + RecordTypePTR = "PTR" ) // TTL is a structure defining the TTL of a DNS record diff --git a/main.go b/main.go index 5843a583f..3f3bc968e 100644 --- a/main.go +++ b/main.go @@ -244,6 +244,7 @@ func main() { MaxResults: cfg.InfobloxMaxResults, DryRun: cfg.DryRun, FQDNRexEx: cfg.InfobloxFQDNRegEx, + CreatePTR: cfg.InfobloxCreatePTR, }, ) case "dyn": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 89e053b86..43e8aa16f 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -109,6 +109,7 @@ type Config struct { InfobloxView string InfobloxMaxResults int InfobloxFQDNRegEx string + InfobloxCreatePTR bool DynCustomerName string DynUsername string DynPassword string `secure:"yes"` @@ -237,6 +238,7 @@ var defaultConfig = &Config{ InfobloxView: "", InfobloxMaxResults: 0, InfobloxFQDNRegEx: "", + InfobloxCreatePTR: false, OCIConfigFile: "/etc/kubernetes/oci.yaml", InMemoryZones: []string{}, OVHEndpoint: "ovh-eu", @@ -424,6 +426,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("infoblox-view", "DNS view (default: \"\")").Default(defaultConfig.InfobloxView).StringVar(&cfg.InfobloxView) app.Flag("infoblox-max-results", "Add _max_results as query parameter to the URL on all API requests. The default is 0 which means _max_results is not set and the default of the server is used.").Default(strconv.Itoa(defaultConfig.InfobloxMaxResults)).IntVar(&cfg.InfobloxMaxResults) app.Flag("infoblox-fqdn-regex", "Apply this regular expression as a filter for obtaining zone_auth objects. This is disabled by default.").Default(defaultConfig.InfobloxFQDNRegEx).StringVar(&cfg.InfobloxFQDNRegEx) + app.Flag("infoblox-create-ptr", "When using the Infoblox provider, create a ptr entry in addition to an entry").Default(strconv.FormatBool(defaultConfig.InfobloxCreatePTR)).BoolVar(&cfg.InfobloxCreatePTR) app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName) app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername) app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword) diff --git a/provider/infoblox/infoblox.go b/provider/infoblox/infoblox.go index 9c98adf2e..cd2b80074 100644 --- a/provider/infoblox/infoblox.go +++ b/provider/infoblox/infoblox.go @@ -19,6 +19,7 @@ package infoblox import ( "context" "fmt" + "net" "net/http" "os" "sort" @@ -33,6 +34,10 @@ import ( "sigs.k8s.io/external-dns/provider" ) +const ( + infobloxRecordTTL = 300 +) + // InfobloxConfig clarifies the method signature type InfobloxConfig struct { DomainFilter endpoint.DomainFilter @@ -222,6 +227,23 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical)) } + if p.createPTR { + var resP []ibclient.RecordPTR + objP := ibclient.NewRecordPTR( + ibclient.RecordPTR{ + Zone: zone.Fqdn, + View: p.view, + }, + ) + err = p.client.GetObject(objP, "", &resP) + if err != nil { + return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %s", zone.Fqdn, err) + } + for _, res := range resP { + endpoints = append(endpoints, endpoint.NewEndpoint(res.PtrdName, endpoint.RecordTypePTR, res.Ipv4Addr)) + } + } + var resT []ibclient.RecordTXT objT := ibclient.NewRecordTXT( ibclient.RecordTXT{ @@ -301,6 +323,17 @@ func (p *InfobloxProvider) mapChanges(zones []ibclient.ZoneAuth, changes *plan.C } // Ensure the record type is suitable changeMap[zone.Fqdn] = append(changeMap[zone.Fqdn], change) + + if p.createPTR && change.RecordType == endpoint.RecordTypeA { + reverseZone := p.findReverseZone(zones, change.Targets[0]) + if reverseZone == nil { + logrus.Debugf("Ignoring changes to '%s' because a suitable Infoblox DNS zone was not found.", change.Targets[0]) + return + } + changecopy := *change + changecopy.RecordType = endpoint.RecordTypePTR + changeMap[reverseZone.Fqdn] = append(changeMap[reverseZone.Fqdn], &changecopy) + } } for _, change := range changes.Delete { @@ -338,7 +371,34 @@ func (p *InfobloxProvider) findZone(zones []ibclient.ZoneAuth, name string) *ibc return result } +func (p *InfobloxProvider) findReverseZone(zones []ibclient.ZoneAuth, name string) *ibclient.ZoneAuth { + ip := net.ParseIP(name) + networks := map[int]*ibclient.ZoneAuth{} + maxMask := 0 + + for _, zone := range zones { + _, net, err := net.ParseCIDR(zone.Fqdn) + if err != nil { + logrus.WithError(err).Debugf("fqdn %s is no cidr", zone.Fqdn) + } else { + if net.Contains(ip) { + _, mask := net.Mask.Size() + networks[mask] = &zone + if mask > maxMask { + maxMask = mask + } + } + } + } + return networks[maxMask] +} + func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) { + var ttl uint = infobloxRecordTTL + if ep.RecordTTL.IsConfigured() { + ttl = uint(ep.RecordTTL) + } + switch ep.RecordType { case endpoint.RecordTypeA: var res []ibclient.RecordA @@ -355,6 +415,27 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targ return } } + obj.Ttl = ttl + recordSet = infobloxRecordSet{ + obj: obj, + res: &res, + } + case endpoint.RecordTypePTR: + var res []ibclient.RecordPTR + obj := ibclient.NewRecordPTR( + ibclient.RecordPTR{ + PtrdName: ep.DNSName, + Ipv4Addr: ep.Targets[0], + View: p.view, + }, + ) + if getObject { + err = p.client.GetObject(obj, "", &res) + if err != nil { + return + } + } + obj.Ttl = ttl recordSet = infobloxRecordSet{ obj: obj, res: &res, @@ -398,6 +479,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targ return } } + obj.TTL = int(ttl) recordSet = infobloxRecordSet{ obj: obj, res: &res, @@ -483,6 +565,10 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) { for _, record := range *recordSet.res.(*[]ibclient.RecordA) { _, err = p.client.DeleteObject(record.Ref) } + case endpoint.RecordTypePTR: + for _, record := range *recordSet.res.(*[]ibclient.RecordPTR) { + _, err = p.client.DeleteObject(record.Ref) + } case endpoint.RecordTypeCNAME: for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) { _, err = p.client.DeleteObject(record.Ref) diff --git a/provider/infoblox/infoblox_test.go b/provider/infoblox/infoblox_test.go index a91e03664..a7f1568a7 100644 --- a/provider/infoblox/infoblox_test.go +++ b/provider/infoblox/infoblox_test.go @@ -25,6 +25,7 @@ import ( "testing" ibclient "github.com/infobloxopen/infoblox-go-client" + "github.com/miekg/dns" "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" @@ -89,6 +90,22 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string, ) obj.(*ibclient.RecordTXT).Ref = ref ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordTXT).Name)), obj.(*ibclient.RecordTXT).Name) + case "record:ptr": + fmt.Printf("create ptr record\n") + client.createdEndpoints = append( + client.createdEndpoints, + endpoint.NewEndpoint( + obj.(*ibclient.RecordPTR).PtrdName, + endpoint.RecordTypePTR, + obj.(*ibclient.RecordPTR).Ipv4Addr, + ), + ) + obj.(*ibclient.RecordPTR).Ref = ref + reverseAddr, err := dns.ReverseAddr(obj.(*ibclient.RecordPTR).Ipv4Addr) + if err != nil { + return ref, fmt.Errorf("unable to create reverse addr from %s", obj.(*ibclient.RecordPTR).Ipv4Addr) + } + ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordPTR).PtrdName)), reverseAddr) } *client.mockInfobloxObjects = append( *client.mockInfobloxObjects, @@ -163,6 +180,22 @@ func (client *mockIBConnector) GetObject(obj ibclient.IBObject, ref string, res } } *res.(*[]ibclient.RecordTXT) = result + case "record:ptr": + var result []ibclient.RecordPTR + for _, object := range *client.mockInfobloxObjects { + if object.ObjectType() == "record:ptr" { + if ref != "" && + ref != object.(*ibclient.RecordPTR).Ref { + continue + } + if obj.(*ibclient.RecordPTR).PtrdName != "" && + obj.(*ibclient.RecordPTR).PtrdName != object.(*ibclient.RecordPTR).PtrdName { + continue + } + result = append(result, *object.(*ibclient.RecordPTR)) + } + } + *res.(*[]ibclient.RecordPTR) = result case "zone_auth": *res.(*[]ibclient.ZoneAuth) = *client.mockInfobloxZones } @@ -246,6 +279,24 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro ), ) } + case "record:ptr": + var records []ibclient.RecordPTR + obj := ibclient.NewRecordPTR( + ibclient.RecordPTR{ + Name: result[2], + }, + ) + client.GetObject(obj, ref, &records) + for _, record := range records { + client.deletedEndpoints = append( + client.deletedEndpoints, + endpoint.NewEndpoint( + record.PtrdName, + endpoint.RecordTypePTR, + "", + ), + ) + } } return "", nil } @@ -339,16 +390,25 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject }, }, ) + case endpoint.RecordTypePTR: + return ibclient.NewRecordPTR( + ibclient.RecordPTR{ + Ref: ref, + PtrdName: name, + Ipv4Addr: value, + }, + ) } return nil } -func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider { +func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, createPTR bool, client ibclient.IBConnector) *InfobloxProvider { return &InfobloxProvider{ client: client, domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, dryRun: dryRun, + createPTR: createPTR, } } @@ -376,7 +436,7 @@ func TestInfobloxRecords(t *testing.T) { }, } - provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client) + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, false, &client) actual, err := provider.Records(context.Background()) if err != nil { @@ -399,10 +459,35 @@ func TestInfobloxRecords(t *testing.T) { validateEndpoints(t, actual, expected) } +func TestInfobloxRecordsReverse(t *testing.T) { + client := mockIBConnector{ + mockInfobloxZones: &[]ibclient.ZoneAuth{ + createMockInfobloxZone("10.0.0.0/24"), + createMockInfobloxZone("10.0.1.0/24"), + }, + mockInfobloxObjects: &[]ibclient.IBObject{ + createMockInfobloxObject("example.com", endpoint.RecordTypePTR, "10.0.0.1"), + createMockInfobloxObject("example2.com", endpoint.RecordTypePTR, "10.0.0.2"), + }, + } + + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"10.0.0.0/24"}), provider.NewZoneIDFilter([]string{""}), true, true, &client) + actual, err := provider.Records(context.Background()) + + if err != nil { + t.Fatal(err) + } + expected := []*endpoint.Endpoint{ + endpoint.NewEndpoint("example.com", endpoint.RecordTypePTR, "10.0.0.1"), + endpoint.NewEndpoint("example2.com", endpoint.RecordTypePTR, "10.0.0.2"), + } + validateEndpoints(t, actual, expected) +} + func TestInfobloxApplyChanges(t *testing.T) { client := mockIBConnector{} - testInfobloxApplyChangesInternal(t, false, &client) + testInfobloxApplyChangesInternal(t, false, false, &client) validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{ endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"), @@ -423,7 +508,39 @@ func TestInfobloxApplyChanges(t *testing.T) { endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""), endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""), endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""), - endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, ""), + endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""), + }) + + validateEndpoints(t, client.updatedEndpoints, []*endpoint.Endpoint{}) +} + +func TestInfobloxApplyChangesReverse(t *testing.T) { + client := mockIBConnector{} + + testInfobloxApplyChangesInternal(t, false, true, &client) + + validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{ + endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("example.com", endpoint.RecordTypePTR, "1.2.3.4"), + endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypePTR, "1.2.3.4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"), + endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"), + endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"), + endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"), + endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "1.2.3.4,3.4.5.6,8.9.10.11"), + endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "tag-multiple-A-records"), + }) + + validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{ + endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""), + endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""), + endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""), + endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypePTR, ""), endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""), }) @@ -435,7 +552,7 @@ func TestInfobloxApplyChangesDryRun(t *testing.T) { mockInfobloxObjects: &[]ibclient.IBObject{}, } - testInfobloxApplyChangesInternal(t, true, &client) + testInfobloxApplyChangesInternal(t, true, false, &client) validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{}) @@ -444,14 +561,16 @@ func TestInfobloxApplyChangesDryRun(t *testing.T) { validateEndpoints(t, client.updatedEndpoints, []*endpoint.Endpoint{}) } -func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient.IBConnector) { +func testInfobloxApplyChangesInternal(t *testing.T, dryRun, createPTR bool, client ibclient.IBConnector) { client.(*mockIBConnector).mockInfobloxZones = &[]ibclient.ZoneAuth{ createMockInfobloxZone("example.com"), createMockInfobloxZone("other.com"), + createMockInfobloxZone("1.2.3.0/24"), } client.(*mockIBConnector).mockInfobloxObjects = &[]ibclient.IBObject{ createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"), createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"), + createMockInfobloxObject("deleted.example.com", endpoint.RecordTypePTR, "121.212.121.212"), createMockInfobloxObject("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"), createMockInfobloxObject("old.example.com", endpoint.RecordTypeA, "121.212.121.212"), createMockInfobloxObject("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"), @@ -461,6 +580,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient endpoint.NewDomainFilter([]string{""}), provider.NewZoneIDFilter([]string{""}), dryRun, + createPTR, client, ) @@ -493,11 +613,14 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient deleteRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"), - endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"), endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"), endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"), } + if createPTR { + deleteRecords = append(deleteRecords, endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypePTR, "121.212.121.212")) + } + changes := &plan.Changes{ Create: createRecords, UpdateNew: updateNewRecords, @@ -516,11 +639,12 @@ func TestInfobloxZones(t *testing.T) { createMockInfobloxZone("example.com"), createMockInfobloxZone("lvl1-1.example.com"), createMockInfobloxZone("lvl2-1.lvl1-1.example.com"), + createMockInfobloxZone("1.2.3.0/24"), }, mockInfobloxObjects: &[]ibclient.IBObject{}, } - provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, &client) + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com", "1.2.3.0/24"}), provider.NewZoneIDFilter([]string{""}), true, false, &client) zones, _ := provider.zones() var emptyZoneAuth *ibclient.ZoneAuth assert.Equal(t, provider.findZone(zones, "example.com").Fqdn, "example.com") @@ -531,6 +655,7 @@ func TestInfobloxZones(t *testing.T) { assert.Equal(t, provider.findZone(zones, "lvl2-1.lvl1-1.example.com").Fqdn, "lvl2-1.lvl1-1.example.com") assert.Equal(t, provider.findZone(zones, "lvl2-2.lvl1-1.example.com").Fqdn, "lvl1-1.example.com") assert.Equal(t, provider.findZone(zones, "lvl2-2.lvl1-2.example.com").Fqdn, "example.com") + assert.Equal(t, provider.findZone(zones, "1.2.3.0/24").Fqdn, "1.2.3.0/24") } func TestExtendedRequestFDQDRegExBuilder(t *testing.T) {