diff --git a/docs/tutorials/infoblox.md b/docs/tutorials/infoblox.md index 701e7b35e..bab67dfaf 100644 --- a/docs/tutorials/infoblox.md +++ b/docs/tutorials/infoblox.md @@ -159,6 +159,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". @@ -269,3 +270,11 @@ There is also the ability to filter results from the Infoblox zone_auth service ``` --infoblox-fqdn-regex=^staging.*test.com$ ``` + +## Infoblox PTR record support + +There is an option to enable PTR records support for infoblox provider. PTR records allow to do reverse dns search. To enable PTR records support, add following into arguments for external-dns: +`--infoblox-create-ptr` to allow management of PTR records. +You can also add a filter for reverse dns zone to limit PTR records to specific zones only: +`--domain-filter=10.196.0.0/16` change this to the reverse zone(s) as defined in your infoblox. +Now external-dns will manage PTR records for you. diff --git a/go.mod b/go.mod index 182d4323d..97b627cc5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/Azure/go-autorest/autorest/adal v0.9.16 github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 + github.com/StackExchange/dnscontrol v0.2.8 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 b5eb4bbf1..195f83d01 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhprfxlpofQ= +github.com/StackExchange/dnscontrol v0.2.8 h1:7jviqDH9cIqRSRpH0UxgmpT7a8CwEhs9mLHBhoYhXo8= +github.com/StackExchange/dnscontrol v0.2.8/go.mod h1:BH+5nX50JxHDdb3+AD/z/UfYMCc7iaqEkRtQ+NjcFGE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= diff --git a/provider/infoblox/infoblox.go b/provider/infoblox/infoblox.go index b8e30d08c..f38f7a67f 100644 --- a/provider/infoblox/infoblox.go +++ b/provider/infoblox/infoblox.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" + transform "github.com/StackExchange/dnscontrol/pkg/transform" ibclient "github.com/infobloxopen/infoblox-go-client" "github.com/sirupsen/logrus" @@ -34,6 +35,11 @@ import ( "sigs.k8s.io/external-dns/provider" ) +const ( + // provider specific key to track if PTR record was already created or not for A records + providerSpecificInfobloxPtrRecord = "infoblox-ptr-record-exists" +) + // InfobloxConfig clarifies the method signature type InfobloxConfig struct { DomainFilter endpoint.DomainFilter @@ -174,6 +180,9 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E } for _, res := range resA { newEndpoint := endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr) + if p.createPTR { + newEndpoint.WithProviderSpecific(providerSpecificInfobloxPtrRecord, "false") + } // Check if endpoint already exists and add to existing endpoint if it does foundExisting := false for _, ep := range endpoints { @@ -207,7 +216,13 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E } for _, res := range resH { for _, ip := range res.Ipv4Addrs { - endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, ip.Ipv4Addr)) + // host record is an abstraction in infoblox that combines A and PTR records + // for any host record we already should have a PTR record in infoblox, so mark it as created + newEndpoint := endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, ip.Ipv4Addr) + if p.createPTR { + newEndpoint.WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true") + } + endpoints = append(endpoints, newEndpoint) } } @@ -227,19 +242,25 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E } 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)) + // infoblox doesn't accept reverse zone's fqdn, and instead expects .in-addr.arpa zone + // so convert our zone fqdn (if it is a correct cidr block) into in-addr.arpa address and pass that into infoblox + // example: 10.196.38.0/24 becomes 38.196.10.in-addr.arpa + arpaZone, err := transform.ReverseDomainName(zone.Fqdn) + if err == nil { + var resP []ibclient.RecordPTR + objP := ibclient.NewRecordPTR( + ibclient.RecordPTR{ + Zone: arpaZone, + 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)) + } } } @@ -263,10 +284,66 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text)) } } + + // update A records that have PTR record created for them already + if p.createPTR { + // save all ptr records into map for a quick look up + ptrRecordsMap := make(map[string]bool) + for _, ptrRecord := range endpoints { + if ptrRecord.RecordType != endpoint.RecordTypePTR { + continue + } + ptrRecordsMap[ptrRecord.DNSName] = true + } + + for i := range endpoints { + if endpoints[i].RecordType != endpoint.RecordTypeA { + continue + } + // if PTR record already exists for A record, then mark it as such + if ptrRecordsMap[endpoints[i].DNSName] { + found := false + for j := range endpoints[i].ProviderSpecific { + if endpoints[i].ProviderSpecific[j].Name == providerSpecificInfobloxPtrRecord { + endpoints[i].ProviderSpecific[j].Value = "true" + found = true + } + } + if !found { + endpoints[i].WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true") + } + } + } + } logrus.Debugf("fetched %d records from infoblox", len(endpoints)) return endpoints, nil } +func (p *InfobloxProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { + if !p.createPTR { + return endpoints + } + + // for all A records, we want to create PTR records + // so add provider specific property to track if the record was created or not + for i := range endpoints { + if endpoints[i].RecordType == endpoint.RecordTypeA { + found := false + for j := range endpoints[i].ProviderSpecific { + if endpoints[i].ProviderSpecific[j].Name == providerSpecificInfobloxPtrRecord { + endpoints[i].ProviderSpecific[j].Value = "true" + found = true + } + } + if !found { + endpoints[i].WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true") + } + } + } + + return endpoints +} + // ApplyChanges applies the given changes. func (p *InfobloxProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, err := p.zones() @@ -418,7 +495,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targ obj := ibclient.NewRecordPTR( ibclient.RecordPTR{ PtrdName: ep.DNSName, - Ipv4Addr: ep.Targets[0], + Ipv4Addr: ep.Targets[targetIndex], View: p.view, }, ) diff --git a/provider/infoblox/infoblox_test.go b/provider/infoblox/infoblox_test.go index a27084725..9a9a57d3d 100644 --- a/provider/infoblox/infoblox_test.go +++ b/provider/infoblox/infoblox_test.go @@ -91,7 +91,6 @@ 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( @@ -459,7 +458,38 @@ func TestInfobloxRecords(t *testing.T) { validateEndpoints(t, actual, expected) } +func TestInfobloxAdjustEndpoints(t *testing.T) { + client := mockIBConnector{ + mockInfobloxZones: &[]ibclient.ZoneAuth{ + createMockInfobloxZone("example.com"), + createMockInfobloxZone("other.com"), + }, + mockInfobloxObjects: &[]ibclient.IBObject{ + createMockInfobloxObject("example.com", endpoint.RecordTypeA, "123.123.123.122"), + createMockInfobloxObject("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), + createMockInfobloxObject("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"), + createMockInfobloxObject("host.example.com", "HOST", "125.1.1.1"), + }, + } + + provider := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, true, &client) + actual, err := provider.Records(context.Background()) + if err != nil { + t.Fatal(err) + } + provider.AdjustEndpoints(actual) + + expected := []*endpoint.Endpoint{ + endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122").WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true"), + endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""), + endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"), + endpoint.NewEndpoint("host.example.com", endpoint.RecordTypeA, "125.1.1.1").WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true"), + } + validateEndpoints(t, actual, expected) +} + func TestInfobloxRecordsReverse(t *testing.T) { + client := mockIBConnector{ mockInfobloxZones: &[]ibclient.ZoneAuth{ createMockInfobloxZone("10.0.0.0/24"),