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

@ -83,7 +83,6 @@ Brief summary of open PRs and what they are trying to address:
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.
Rebase on default branch and add missing pieces. Depends on `2`.

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