Skip to content

Commit

Permalink
Merge pull request kubernetes-sigs#2258 from sagor999/ptr-support
Browse files Browse the repository at this point in the history
Infoblox: add PTR record support
  • Loading branch information
k8s-ci-robot authored Oct 20, 2021
2 parents 0025410 + ae07a2d commit 3756f60
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 9 deletions.
10 changes: 10 additions & 0 deletions docs/tutorials/infoblox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down Expand Up @@ -158,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".
Expand Down Expand Up @@ -268,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.
2 changes: 2 additions & 0 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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=
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func main() {
MaxResults: cfg.InfobloxMaxResults,
DryRun: cfg.DryRun,
FQDNRexEx: cfg.InfobloxFQDNRegEx,
CreatePTR: cfg.InfobloxCreatePTR,
},
)
case "dyn":
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type Config struct {
InfobloxView string
InfobloxMaxResults int
InfobloxFQDNRegEx string
InfobloxCreatePTR bool
DynCustomerName string
DynUsername string
DynPassword string `secure:"yes"`
Expand Down Expand Up @@ -239,6 +240,7 @@ var defaultConfig = &Config{
InfobloxView: "",
InfobloxMaxResults: 0,
InfobloxFQDNRegEx: "",
InfobloxCreatePTR: false,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{},
OVHEndpoint: "ovh-eu",
Expand Down Expand Up @@ -426,6 +428,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)
Expand Down
156 changes: 155 additions & 1 deletion provider/infoblox/infoblox.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ package infoblox
import (
"context"
"fmt"
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"

transform "github.com/StackExchange/dnscontrol/pkg/transform"
ibclient "github.com/infobloxopen/infoblox-go-client"
"github.com/sirupsen/logrus"

Expand All @@ -33,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
Expand All @@ -47,6 +54,7 @@ type InfobloxConfig struct {
View string
MaxResults int
FQDNRexEx string
CreatePTR bool
}

// InfobloxProvider implements the DNS provider for Infoblox.
Expand All @@ -58,6 +66,7 @@ type InfobloxProvider struct {
view string
dryRun bool
fqdnRegEx string
createPTR bool
}

type infobloxRecordSet struct {
Expand Down Expand Up @@ -143,6 +152,7 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
dryRun: infobloxConfig.DryRun,
view: infobloxConfig.View,
fqdnRegEx: infobloxConfig.FQDNRexEx,
createPTR: infobloxConfig.CreatePTR,
}

return provider, nil
Expand Down Expand Up @@ -170,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 {
Expand Down Expand Up @@ -203,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)
}
}

Expand All @@ -222,6 +241,29 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical))
}

if p.createPTR {
// 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))
}
}
}

var resT []ibclient.RecordTXT
objT := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Expand All @@ -242,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()
Expand Down Expand Up @@ -301,6 +399,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 reverse 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 {
Expand Down Expand Up @@ -338,6 +447,28 @@ 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 i, 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] = &zones[i]
if mask > maxMask {
maxMask = mask
}
}
}
}
return networks[maxMask]
}

func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) {
switch ep.RecordType {
case endpoint.RecordTypeA:
Expand All @@ -359,6 +490,25 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targ
obj: obj,
res: &res,
}
case endpoint.RecordTypePTR:
var res []ibclient.RecordPTR
obj := ibclient.NewRecordPTR(
ibclient.RecordPTR{
PtrdName: ep.DNSName,
Ipv4Addr: ep.Targets[targetIndex],
View: p.view,
},
)
if getObject {
err = p.client.GetObject(obj, "", &res)
if err != nil {
return
}
}
recordSet = infobloxRecordSet{
obj: obj,
res: &res,
}
case endpoint.RecordTypeCNAME:
var res []ibclient.RecordCNAME
obj := ibclient.NewRecordCNAME(
Expand Down Expand Up @@ -483,6 +633,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)
Expand Down
Loading

0 comments on commit 3756f60

Please sign in to comment.