Merge pull request #4516 from mloiseleur/feat/drop-infobox-provider

chore!: Remove infoblox in-tree provider
This commit is contained in:
Kubernetes Prow Robot 2024-06-06 10:00:17 -07:00 committed by GitHub
commit e608c9e9d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 59 additions and 2139 deletions

View File

@ -38,7 +38,6 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
* [RcodeZero](https://www.rcodezero.at/)
* [DigitalOcean](https://www.digitalocean.com/products/networking)
* [DNSimple](https://dnsimple.com/)
* [Infoblox](https://www.infoblox.com/products/dns/)
* [Dyn](https://dyn.com/dns/)
* [OpenStack Designate](https://docs.openstack.org/designate/latest/)
* [PowerDNS](https://www.powerdns.com/)
@ -118,7 +117,6 @@ The following table clarifies the current status of the providers according to t
| RcodeZero | Alpha | |
| DigitalOcean | Alpha | |
| DNSimple | Alpha | |
| Infoblox | Alpha | @saileshgiri |
| Dyn | Alpha | |
| OpenStack Designate | Alpha | |
| PowerDNS | Alpha | |
@ -187,7 +185,6 @@ The following tutorials are provided:
* [Using Google's Default Ingress Controller](docs/tutorials/gke.md)
* [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
* [Headless Services](docs/tutorials/hostport.md)
* [Infoblox](docs/tutorials/infoblox.md)
* [Istio Gateway Source](docs/tutorials/istio.md)
* [Kubernetes Security Context](docs/tutorials/security-context.md)
* [Linode](docs/tutorials/linode.md)

View File

@ -1,64 +1,64 @@
# Multiple Targets per hostname
# Multiple Targets per hostname
*(November 2017)*
## Purpose
## Purpose
One should be able to define multiple targets (IPs/Hostnames) in the **same** Kubernetes resource object and expect
ExternalDNS create DNS record(s) with a specified hostname and all targets. So far the connection between k8s resources (ingress/services) and DNS records
One should be able to define multiple targets (IPs/Hostnames) in the **same** Kubernetes resource object and expect
ExternalDNS create DNS record(s) with a specified hostname and all targets. So far the connection between k8s resources (ingress/services) and DNS records
were not streamlined. This proposal aims to make the connection explicit, making k8s resources acquire or release certain DNS names. As long as the resource
ingress/service owns the record it can have multiple targets enable iff they are specified in the same resource.
ingress/service owns the record it can have multiple targets enable iff they are specified in the same resource.
## Use cases
## Use cases
See https://github.com/kubernetes-sigs/external-dns/issues/239
## Current behaviour
## Current behaviour
*(as of the moment of writing)*
Central piece of enabling multi-target is having consistent and correct behaviour in `plan` component in regards to how endpoints generated
Central piece of enabling multi-target is having consistent and correct behaviour in `plan` component in regards to how endpoints generated
from kubernetes resources are mapped to dns records. Current implementation of the `plan` has inconsistent behaviour in the following scenarios, all
of which must be resolved before multi-target support can be enabled in the provider implementations:
of which must be resolved before multi-target support can be enabled in the provider implementations:
1. No records registered so far. Two **different** ingresses request same hostname but different targets, e.g. Ingress A: example.com -> 1.1.1.1 and Ingress B: example.com -> 2.2.2.2
1. No records registered so far. Two **different** ingresses request same hostname but different targets, e.g. Ingress A: example.com -> 1.1.1.1 and Ingress B: example.com -> 2.2.2.2
* *Current Behaviour*: both are added to the "Create" (records to be created) list and passed to Provider
* *Expected Behaviour*: only one (random/ or according to predefined strategy) should be chosen and passed to Provider
**NOTE**: while this seems to go against multi-target support, this is done so no other resource can "hijack" already created DNS record. Multi targets are supported only
on per single resource basis
* *Expected Behaviour*: only one (random/ or according to predefined strategy) should be chosen and passed to Provider
2. Now let's say Ingress A was chosen and successfully created, but both ingress A and B are still there. So on next iteration ExternalDNS would see both again in the Desired list.
**NOTE**: while this seems to go against multi-target support, this is done so no other resource can "hijack" already created DNS record. Multi targets are supported only
on per single resource basis
2. Now let's say Ingress A was chosen and successfully created, but both ingress A and B are still there. So on next iteration ExternalDNS would see both again in the Desired list.
* *Current Behaviour*: DNS record target will change to that of Ingress B.
* *Expected Behaviour*: Ingress A should stay unchanged. Ingress B record is not created
3. DNS record for Ingress A was created but its target has changed. Ingress B is still there
* *Current Behaviour*: Undetermined behaviour based on which ingress will be parsed last.
* *Current Behaviour*: Undetermined behaviour based on which ingress will be parsed last.
* *Expected Behaviour*: DNS record should point to the new target specified in A. Ingress B should still be ignored.
**NOTE**: both 2. and 3. can be resolved if External DNS is aware which resource has already acquired DNS record
**NOTE**: both 2. and 3. can be resolved if External DNS is aware which resource has already acquired DNS record
4. Ingress C has multiple targets: 1.1.1.1 and 2.2.2.2
* *Current Behaviour*: Both targets are split into different endpoints and we end up in one of the cases above
* *Expected Behaviour*: Endpoint should contain list of targets and treated as one ingress object.
* *Current Behaviour*: Both targets are split into different endpoints and we end up in one of the cases above
* *Expected Behaviour*: Endpoint should contain list of targets and treated as one ingress object.
## Requirements and assumptions
For this feature to work we have to make sure that:
For this feature to work we have to make sure that:
1. DNS records are now owned by certain ingress/service resources. For External DNS it would mean that TXT records now
should store back-reference for the resource this record was created for, i.e. `"heritage=external-dns,external-dns/resource=ingress/default/my-ingress-object-name"`
2. DNS records are updated only:
1. DNS records are now owned by certain ingress/service resources. For External DNS it would mean that TXT records now
should store back-reference for the resource this record was created for, i.e. `"heritage=external-dns,external-dns/resource=ingress/default/my-ingress-object-name"`
2. DNS records are updated only:
- If owning resource target list has changed
- If owning resource target list has changed
- If owning resource record is not found in the desired list (meaning it was deleted), therefore it will now be owned by another record. So its target list will be updated
- Changes related to other record properties (e.g. TTL)
- Changes related to other record properties (e.g. TTL)
4. All of the issues described in `Current Behaviour` sections are resolved
4. All of the issues described in `Current Behaviour` sections are resolved
Once Create/Update/Delete lists are calculated correctly (this is where conflicts based on requested DNS names are resolved) they are passed to `provider`, where
`provider` specific implementation will decide how to convert the structures into required formats. If DNS provider does not (or partially) support multi targets
then it is up to the provider to make sure that the change list of records passed to the DNS provider API is valid. **TODO**: explain best strategy.
then it is up to the provider to make sure that the change list of records passed to the DNS provider API is valid. **TODO**: explain best strategy.
Additionally see https://github.com/kubernetes-sigs/external-dns/issues/258
@ -66,54 +66,53 @@ Additionally see https://github.com/kubernetes-sigs/external-dns/issues/258
Brief summary of open PRs and what they are trying to address:
### PRs
### PRs
1. https://github.com/kubernetes-sigs/external-dns/pull/243 - first attempt to add support for multiple targets. It is lagging far behind from tip
*what it does*: unfinished attempt to extend `Endpoint` struct, for it to allow multiple targets (essentially `target string -> targets []string`)
*action*: evaluate if rebasing makes sense, or we can just close it.
2. https://github.com/kubernetes-sigs/external-dns/pull/261 - attempt to rework `plan` to make it work correctly with multiple targets.
*action*: evaluate if rebasing makes sense, or we can just close it.
2. https://github.com/kubernetes-sigs/external-dns/pull/261 - attempt to rework `plan` to make it work correctly with multiple targets.
*what it does* : attempts to fix issues with `plan` described in `Current Behaviour` section above. Included tests reveal the current problem with `plan`
*action*: rebase on default branch and make necessary changes to satisfy requirements listed in this document including back-reference to owning record
3. https://github.com/kubernetes-sigs/external-dns/pull/326 - attempt to add multiple target support.
3. https://github.com/kubernetes-sigs/external-dns/pull/326 - attempt to add multiple target support.
*what it does*: for each pair `DNS Name` + `Record Type` it aggregates **all** targets from the cluster and passes them to Provider. It adds basic support
for DO, Azure, Cloudflare, AWS, GCP, however those are not tested (?). (DNSSimple and Infoblox providers were not updated)
*action*: the `plan` logic will probably needs to be reworked, however the rest concerning support in Providers and extending `Endpoint` struct can be reused.
*action*: the `plan` logic will probably needs to be reworked, however the rest concerning support in Providers and extending `Endpoint` struct can be reused.
Rebase on default branch and add missing pieces. Depends on `2`.
Related PRs: https://github.com/kubernetes-sigs/external-dns/pull/331/files, https://github.com/kubernetes-sigs/external-dns/pull/347/files - aiming at AWS Route53 weighted records.
These PRs should be considered after common agreement about the way to address multi-target support is achieved. Related discussion: https://github.com/kubernetes-sigs/external-dns/issues/196
### How to proceed from here
The following steps are needed:
1. Make sure consensus regarding the approach is achieved via collaboration on the current document
The following steps are needed:
1. Make sure consensus regarding the approach is achieved via collaboration on the current document
2. Notify all PR (see above) authors about the agreed approach
3. Implementation:
3. Implementation:
a. `Plan` is working as expected - either based on #261 above or from scratch. `Plan` should be working correctly regardless of multi-target support
b. Extensive testing making sure new `plan` does not introduce any breaking changes
c. Change Endpoint struct to support multiple targets - based on #326 - integrate it with new `plan` @sethpollack
d. Make sure new endpoint format can still be used in providers which have only partial support for multi targets ~~**TODO**: how ?~~ . This is to be done by simply using first target in the targets list.
e. Add support for multi target which are already addressed in #326. It goes alongside c. and can be based on the same PR @sethpollack. New providers
added since then should maintain same functionality.
d. Make sure new endpoint format can still be used in providers which have only partial support for multi targets ~~**TODO**: how ?~~ . This is to be done by simply using first target in the targets list.
e. Add support for multi target which are already addressed in #326. It goes alongside c. and can be based on the same PR @sethpollack. New providers
added since then should maintain same functionality.
5. Extensive testing on **all** providers before making new release
6. Update all related documentation and explain how multi targets are supported on per provider basis
7. Think of introducing weighted records (see PRs section above) and making them configurable.
## Open questions
6. Update all related documentation and explain how multi targets are supported on per provider basis
7. Think of introducing weighted records (see PRs section above) and making them configurable.
## Open questions
- Handling cases when ingress/service targets include both hostnames and IPs - postpone this until use cases occurs
- "Weighted records scope": https://github.com/kubernetes-sigs/external-dns/issues/196 - this should be considered once multi-target support is implemented

View File

@ -1,290 +0,0 @@
# Setting up ExternalDNS for Infoblox
This tutorial describes how to setup ExternalDNS for usage with Infoblox.
Make sure to use **>=0.4.6** version of ExternalDNS for this tutorial. The only WAPI version that
has been validated is **v2.3.1**. It is assumed that the API user has rights to create objects of
the following types: `zone_auth`, `record:a`, `record:cname`, `record:txt`.
This tutorial assumes you have substituted the correct values for the following environment variables:
```
export GRID_HOST=127.0.0.1
export WAPI_PORT=443
export WAPI_VERSION=2.3.1
export WAPI_USERNAME=admin
export WAPI_PASSWORD=infoblox
```
## Creating an Infoblox DNS zone
The Infoblox provider for ExternalDNS will find suitable zones for domains it manages; it will
not automatically create zones.
Create an Infoblox DNS zone for "example.com":
```
$ curl -kl \
-X POST \
-d fqdn=example.com \
-u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth
```
Substitute a domain you own for "example.com" if desired.
## Creating an Infoblox Configuration Secret
For ExternalDNS to access the Infoblox API, create a Kubernetes secret.
To create the secret:
```
$ kubectl create secret generic external-dns \
--from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME=${WAPI_USERNAME} \
--from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD=${WAPI_PASSWORD}
```
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.2
args:
- --source=service
- --domain-filter=example.com # (optional) limit to only example.com domains.
- --provider=infoblox
- --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host.
- --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.
- --infoblox-view="" # (optional) DNS view (default: "")
env:
- name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS
value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10".
- name: EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT
value: "60" # (optional) Infoblox WAPI request timeout in seconds. The default is "60".
- name: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
- name: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.2
args:
- --source=service
- --domain-filter=example.com # (optional) limit to only example.com domains.
- --provider=infoblox
- --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host.
- --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.
- --infoblox-view="" # (optional) DNS view (default: "")
env:
- name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS
value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10".
- name: EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT
value: "60" # (optional) Infoblox WAPI request timeout in seconds. The default is "60".
- name: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
- name: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: example.com
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Note the annotation on the service; use the same hostname as the Infoblox DNS zone created above. The annotation may also be a subdomain
of the DNS zone (e.g. 'www.example.com').
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
will cause ExternalDNS to remove the corresponding DNS records.
Create the deployment and service:
```
$ kubectl create -f nginx.yaml
```
It takes a little while for the Infoblox cloud provider to create an external IP for the service. Check the status by running
`kubectl get services nginx`. If the `EXTERNAL-IP` field shows an address, the service is ready to be accessed externally.
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
the Infoblox DNS records.
## Verifying Infoblox DNS records
Run the following command to view the A records for your Infoblox DNS zone:
```
$ curl -kl \
-X GET \
-u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/record:a?zone=example.com
```
Substitute the zone for the one created above if a different domain was used.
This should show the external IP address of the service as the A record for your domain ('@' indicates the record is for the zone itself).
## Clean up
Now that we have verified that ExternalDNS will automatically manage Infoblox DNS records, we can delete the tutorial's
DNS zone:
```
$ curl -kl \
-X DELETE \
-u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth?fqdn=example.com
```
## Ability to filter results from the zone auth API using a regular expression
There is also the ability to filter results from the Infoblox zone_auth service based upon a regular expression. See the [Infoblox API document](https://www.infoblox.com/wp-content/uploads/infoblox-deployment-infoblox-rest-api.pdf) for examples. To use this feature for the zone_auth service, set the parameter infoblox-fqdn-regex for external-dns to a regular expression that makes sense for you. For instance, to only return hosted zones that start with staging in the test.com domain (like staging.beta.test.com, or staging.test.com), use the following command line option when starting external-dns
```
--infoblox-fqdn-regex=^staging.*test.com$
```
## Ability to filter A, Host, CNAME and TXT records from the by name using a regular expression
Infoblox supports filtering records by name using a regular expression. See the [Infoblox API document](https://www.infoblox.com/wp-content/uploads/infoblox-deployment-infoblox-rest-api.pdf) for examples. To use this feature, set the parameter infoblox-name-regex for external-dns to a regular expression that makes sense for you. For instance, if all your dns records end with `cluster1.example.com`, you can fetch records matching this style by setting the following:
```
--infoblox-name-regex=cluster1.example.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.

1
go.mod
View File

@ -34,7 +34,6 @@ require (
github.com/google/uuid v1.6.0
github.com/gophercloud/gophercloud v1.12.0
github.com/hooklift/gowsdl v0.5.0
github.com/infobloxopen/infoblox-go-client/v2 v2.6.0
github.com/linki/instrumented_http v0.3.0
github.com/linode/linodego v1.34.0
github.com/maxatome/go-testdeep v1.14.0

2
go.sum
View File

@ -655,8 +655,6 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/infobloxopen/infoblox-go-client/v2 v2.6.0 h1:nwdGhQ5XRheGybEdUQ4cSl1Vw2UsSQKKi+HEleguQug=
github.com/infobloxopen/infoblox-go-client/v2 v2.6.0/go.mod h1:Zu7c+X0mTB6ahIYm7p9LlvfcH814ZUEP+eXGPEYLDU4=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=

21
main.go
View File

@ -61,7 +61,6 @@ import (
"sigs.k8s.io/external-dns/provider/godaddy"
"sigs.k8s.io/external-dns/provider/google"
"sigs.k8s.io/external-dns/provider/ibmcloud"
"sigs.k8s.io/external-dns/provider/infoblox"
"sigs.k8s.io/external-dns/provider/inmemory"
"sigs.k8s.io/external-dns/provider/linode"
"sigs.k8s.io/external-dns/provider/ns1"
@ -280,26 +279,6 @@ func main() {
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
case "dnsimple":
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "infoblox":
p, err = infoblox.NewInfobloxProvider(
infoblox.StartupConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
Host: cfg.InfobloxGridHost,
Port: cfg.InfobloxWapiPort,
Username: cfg.InfobloxWapiUsername,
Password: cfg.InfobloxWapiPassword,
Version: cfg.InfobloxWapiVersion,
SSLVerify: cfg.InfobloxSSLVerify,
View: cfg.InfobloxView,
MaxResults: cfg.InfobloxMaxResults,
DryRun: cfg.DryRun,
FQDNRegEx: cfg.InfobloxFQDNRegEx,
NameRegEx: cfg.InfobloxNameRegEx,
CreatePTR: cfg.InfobloxCreatePTR,
CacheDuration: cfg.InfobloxCacheDuration,
},
)
case "dyn":
p, err = dyn.NewDynProvider(
dyn.DynConfig{

View File

@ -120,18 +120,6 @@ type Config struct {
AkamaiAccessToken string
AkamaiEdgercPath string
AkamaiEdgercSection string
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
InfobloxWapiPassword string `secure:"yes"`
InfobloxWapiVersion string
InfobloxSSLVerify bool
InfobloxView string
InfobloxMaxResults int
InfobloxFQDNRegEx string
InfobloxNameRegEx string
InfobloxCreatePTR bool
InfobloxCacheDuration int
DynCustomerName string
DynUsername string
DynPassword string `secure:"yes"`
@ -292,17 +280,6 @@ var defaultConfig = &Config{
AkamaiAccessToken: "",
AkamaiEdgercSection: "",
AkamaiEdgercPath: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InfobloxView: "",
InfobloxMaxResults: 0,
InfobloxFQDNRegEx: "",
InfobloxCreatePTR: false,
InfobloxCacheDuration: 0,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
OCIZoneScope: "GLOBAL",
OCIZoneCacheDuration: 0 * time.Second,
@ -475,7 +452,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
// Flags related to providers
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "webhook"}
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "webhook"}
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
@ -529,18 +506,6 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
app.Flag("infoblox-grid-host", "When using the Infoblox provider, specify the Grid Manager host (required when --provider=infoblox)").Default(defaultConfig.InfobloxGridHost).StringVar(&cfg.InfobloxGridHost)
app.Flag("infoblox-wapi-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort)
app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername)
app.Flag("infoblox-wapi-password", "When using the Infoblox provider, specify the WAPI password (required when --provider=infoblox)").Default(defaultConfig.InfobloxWapiPassword).StringVar(&cfg.InfobloxWapiPassword)
app.Flag("infoblox-wapi-version", "When using the Infoblox provider, specify the WAPI version (default: 2.3.1)").Default(defaultConfig.InfobloxWapiVersion).StringVar(&cfg.InfobloxWapiVersion)
app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true, disable with --no-infoblox-ssl-verify)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify)
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-name-regex", "Apply this regular expression as a filter on the name field for obtaining infoblox records. This is disabled by default.").Default(defaultConfig.InfobloxNameRegEx).StringVar(&cfg.InfobloxNameRegEx)
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("infoblox-cache-duration", "When using the Infoblox provider, set the record TTL (0s to disable).").Default(strconv.Itoa(defaultConfig.InfobloxCacheDuration)).IntVar(&cfg.InfobloxCacheDuration)
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

@ -88,14 +88,6 @@ var (
AkamaiAccessToken: "",
AkamaiEdgercPath: "",
AkamaiEdgercSection: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxView: "",
InfobloxSSLVerify: true,
InfobloxMaxResults: 0,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
OCIZoneScope: "GLOBAL",
OCIZoneCacheDuration: 0 * time.Second,
@ -202,14 +194,6 @@ var (
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiEdgercPath: "/home/test/.edgerc",
AkamaiEdgercSection: "default",
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxView: "internal",
InfobloxSSLVerify: false,
InfobloxMaxResults: 2000,
OCIConfigFile: "oci.yaml",
OCIZoneScope: "PRIVATE",
OCIZoneCacheDuration: 30 * time.Second,
@ -320,13 +304,6 @@ func TestParseFlags(t *testing.T) {
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
"--akamai-edgerc-path=/home/test/.edgerc",
"--akamai-edgerc-section=default",
"--infoblox-grid-host=127.0.0.1",
"--infoblox-wapi-port=8443",
"--infoblox-wapi-username=infoblox",
"--infoblox-wapi-password=infoblox",
"--infoblox-wapi-version=2.6.1",
"--infoblox-view=internal",
"--infoblox-max-results=2000",
"--inmemory-zone=example.org",
"--inmemory-zone=company.com",
"--ovh-endpoint=ovh-ca",
@ -340,7 +317,6 @@ func TestParseFlags(t *testing.T) {
"--tls-ca=/path/to/ca.crt",
"--tls-client-cert=/path/to/cert.pem",
"--tls-client-cert-key=/path/to/key.pem",
"--no-infoblox-ssl-verify",
"--domain-filter=example.org",
"--domain-filter=company.com",
"--exclude-domains=xapi.example.org",
@ -451,14 +427,6 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc",
"EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_VIEW": "internal",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE",
"EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s",
@ -564,7 +532,6 @@ func restoreEnv(t *testing.T, originalEnv map[string]string) {
func TestPasswordsNotLogged(t *testing.T) {
cfg := Config{
DynPassword: "dyn-pass",
InfobloxWapiPassword: "infoblox-pass",
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
}
@ -572,7 +539,6 @@ func TestPasswordsNotLogged(t *testing.T) {
s := cfg.String()
assert.False(t, strings.Contains(s, "dyn-pass"))
assert.False(t, strings.Contains(s, "infoblox-pass"))
assert.False(t, strings.Contains(s, "pdns-api-key"))
assert.False(t, strings.Contains(s, "tsig-secret"))
}

View File

@ -61,16 +61,6 @@ func ValidateConfig(cfg *externaldns.Config) error {
}
}
// Infoblox provider specific validations
if cfg.Provider == "infoblox" {
if cfg.InfobloxGridHost == "" {
return errors.New("no Infoblox Grid Manager host specified")
}
if cfg.InfobloxWapiPassword == "" {
return errors.New("no Infoblox WAPI password specified")
}
}
if cfg.Provider == "dyn" {
if cfg.DynUsername == "" {
return errors.New("no Dyn username specified")

View File

@ -1,744 +0,0 @@
/*
Copyright 2017 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 infoblox
import (
"context"
"fmt"
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"
ibclient "github.com/infobloxopen/infoblox-go-client/v2"
"github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/rfc2317"
"sigs.k8s.io/external-dns/plan"
"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"
)
func isNotFoundError(err error) bool {
_, ok := err.(*ibclient.NotFoundError)
return ok
}
// StartupConfig clarifies the method signature
type StartupConfig struct {
DomainFilter endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
Host string
Port int
Username string
Password string
Version string
SSLVerify bool
DryRun bool
View string
MaxResults int
FQDNRegEx string
NameRegEx string
CreatePTR bool
CacheDuration int
}
// ProviderConfig implements the DNS provider for Infoblox.
type ProviderConfig struct {
provider.BaseProvider
client ibclient.IBConnector
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
view string
dryRun bool
fqdnRegEx string
createPTR bool
cacheDuration int
}
type infobloxRecordSet struct {
obj ibclient.IBObject
res interface{}
}
// ExtendedRequestBuilder implements a HttpRequestBuilder which sets
// additional query parameter on all get requests
type ExtendedRequestBuilder struct {
fqdnRegEx string
nameRegEx string
maxResults int
ibclient.WapiRequestBuilder
}
// NewExtendedRequestBuilder returns a ExtendedRequestBuilder which adds
// _max_results query parameter to all GET requests
func NewExtendedRequestBuilder(maxResults int, fqdnRegEx string, nameRegEx string) *ExtendedRequestBuilder {
return &ExtendedRequestBuilder{
fqdnRegEx: fqdnRegEx,
nameRegEx: nameRegEx,
maxResults: maxResults,
}
}
// BuildRequest prepares the api request. it uses BuildRequest of
// WapiRequestBuilder and then add the _max_requests parameter
func (mrb *ExtendedRequestBuilder) BuildRequest(t ibclient.RequestType, obj ibclient.IBObject, ref string, queryParams *ibclient.QueryParams) (req *http.Request, err error) {
req, err = mrb.WapiRequestBuilder.BuildRequest(t, obj, ref, queryParams)
if req.Method == "GET" {
query := req.URL.Query()
if mrb.maxResults > 0 {
query.Set("_max_results", strconv.Itoa(mrb.maxResults))
}
_, zoneAuthQuery := obj.(*ibclient.ZoneAuth)
if zoneAuthQuery && t == ibclient.GET && mrb.fqdnRegEx != "" {
query.Set("fqdn~", mrb.fqdnRegEx)
}
// if we are not doing a ZoneAuth query, support the name filter
if !zoneAuthQuery && mrb.nameRegEx != "" {
query.Set("name~", mrb.nameRegEx)
}
req.URL.RawQuery = query.Encode()
}
return
}
// NewInfobloxProvider creates a new Infoblox provider.
func NewInfobloxProvider(ibStartupCfg StartupConfig) (*ProviderConfig, error) {
hostCfg := ibclient.HostConfig{
Host: ibStartupCfg.Host,
Port: strconv.Itoa(ibStartupCfg.Port),
Version: ibStartupCfg.Version,
}
authCfg := ibclient.AuthConfig{
Username: ibStartupCfg.Username,
Password: ibStartupCfg.Password,
}
httpPoolConnections := lookupEnvAtoi("EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS", 10)
httpRequestTimeout := lookupEnvAtoi("EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT", 60)
transportConfig := ibclient.NewTransportConfig(
strconv.FormatBool(ibStartupCfg.SSLVerify),
httpRequestTimeout,
httpPoolConnections,
)
var (
requestBuilder ibclient.HttpRequestBuilder
err error
)
if ibStartupCfg.MaxResults != 0 || ibStartupCfg.FQDNRegEx != "" || ibStartupCfg.NameRegEx != "" {
// use our own HttpRequestBuilder which sets _max_results parameter on GET requests
requestBuilder = NewExtendedRequestBuilder(ibStartupCfg.MaxResults, ibStartupCfg.FQDNRegEx, ibStartupCfg.NameRegEx)
} else {
// use the default HttpRequestBuilder of the infoblox client
requestBuilder, err = ibclient.NewWapiRequestBuilder(hostCfg, authCfg)
if err != nil {
return nil, err
}
}
requestor := &ibclient.WapiHttpRequestor{}
client, err := ibclient.NewConnector(hostCfg, authCfg, transportConfig, requestBuilder, requestor)
if err != nil {
return nil, err
}
providerCfg := &ProviderConfig{
client: client,
domainFilter: ibStartupCfg.DomainFilter,
zoneIDFilter: ibStartupCfg.ZoneIDFilter,
dryRun: ibStartupCfg.DryRun,
view: ibStartupCfg.View,
fqdnRegEx: ibStartupCfg.FQDNRegEx,
createPTR: ibStartupCfg.CreatePTR,
cacheDuration: ibStartupCfg.CacheDuration,
}
return providerCfg, nil
}
func recordQueryParams(zone string, view string) *ibclient.QueryParams {
searchFields := map[string]string{}
if zone != "" {
searchFields["zone"] = zone
}
if view != "" {
searchFields["view"] = view
}
return ibclient.NewQueryParams(false, searchFields)
}
// Records gets the current records.
func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.zones()
if err != nil {
return nil, fmt.Errorf("could not fetch zones: %w", err)
}
for _, zone := range zones {
logrus.Debugf("fetch records from zone '%s'", zone.Fqdn)
searchParams := recordQueryParams(zone.Fqdn, p.view)
var resA []ibclient.RecordA
objA := ibclient.NewEmptyRecordA()
err = p.client.GetObject(objA, "", searchParams, &resA)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch A records from zone '%s': %w", zone.Fqdn, err)
}
for _, res := range resA {
// Check if endpoint already exists and add to existing endpoint if it does
foundExisting := false
for _, ep := range endpoints {
if ep.DNSName == *res.Name && ep.RecordType == endpoint.RecordTypeA {
foundExisting = true
duplicateTarget := false
for _, t := range ep.Targets {
if t == *res.Ipv4Addr {
duplicateTarget = true
break
}
}
if duplicateTarget {
logrus.Debugf("A duplicate target '%s' found for existing A record '%s'", *res.Ipv4Addr, ep.DNSName)
} else {
logrus.Debugf("Adding target '%s' to existing A record '%s'", *res.Ipv4Addr, *res.Name)
ep.Targets = append(ep.Targets, *res.Ipv4Addr)
}
break
}
}
if !foundExisting {
newEndpoint := endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeA, *res.Ipv4Addr)
if p.createPTR {
newEndpoint.WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true")
}
endpoints = append(endpoints, newEndpoint)
}
}
// sort targets so that they are always in same order, as infoblox might return them in different order
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
// Include Host records since they should be treated synonymously with A records
var resH []ibclient.HostRecord
objH := ibclient.NewEmptyHostRecord()
err = p.client.GetObject(objH, "", searchParams, &resH)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch host records from zone '%s': %w", zone.Fqdn, err)
}
for _, res := range resH {
for _, ip := range res.Ipv4Addrs {
logrus.Debugf("Record='%s' A(H):'%s'", *res.Name, *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)
}
}
var resC []ibclient.RecordCNAME
objC := ibclient.NewEmptyRecordCNAME()
err = p.client.GetObject(objC, "", searchParams, &resC)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %w", zone.Fqdn, err)
}
for _, res := range resC {
logrus.Debugf("Record='%s' CNAME:'%s'", *res.Name, *res.Canonical)
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 := rfc2317.CidrToInAddr(zone.Fqdn)
if err == nil {
var resP []ibclient.RecordPTR
objP := ibclient.NewEmptyRecordPTR()
err = p.client.GetObject(objP, "", recordQueryParams(arpaZone, p.view), &resP)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %w", zone.Fqdn, err)
}
for _, res := range resP {
endpoints = append(endpoints, endpoint.NewEndpoint(*res.PtrdName, endpoint.RecordTypePTR, *res.Ipv4Addr))
}
}
}
var resT []ibclient.RecordTXT
objT := ibclient.NewEmptyRecordTXT()
err = p.client.GetObject(objT, "", searchParams, &resT)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %w", zone.Fqdn, err)
}
for _, res := range resT {
// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
// Unhandled, the missing double quotes would break the extractOwnerID method of the registry package.
if _, err := strconv.Unquote(*res.Text); err != nil {
quoted := strconv.Quote(*res.Text)
res.Text = &quoted
}
foundExisting := false
for _, ep := range endpoints {
if ep.DNSName == *res.Name && ep.RecordType == endpoint.RecordTypeTXT {
foundExisting = true
duplicateTarget := false
for _, t := range ep.Targets {
if t == *res.Text {
duplicateTarget = true
break
}
}
if duplicateTarget {
logrus.Debugf("A duplicate target '%s' found for existing TXT record '%s'", *res.Text, ep.DNSName)
} else {
logrus.Debugf("Adding target '%s' to existing TXT record '%s'", *res.Text, *res.Name)
ep.Targets = append(ep.Targets, *res.Text)
}
break
}
}
if !foundExisting {
logrus.Debugf("Record='%s' TXT:'%s'", *res.Name, *res.Text)
newEndpoint := endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeTXT, *res.Text)
endpoints = append(endpoints, newEndpoint)
}
}
}
// 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 *ProviderConfig) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
// Update user specified TTL (0 == disabled)
for i := range endpoints {
endpoints[i].RecordTTL = endpoint.TTL(p.cacheDuration)
}
if !p.createPTR {
return endpoints, nil
}
// 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, nil
}
// ApplyChanges applies the given changes.
func (p *ProviderConfig) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.zones()
if err != nil {
return err
}
created, deleted := p.mapChanges(zones, changes)
p.deleteRecords(deleted)
p.createRecords(created)
return nil
}
func (p *ProviderConfig) zones() ([]ibclient.ZoneAuth, error) {
var res, result []ibclient.ZoneAuth
obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
queryParams := recordQueryParams("", p.view)
err := p.client.GetObject(obj, "", queryParams, &res)
if err != nil && !isNotFoundError(err) {
return nil, err
}
for _, zone := range res {
if !p.domainFilter.Match(zone.Fqdn) {
continue
}
if !p.zoneIDFilter.Match(zone.Ref) {
continue
}
result = append(result, zone)
}
return result, nil
}
type infobloxChangeMap map[string][]*endpoint.Endpoint
func (p *ProviderConfig) mapChanges(zones []ibclient.ZoneAuth, changes *plan.Changes) (infobloxChangeMap, infobloxChangeMap) {
created := infobloxChangeMap{}
deleted := infobloxChangeMap{}
mapChange := func(changeMap infobloxChangeMap, change *endpoint.Endpoint) {
zone := p.findZone(zones, change.DNSName)
if zone == nil {
logrus.Debugf("Ignoring changes to '%s' because a suitable Infoblox DNS zone was not found.", change.DNSName)
return
}
// 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 {
mapChange(deleted, change)
}
for _, change := range changes.UpdateOld {
mapChange(deleted, change)
}
for _, change := range changes.Create {
mapChange(created, change)
}
for _, change := range changes.UpdateNew {
mapChange(created, change)
}
return created, deleted
}
func (p *ProviderConfig) findZone(zones []ibclient.ZoneAuth, name string) *ibclient.ZoneAuth {
var result *ibclient.ZoneAuth
// Go through every zone looking for the longest name (i.e. most specific) as a matching suffix
for idx := range zones {
zone := &zones[idx]
if strings.HasSuffix(name, "."+zone.Fqdn) {
if result == nil || len(zone.Fqdn) > len(result.Fqdn) {
result = zone
}
} else if strings.EqualFold(name, zone.Fqdn) {
if result == nil || len(zone.Fqdn) > len(result.Fqdn) {
result = zone
}
}
}
return result
}
func (p *ProviderConfig) findReverseZone(zones []ibclient.ZoneAuth, name string) *ibclient.ZoneAuth {
ip := net.ParseIP(name)
networks := map[int]*ibclient.ZoneAuth{}
maxMask := 0
for i, zone := range zones {
_, rZoneNet, err := net.ParseCIDR(zone.Fqdn)
if err != nil {
logrus.WithError(err).Debugf("fqdn %s is no cidr", zone.Fqdn)
} else {
if rZoneNet.Contains(ip) {
_, mask := rZoneNet.Mask.Size()
networks[mask] = &zones[i]
if mask > maxMask {
maxMask = mask
}
}
}
}
return networks[maxMask]
}
func (p *ProviderConfig) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) {
switch ep.RecordType {
case endpoint.RecordTypeA:
var res []ibclient.RecordA
obj := ibclient.NewEmptyRecordA()
obj.Name = &ep.DNSName
obj.Ipv4Addr = &ep.Targets[targetIndex]
obj.View = p.view
if getObject {
queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
err = p.client.GetObject(obj, "", queryParams, &res)
if err != nil && !isNotFoundError(err) {
return
}
}
recordSet = infobloxRecordSet{
obj: obj,
res: &res,
}
case endpoint.RecordTypePTR:
var res []ibclient.RecordPTR
obj := ibclient.NewEmptyRecordPTR()
obj.PtrdName = &ep.DNSName
obj.Ipv4Addr = &ep.Targets[targetIndex]
obj.View = p.view
if getObject {
queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.PtrdName})
err = p.client.GetObject(obj, "", queryParams, &res)
if err != nil && !isNotFoundError(err) {
return
}
}
recordSet = infobloxRecordSet{
obj: obj,
res: &res,
}
case endpoint.RecordTypeCNAME:
var res []ibclient.RecordCNAME
obj := ibclient.NewEmptyRecordCNAME()
obj.Name = &ep.DNSName
obj.Canonical = &ep.Targets[0]
obj.View = &p.view
if getObject {
queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
err = p.client.GetObject(obj, "", queryParams, &res)
if err != nil && !isNotFoundError(err) {
return
}
}
recordSet = infobloxRecordSet{
obj: obj,
res: &res,
}
case endpoint.RecordTypeTXT:
var res []ibclient.RecordTXT
// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
// Here we reconcile that fact by making this state match that reality.
if target, err2 := strconv.Unquote(ep.Targets[0]); err2 == nil && !strings.Contains(ep.Targets[0], " ") {
ep.Targets = endpoint.Targets{target}
}
obj := ibclient.NewEmptyRecordTXT()
obj.Name = &ep.DNSName
obj.Text = &ep.Targets[0]
obj.View = &p.view
if getObject {
queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
err = p.client.GetObject(obj, "", queryParams, &res)
if err != nil && !isNotFoundError(err) {
return
}
}
recordSet = infobloxRecordSet{
obj: obj,
res: &res,
}
}
return
}
func (p *ProviderConfig) createRecords(created infobloxChangeMap) {
for zone, endpoints := range created {
for _, ep := range endpoints {
for targetIndex := range ep.Targets {
if p.dryRun {
logrus.Infof(
"Would create %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
)
continue
}
logrus.Infof(
"Creating %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
)
recordSet, err := p.recordSet(ep, false, targetIndex)
if err != nil && !isNotFoundError(err) {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
_, err = p.client.CreateObject(recordSet.obj)
if err != nil {
logrus.Errorf(
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
}
}
}
}
}
func (p *ProviderConfig) deleteRecords(deleted infobloxChangeMap) {
// Delete records first
for zone, endpoints := range deleted {
for _, ep := range endpoints {
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, true, targetIndex)
if err != nil && !isNotFoundError(err) {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%p' to '%p' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
} else {
logrus.Infof("Deleting %s record named '%p' to '%p' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypePTR:
for _, record := range *recordSet.res.(*[]ibclient.RecordPTR) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", *record.PtrdName, *record.Ipv4Addr, record.Zone)
} else {
logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", *record.PtrdName, *record.Ipv4Addr, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", *record.Name, *record.Canonical, record.Zone)
} else {
logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", *record.Name, *record.Canonical, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
if p.dryRun {
logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", *record.Name, *record.Text, record.Zone)
} else {
logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", *record.Name, *record.Text, record.Zone)
_, err = p.client.DeleteObject(record.Ref)
}
}
}
if err != nil && !isNotFoundError(err) {
logrus.Errorf(
"Failed to delete %s record named '%s' to '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
}
}
}
}
}
func lookupEnvAtoi(key string, fallback int) (i int) {
val, ok := os.LookupEnv(key)
if !ok {
i = fallback
return
}
i, err := strconv.Atoi(val)
if err != nil {
i = fallback
return
}
return
}

View File

@ -1,939 +0,0 @@
/*
Copyright 2017 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 infoblox
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"testing"
ibclient "github.com/infobloxopen/infoblox-go-client/v2"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type mockIBConnector struct {
mockInfobloxZones *[]ibclient.ZoneAuth
mockInfobloxObjects *[]ibclient.IBObject
createdEndpoints []*endpoint.Endpoint
deletedEndpoints []*endpoint.Endpoint
updatedEndpoints []*endpoint.Endpoint
getObjectRequests []*getObjectRequest
requestBuilder ExtendedRequestBuilder
}
type getObjectRequest struct {
obj string
ref string
queryParams string
url url.URL
verified bool
}
func (req *getObjectRequest) ExpectRequestURLQueryParam(t *testing.T, name string, value string) *getObjectRequest {
if req.url.Query().Get(name) != value {
t.Errorf("Expected GetObject Request URL to contain query parameter %s=%s, Got: %v", name, value, req.url.Query())
}
return req
}
func (req *getObjectRequest) ExpectNotRequestURLQueryParam(t *testing.T, name string) *getObjectRequest {
if req.url.Query().Has(name) {
t.Errorf("Expected GetObject Request URL not to contain query parameter %s, Got: %v", name, req.url.Query())
}
return req
}
func (client *mockIBConnector) verifyGetObjectRequest(t *testing.T, obj string, ref string, query *map[string]string) *getObjectRequest {
qp := ""
if query != nil {
qp = fmt.Sprint(ibclient.NewQueryParams(false, *query))
}
for _, req := range client.getObjectRequests {
if !req.verified && req.obj == obj && req.ref == ref && req.queryParams == qp {
req.verified = true
return req
}
}
t.Errorf("Expected GetObject obj=%s, query=%s, ref=%s", obj, qp, ref)
return &getObjectRequest{}
}
// verifyNoMoreGetObjectRequests will assert that all "GetObject" calls have been verified.
func (client *mockIBConnector) verifyNoMoreGetObjectRequests(t *testing.T) {
unverified := []getObjectRequest{}
for _, req := range client.getObjectRequests {
if !req.verified {
unverified = append(unverified, *req)
}
}
if len(unverified) > 0 {
b := new(bytes.Buffer)
for _, req := range unverified {
fmt.Fprintf(b, "obj=%s, ref=%s, params=%s (url=%s)\n", req.obj, req.ref, req.queryParams, req.url.String())
}
t.Errorf("Unverified GetObject Requests: %v", unverified)
}
}
func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string, err error) {
switch obj.ObjectType() {
case "record:a":
client.createdEndpoints = append(
client.createdEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordA).Name,
endpoint.RecordTypeA,
*obj.(*ibclient.RecordA).Ipv4Addr,
),
)
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(*obj.(*ibclient.RecordA).Name)), *obj.(*ibclient.RecordA).Name)
obj.(*ibclient.RecordA).Ref = ref
case "record:cname":
client.createdEndpoints = append(
client.createdEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordCNAME).Name,
endpoint.RecordTypeCNAME,
*obj.(*ibclient.RecordCNAME).Canonical,
),
)
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(*obj.(*ibclient.RecordCNAME).Name)), *obj.(*ibclient.RecordCNAME).Name)
obj.(*ibclient.RecordCNAME).Ref = ref
case "record:host":
for _, i := range obj.(*ibclient.HostRecord).Ipv4Addrs {
client.createdEndpoints = append(
client.createdEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.HostRecord).Name,
endpoint.RecordTypeA,
*i.Ipv4Addr,
),
)
}
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(*obj.(*ibclient.HostRecord).Name)), *obj.(*ibclient.HostRecord).Name)
obj.(*ibclient.HostRecord).Ref = ref
case "record:txt":
client.createdEndpoints = append(
client.createdEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordTXT).Name,
endpoint.RecordTypeTXT,
*obj.(*ibclient.RecordTXT).Text,
),
)
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":
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,
obj,
)
return ref, nil
}
func (client *mockIBConnector) GetObject(obj ibclient.IBObject, ref string, queryParams *ibclient.QueryParams, res interface{}) (err error) {
req := getObjectRequest{
obj: obj.ObjectType(),
ref: ref,
}
if queryParams != nil {
req.queryParams = fmt.Sprint(queryParams)
}
r, _ := client.requestBuilder.BuildRequest(ibclient.GET, obj, ref, queryParams)
if r != nil {
req.url = *r.URL
}
client.getObjectRequests = append(client.getObjectRequests, &req)
switch obj.ObjectType() {
case "record:a":
var result []ibclient.RecordA
for _, object := range *client.mockInfobloxObjects {
if object.ObjectType() == "record:a" {
if ref != "" &&
ref != object.(*ibclient.RecordA).Ref {
continue
}
if obj.(*ibclient.RecordA).Name != nil &&
*obj.(*ibclient.RecordA).Name != *object.(*ibclient.RecordA).Name {
continue
}
result = append(result, *object.(*ibclient.RecordA))
}
}
*res.(*[]ibclient.RecordA) = result
case "record:cname":
var result []ibclient.RecordCNAME
for _, object := range *client.mockInfobloxObjects {
if object.ObjectType() == "record:cname" {
if ref != "" &&
ref != object.(*ibclient.RecordCNAME).Ref {
continue
}
if obj.(*ibclient.RecordCNAME).Name != nil &&
*obj.(*ibclient.RecordCNAME).Name != *object.(*ibclient.RecordCNAME).Name {
continue
}
result = append(result, *object.(*ibclient.RecordCNAME))
}
}
*res.(*[]ibclient.RecordCNAME) = result
case "record:host":
var result []ibclient.HostRecord
for _, object := range *client.mockInfobloxObjects {
if object.ObjectType() == "record:host" {
if ref != "" &&
ref != object.(*ibclient.HostRecord).Ref {
continue
}
if obj.(*ibclient.HostRecord).Name != nil &&
*obj.(*ibclient.HostRecord).Name != *object.(*ibclient.HostRecord).Name {
continue
}
result = append(result, *object.(*ibclient.HostRecord))
}
}
*res.(*[]ibclient.HostRecord) = result
case "record:txt":
var result []ibclient.RecordTXT
for _, object := range *client.mockInfobloxObjects {
if object.ObjectType() == "record:txt" {
if ref != "" &&
ref != object.(*ibclient.RecordTXT).Ref {
continue
}
if obj.(*ibclient.RecordTXT).Name != nil &&
*obj.(*ibclient.RecordTXT).Name != *object.(*ibclient.RecordTXT).Name {
continue
}
result = append(result, *object.(*ibclient.RecordTXT))
}
}
*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 != nil &&
*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
}
return
}
func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err error) {
re := regexp.MustCompile(`([^/]+)/[^:]+:([^/]+)/default`)
result := re.FindStringSubmatch(ref)
switch result[1] {
case "record:a":
var records []ibclient.RecordA
obj := ibclient.NewEmptyRecordA()
obj.Name = &result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
client.deletedEndpoints,
endpoint.NewEndpoint(
*record.Name,
endpoint.RecordTypeA,
"",
),
)
}
case "record:cname":
var records []ibclient.RecordCNAME
obj := ibclient.NewEmptyRecordCNAME()
obj.Name = &result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
client.deletedEndpoints,
endpoint.NewEndpoint(
*record.Name,
endpoint.RecordTypeCNAME,
"",
),
)
}
case "record:host":
var records []ibclient.HostRecord
obj := ibclient.NewEmptyHostRecord()
obj.Name = &result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
client.deletedEndpoints,
endpoint.NewEndpoint(
*record.Name,
endpoint.RecordTypeA,
"",
),
)
}
case "record:txt":
var records []ibclient.RecordTXT
obj := ibclient.NewEmptyRecordTXT()
obj.Name = &result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
client.deletedEndpoints,
endpoint.NewEndpoint(
*record.Name,
endpoint.RecordTypeTXT,
"",
),
)
}
case "record:ptr":
var records []ibclient.RecordPTR
obj := ibclient.NewEmptyRecordPTR()
obj.Name = &result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
client.deletedEndpoints,
endpoint.NewEndpoint(
*record.PtrdName,
endpoint.RecordTypePTR,
"",
),
)
}
}
return "", nil
}
func (client *mockIBConnector) UpdateObject(obj ibclient.IBObject, ref string) (refRes string, err error) {
switch obj.ObjectType() {
case "record:a":
client.updatedEndpoints = append(
client.updatedEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordA).Name,
*obj.(*ibclient.RecordA).Ipv4Addr,
endpoint.RecordTypeA,
),
)
case "record:cname":
client.updatedEndpoints = append(
client.updatedEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordCNAME).Name,
*obj.(*ibclient.RecordCNAME).Canonical,
endpoint.RecordTypeCNAME,
),
)
case "record:host":
for _, i := range obj.(*ibclient.HostRecord).Ipv4Addrs {
client.updatedEndpoints = append(
client.updatedEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.HostRecord).Name,
*i.Ipv4Addr,
endpoint.RecordTypeA,
),
)
}
case "record:txt":
client.updatedEndpoints = append(
client.updatedEndpoints,
endpoint.NewEndpoint(
*obj.(*ibclient.RecordTXT).Name,
*obj.(*ibclient.RecordTXT).Text,
endpoint.RecordTypeTXT,
),
)
}
return "", nil
}
func createMockInfobloxZone(fqdn string) ibclient.ZoneAuth {
return ibclient.ZoneAuth{
Fqdn: fqdn,
}
}
func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject {
ref := fmt.Sprintf("record:%s/%s:%s/default", strings.ToLower(recordType), base64.StdEncoding.EncodeToString([]byte(name)), name)
switch recordType {
case endpoint.RecordTypeA:
obj := ibclient.NewEmptyRecordA()
obj.Name = &name
obj.Ref = ref
obj.Ipv4Addr = &value
return obj
case endpoint.RecordTypeCNAME:
obj := ibclient.NewEmptyRecordCNAME()
obj.Name = &name
obj.Ref = ref
obj.Canonical = &value
return obj
case endpoint.RecordTypeTXT:
obj := ibclient.NewEmptyRecordTXT()
obj.Name = &name
obj.Ref = ref
obj.Text = &value
return obj
case "HOST":
obj := ibclient.NewEmptyHostRecord()
obj.Name = &name
obj.Ref = ref
obj.Ipv4Addrs = []ibclient.HostRecordIpv4Addr{
{
Ipv4Addr: &value,
},
}
return obj
case endpoint.RecordTypePTR:
obj := ibclient.NewEmptyRecordPTR()
obj.PtrdName = &name
obj.Ref = ref
obj.Ipv4Addr = &value
return obj
}
return nil
}
func newInfobloxProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, view string, dryRun bool, createPTR bool, client ibclient.IBConnector) *ProviderConfig {
return &ProviderConfig{
client: client,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
createPTR: createPTR,
view: view,
}
}
func TestInfobloxRecords(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("nginx.example.com", endpoint.RecordTypeA, "123.123.123.123"),
createMockInfobloxObject("nginx.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=white space"),
createMockInfobloxObject("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.121"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.1"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.2"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=existing"),
createMockInfobloxObject("host.example.com", "HOST", "125.1.1.1"),
},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "", true, false, &client)
actual, err := providerCfg.Records(context.Background())
if err != nil {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("nginx.example.com", endpoint.RecordTypeA, "123.123.123.123"),
endpoint.NewEndpoint("nginx.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=white space\""),
endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122", "123.123.123.121"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeA, "124.1.1.1", "124.1.1.2"),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=existing\""),
endpoint.NewEndpoint("host.example.com", endpoint.RecordTypeA, "125.1.1.1"),
}
validateEndpoints(t, actual, expected)
client.verifyGetObjectRequest(t, "zone_auth", "", &map[string]string{}).
ExpectNotRequestURLQueryParam(t, "view").
ExpectNotRequestURLQueryParam(t, "zone")
client.verifyGetObjectRequest(t, "record:a", "", &map[string]string{"zone": "example.com"}).
ExpectRequestURLQueryParam(t, "zone", "example.com")
client.verifyGetObjectRequest(t, "record:host", "", &map[string]string{"zone": "example.com"}).
ExpectRequestURLQueryParam(t, "zone", "example.com")
client.verifyGetObjectRequest(t, "record:cname", "", &map[string]string{"zone": "example.com"}).
ExpectRequestURLQueryParam(t, "zone", "example.com")
client.verifyGetObjectRequest(t, "record:txt", "", &map[string]string{"zone": "example.com"}).
ExpectRequestURLQueryParam(t, "zone", "example.com")
client.verifyNoMoreGetObjectRequests(t)
}
func TestInfobloxRecordsWithView(t *testing.T) {
client := mockIBConnector{
mockInfobloxZones: &[]ibclient.ZoneAuth{
createMockInfobloxZone("foo.example.com"),
createMockInfobloxZone("bar.example.com"),
},
mockInfobloxObjects: &[]ibclient.IBObject{
createMockInfobloxObject("cat.foo.example.com", endpoint.RecordTypeA, "123.123.123.122"),
createMockInfobloxObject("dog.bar.example.com", endpoint.RecordTypeA, "123.123.123.123"),
},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"foo.example.com", "bar.example.com"}), provider.NewZoneIDFilter([]string{""}), "Inside", true, false, &client)
actual, err := providerCfg.Records(context.Background())
if err != nil {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpoint("cat.foo.example.com", endpoint.RecordTypeA, "123.123.123.122"),
endpoint.NewEndpoint("dog.bar.example.com", endpoint.RecordTypeA, "123.123.123.123"),
}
validateEndpoints(t, actual, expected)
client.verifyGetObjectRequest(t, "zone_auth", "", &map[string]string{"view": "Inside"}).
ExpectRequestURLQueryParam(t, "view", "Inside").
ExpectNotRequestURLQueryParam(t, "zone")
client.verifyGetObjectRequest(t, "record:a", "", &map[string]string{"zone": "foo.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "foo.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:host", "", &map[string]string{"zone": "foo.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "foo.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:cname", "", &map[string]string{"zone": "foo.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "foo.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:txt", "", &map[string]string{"zone": "foo.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "foo.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:a", "", &map[string]string{"zone": "bar.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "bar.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:host", "", &map[string]string{"zone": "bar.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "bar.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:cname", "", &map[string]string{"zone": "bar.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "bar.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyGetObjectRequest(t, "record:txt", "", &map[string]string{"zone": "bar.example.com", "view": "Inside"}).
ExpectRequestURLQueryParam(t, "zone", "bar.example.com").
ExpectRequestURLQueryParam(t, "view", "Inside")
client.verifyNoMoreGetObjectRequests(t)
}
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"),
},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "", true, true, &client)
actual, err := providerCfg.Records(context.Background())
if err != nil {
t.Fatal(err)
}
providerCfg.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"),
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"),
},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"10.0.0.0/24"}), provider.NewZoneIDFilter([]string{""}), "", true, true, &client)
actual, err := providerCfg.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, false, &client)
validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "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.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("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, ""),
})
validateEndpoints(t, client.updatedEndpoints, []*endpoint.Endpoint{})
}
func TestInfobloxApplyChangesDryRun(t *testing.T) {
client := mockIBConnector{
mockInfobloxObjects: &[]ibclient.IBObject{},
}
testInfobloxApplyChangesInternal(t, true, false, &client)
validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{})
validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{})
validateEndpoints(t, client.updatedEndpoints, []*endpoint.Endpoint{})
}
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"),
}
providerCfg := newInfobloxProvider(
endpoint.NewDomainFilter([]string{""}),
provider.NewZoneIDFilter([]string{""}),
"",
dryRun,
createPTR,
client,
)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "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.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("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
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"),
}
updateOldRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"),
}
updateNewRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
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,
UpdateOld: updateOldRecords,
Delete: deleteRecords,
}
if err := providerCfg.ApplyChanges(context.Background(), changes); err != nil {
t.Fatal(err)
}
}
func TestInfobloxZones(t *testing.T) {
client := mockIBConnector{
mockInfobloxZones: &[]ibclient.ZoneAuth{
createMockInfobloxZone("example.com"),
createMockInfobloxZone("lvl1-1.example.com"),
createMockInfobloxZone("lvl2-1.lvl1-1.example.com"),
createMockInfobloxZone("1.2.3.0/24"),
},
mockInfobloxObjects: &[]ibclient.IBObject{},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com", "1.2.3.0/24"}), provider.NewZoneIDFilter([]string{""}), "", true, false, &client)
zones, _ := providerCfg.zones()
var emptyZoneAuth *ibclient.ZoneAuth
assert.Equal(t, providerCfg.findZone(zones, "example.com").Fqdn, "example.com")
assert.Equal(t, providerCfg.findZone(zones, "nomatch-example.com"), emptyZoneAuth)
assert.Equal(t, providerCfg.findZone(zones, "nginx.example.com").Fqdn, "example.com")
assert.Equal(t, providerCfg.findZone(zones, "lvl1-1.example.com").Fqdn, "lvl1-1.example.com")
assert.Equal(t, providerCfg.findZone(zones, "lvl1-2.example.com").Fqdn, "example.com")
assert.Equal(t, providerCfg.findZone(zones, "lvl2-1.lvl1-1.example.com").Fqdn, "lvl2-1.lvl1-1.example.com")
assert.Equal(t, providerCfg.findZone(zones, "lvl2-2.lvl1-1.example.com").Fqdn, "lvl1-1.example.com")
assert.Equal(t, providerCfg.findZone(zones, "lvl2-2.lvl1-2.example.com").Fqdn, "example.com")
assert.Equal(t, providerCfg.findZone(zones, "1.2.3.0/24").Fqdn, "1.2.3.0/24")
}
func TestInfobloxReverseZones(t *testing.T) {
client := mockIBConnector{
mockInfobloxZones: &[]ibclient.ZoneAuth{
createMockInfobloxZone("example.com"),
createMockInfobloxZone("1.2.3.0/24"),
createMockInfobloxZone("10.0.0.0/8"),
},
mockInfobloxObjects: &[]ibclient.IBObject{},
}
providerCfg := newInfobloxProvider(endpoint.NewDomainFilter([]string{"example.com", "1.2.3.0/24", "10.0.0.0/8"}), provider.NewZoneIDFilter([]string{""}), "", true, false, &client)
zones, _ := providerCfg.zones()
var emptyZoneAuth *ibclient.ZoneAuth
assert.Equal(t, providerCfg.findReverseZone(zones, "nomatch-example.com"), emptyZoneAuth)
assert.Equal(t, providerCfg.findReverseZone(zones, "192.168.0.1"), emptyZoneAuth)
assert.Equal(t, providerCfg.findReverseZone(zones, "1.2.3.4").Fqdn, "1.2.3.0/24")
assert.Equal(t, providerCfg.findReverseZone(zones, "10.28.29.30").Fqdn, "10.0.0.0/8")
}
func TestExtendedRequestFDQDRegExBuilder(t *testing.T) {
hostCfg := ibclient.HostConfig{
Host: "localhost",
Port: "8080",
Version: "2.3.1",
}
authCfg := ibclient.AuthConfig{
Username: "user",
Password: "abcd",
}
requestBuilder := NewExtendedRequestBuilder(0, "^staging.*test.com$", "")
requestBuilder.Init(hostCfg, authCfg)
obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
req, _ := requestBuilder.BuildRequest(ibclient.GET, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("fqdn~") == "^staging.*test.com$")
req, _ = requestBuilder.BuildRequest(ibclient.CREATE, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("fqdn~") == "")
}
func TestExtendedRequestNameRegExBuilder(t *testing.T) {
hostCfg := ibclient.HostConfig{
Host: "localhost",
Port: "8080",
Version: "2.3.1",
}
authCfg := ibclient.AuthConfig{
Username: "user",
Password: "abcd",
}
requestBuilder := NewExtendedRequestBuilder(0, "", "^staging.*test.com$")
requestBuilder.Init(hostCfg, authCfg)
obj := ibclient.NewEmptyRecordCNAME()
req, _ := requestBuilder.BuildRequest(ibclient.GET, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("name~") == "^staging.*test.com$")
req, _ = requestBuilder.BuildRequest(ibclient.CREATE, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("name~") == "")
}
func TestExtendedRequestMaxResultsBuilder(t *testing.T) {
hostCfg := ibclient.HostConfig{
Host: "localhost",
Port: "8080",
Version: "2.3.1",
}
authCfg := ibclient.AuthConfig{
Username: "user",
Password: "abcd",
}
requestBuilder := NewExtendedRequestBuilder(54321, "", "")
requestBuilder.Init(hostCfg, authCfg)
obj := ibclient.NewEmptyRecordCNAME()
obj.Zone = "foo.bar.com"
req, _ := requestBuilder.BuildRequest(ibclient.GET, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("_max_results") == "54321")
req, _ = requestBuilder.BuildRequest(ibclient.CREATE, obj, "", &ibclient.QueryParams{})
assert.True(t, req.URL.Query().Get("_max_results") == "")
}
func TestGetObject(t *testing.T) {
hostCfg := ibclient.HostConfig{}
authCfg := ibclient.AuthConfig{}
transportConfig := ibclient.TransportConfig{}
requestBuilder := NewExtendedRequestBuilder(1000, "mysite.com", "")
requestor := mockRequestor{}
client, _ := ibclient.NewConnector(hostCfg, authCfg, transportConfig, requestBuilder, &requestor)
providerConfig := newInfobloxProvider(endpoint.NewDomainFilter([]string{"mysite.com"}), provider.NewZoneIDFilter([]string{""}), "", true, true, client)
providerConfig.deleteRecords(infobloxChangeMap{
"myzone.com": []*endpoint.Endpoint{
endpoint.NewEndpoint("deletethisrecord.com", endpoint.RecordTypeA, "1.2.3.4"),
},
})
requestQuery := requestor.request.URL.Query()
assert.True(t, requestQuery.Has("name"), "Expected the request to filter objects by name")
}
// Mock requestor that doesn't send request
type mockRequestor struct {
request *http.Request
}
func (r *mockRequestor) Init(ibclient.AuthConfig, ibclient.TransportConfig) {}
func (r *mockRequestor) SendRequest(req *http.Request) (res []byte, err error) {
res = []byte("[{}]")
r.request = req
return
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected)
}