infoblox: also create a ptr entry

This commit is contained in:
Christian Groschupp 2020-10-19 14:47:55 +02:00 committed by Pavel Tumik
parent 253d94023f
commit f9145981c3
No known key found for this signature in database
GPG Key ID: 221BDC861516601D
6 changed files with 226 additions and 8 deletions

View File

@ -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".

View File

@ -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

View File

@ -244,6 +244,7 @@ func main() {
MaxResults: cfg.InfobloxMaxResults,
DryRun: cfg.DryRun,
FQDNRexEx: cfg.InfobloxFQDNRegEx,
CreatePTR: cfg.InfobloxCreatePTR,
},
)
case "dyn":

View File

@ -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)

View File

@ -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)

View File

@ -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) {