Merge branch 'master' into master

This commit is contained in:
Brock Alberry 2021-03-18 14:05:18 -04:00 committed by GitHub
commit f9ad22cbbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2259 additions and 125 deletions

View File

@ -27,7 +27,7 @@ COPY . .
RUN make test build.$ARCH
# final image
FROM $ARCH/alpine:3.12
FROM $ARCH/alpine:3.13
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /sigs.k8s.io/external-dns/build/external-dns /bin/external-dns

View File

@ -27,6 +27,23 @@ cover:
cover-html: cover
go tool cover -html cover.out
# find or download controller-gen
# download controller-gen if necessary
controller-gen:
ifeq (, $(shell which controller-gen))
@{ \
set -e ;\
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$CONTROLLER_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
}
CONTROLLER_GEN=$(GOBIN)/controller-gen
else
CONTROLLER_GEN=$(shell which controller-gen)
endif
.PHONY: go-lint
# Run the golangci-lint tool
@ -51,6 +68,11 @@ licensecheck:
# Run all the linters
lint: licensecheck go-lint
.PHONY: crd
# generates CRD using controller-gen
crd: controller-gen
${CONTROLLER_GEN} crd:crdVersions=v1 paths="./endpoint/..." output:crd:stdout > docs/contributing/crd-source/crd-manifest.yaml
# The verify target runs tasks similar to the CI tasks, but without code coverage
.PHONY: verify test
@ -82,7 +104,7 @@ build.push/multiarch:
for arch in $(ARCHS); do \
image="$(IMAGE):$(VERSION)-$${arch}" ;\
# pre-pull due to https://github.com/kubernetes-sigs/cluster-addons/pull/84/files ;\
docker pull $${arch}/alpine:3.12 ;\
docker pull $${arch}/alpine:3.13 ;\
DOCKER_BUILDKIT=1 docker build --rm --tag $${image} --build-arg VERSION="$(VERSION)" --build-arg ARCH="$${arch}" . ;\
docker push $${image} ;\
arch_specific_tags+=( "--amend $${image}" ) ;\

View File

@ -28,6 +28,7 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
* [AWS Route 53](https://aws.amazon.com/route53/)
* [AWS Cloud Map](https://docs.aws.amazon.com/cloud-map/)
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
* [BlueCat](https://bluecatnetworks.com)
* [CloudFlare](https://www.cloudflare.com/dns)
* [RcodeZero](https://www.rcodezero.at/)
* [DigitalOcean](https://www.digitalocean.com/products/networking)
@ -82,6 +83,7 @@ The following table clarifies the current status of the providers according to t
| AWS Cloud Map | Beta | |
| Akamai Edge DNS | Beta | |
| AzureDNS | Beta | |
| BlueCat | Alpha | @seanmalloy @vinny-sabatini |
| CloudFlare | Beta | |
| RcodeZero | Alpha | |
| DigitalOcean | Alpha | |

View File

@ -1,62 +1,94 @@
apiVersion: apiextensions.k8s.io/v1beta1
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
api-approved.kubernetes.io: "https://github.com/kubernetes-sigs/external-dns/pull/2007"
creationTimestamp: null
labels:
api: externaldns
kubebuilder.k8s.io: 1.0.0
name: dnsendpoints.externaldns.k8s.io
spec:
group: externaldns.k8s.io
names:
kind: DNSEndpoint
listKind: DNSEndpointList
plural: dnsendpoints
singular: dnsendpoint
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
endpoints:
items:
properties:
dnsName:
type: string
labels:
type: object
providerSpecific:
items:
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
format: int64
type: integer
recordType:
type: string
targets:
items:
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: DNSEndpointSpec defines the desired state of DNSEndpoint
properties:
endpoints:
items:
description: Endpoint is a high-level way of a connection between a service and an IP
properties:
dnsName:
description: The hostname of the DNS record
type: string
type: array
type: object
type: array
type: object
status:
properties:
observedGeneration:
format: int64
type: integer
type: object
version: v1alpha1
labels:
additionalProperties:
type: string
description: Labels stores labels defined for the Endpoint
type: object
providerSpecific:
description: ProviderSpecific stores provider specific config
items:
description: ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
description: TTL for the record
format: int64
type: integer
recordType:
description: RecordType type of record, e.g. CNAME, A, SRV, TXT etc
type: string
setIdentifier:
description: Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple')
type: string
targets:
description: The targets the DNS record points to
items:
type: string
type: array
type: object
type: array
type: object
status:
description: DNSEndpointStatus defines the observed state of DNSEndpoint
properties:
observedGeneration:
description: The generation observed by the external-dns controller.
format: int64
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -28,24 +28,7 @@ ExternalDNS can solve this for you as well.
### Which DNS providers are supported?
Currently, the following providers are supported:
- Google Cloud DNS
- AWS Route 53
- AzureDNS
- CloudFlare
- DigitalOcean
- DNSimple
- Infoblox
- Dyn
- OpenStack Designate
- PowerDNS
- CoreDNS
- Exoscale
- Oracle Cloud Infrastructure DNS
- Linode DNS
- RFC2136
- TransIP
Please check the [provider status table](https://github.com/kubernetes-sigs/external-dns#status-of-providers) for the list of supported providers and their status.
As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.

View File

@ -229,7 +229,7 @@ Create an ingress resource manifest file.
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: foo

View File

@ -253,7 +253,7 @@ Create an ingress resource manifest file.
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: foo

View File

@ -375,7 +375,7 @@ spec:
type: ClusterIP
---
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -392,7 +392,7 @@ spec:
type: ClusterIP
---
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

64
docs/tutorials/bluecat.md Normal file
View File

@ -0,0 +1,64 @@
# Setting up external-dns for BlueCat
## Prerequisites
Install the BlueCat Gateway product and deploy the [community gateway workflows](https://github.com/bluecatlabs/gateway-workflows).
## Deploy
Setup configuration file as k8s `Secret`.
```
cat << EOF > ~/bluecat.json
{
"gatewayHost": "https://bluecatgw.example.com",
"gatewayUsername": "user",
"GatewayPassword": "pass",
"dnsConfiguration": "Example",
"dnsView": "Internal",
"rootZone": "example.com"
}
EOF
kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example
```
Setup up deployment/service account:
```
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: bluecat-example
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: bluecat-example
spec:
selector:
matchLabels:
app: external-dns
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
volumes:
- name: bluecatconfig
secret:
secretName: bluecatconfig
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:$TAG # no released versions include the bluecat provider yet
volumeMounts:
- name: bluecatconfig
mountPath: "/etc/external-dns/"
readOnly: true
args:
- --log-level=debug
- --source=service
- --provider=bluecat
- --txt-owner-id=bluecat-example
- --bluecat-config-file=/etc/external-dns/bluecat.json
```

View File

@ -194,7 +194,7 @@ minikube addons enable ingress
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -111,7 +111,7 @@ Having `--dry-run=true` and `--log-level=debug` is a great way to see _exactly_
Create a file called 'test-ingress.yaml' with the following contents:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress

View File

@ -104,7 +104,7 @@ subjects:
Spin up a simple nginx HTTP server with the following spec (`kubectl apply -f`):
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -211,7 +211,7 @@ $ curl nginx.external-dns-test.gcp.zalan.do
Let's check that Ingress works as well. Create the following Ingress.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
@ -460,7 +460,7 @@ $ kubectl annotate serviceaccount --namespace=external-dns external-dns \
Create the following sample application to test that ExternalDNS works.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -0,0 +1,101 @@
# Configuring ExternalDNS to use the Gloo Proxy Source
This tutorial describes how to configure ExternalDNS to use the Gloo Proxy source.
It is meant to supplement the other provider-specific setup tutorials.
### 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
# update this to the desired external-dns version
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
args:
- --source=gloo-proxy
- --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
- --provider=aws
- --registry=txt
- --txt-owner-id=my-identifier
```
### Manifest (for clusters with RBAC enabled)
Could be change if you have mulitple sources
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
- apiGroups: ["gloo.solo.io"]
resources: ["proxies"]
verbs: ["get","watch","list"]
- apiGroups: ["gateway.solo.io"]
resources: ["virtualservices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
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
# update this to the desired external-dns version
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
args:
- --source=gloo-proxy
- --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
- --provider=aws
- --registry=txt
- --txt-owner-id=my-identifier
```

View File

@ -138,7 +138,7 @@ default.
Create the following Ingress to expose the echoserver application to the Internet.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -172,7 +172,7 @@ this Ingress object will only be fronting one backend Service, we might instead
create the following:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -205,7 +205,7 @@ and one AAAA record) for each hostname associated with the Ingress object.
Example:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -239,7 +239,7 @@ set to `nlb` then ExternalDNS will create an NLB instead of an ALB.
Example:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:

View File

@ -290,7 +290,7 @@ Use `--dry-run` if you want to be extra careful on the first run. Note, that you
Create the following sample application to test that ExternalDNS works.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
@ -586,7 +586,7 @@ $ kubectl annotate serviceaccount --namespace=external-dns external-dns \
Create the following sample application to test that ExternalDNS works.
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -292,7 +292,7 @@ For this setup to work, you've to create two Service definitions for your applic
At first, create public Service definition:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -313,7 +313,7 @@ spec:
Then create private Service definition:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -334,7 +334,7 @@ spec:
Additionally, you may leverage [cert-manager](https://github.com/jetstack/cert-manager) to automatically issue SSL certificates from [Let's Encrypt](https://letsencrypt.org/). To do that, request a certificate in public service definition:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
@ -363,7 +363,7 @@ spec:
And reuse the requested certificate in private Service definition:
```yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:

View File

@ -138,7 +138,7 @@ spec:
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx

View File

@ -94,7 +94,7 @@ spec:
selector:
app: nginx
---
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
@ -287,7 +287,7 @@ While `external-dns` was not developed or tested against Microsoft DNS, it can b
1. Create a DNS zone
2. Enable insecure dynamic updates for the zone
3. Enable Zone Transfers from all servers
3. Enable Zone Transfers to all servers
#### `external-dns` configuration
@ -310,8 +310,10 @@ You'll want to configure `external-dns` similarly to the following:
1. Create a DNS zone
2. Enable secure dynamic updates for the zone
3. Enable Zone Transfers from all servers
3. Enable Zone Transfers to all servers
If you see any error messages which indicate that `external-dns` was somehow not able to fetch
existing DNS records from your DNS server, this could mean that you forgot about step 3.
#### Kerberos Configuration
@ -339,18 +341,20 @@ data:
pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
default_ccache_name = KEYRING:persistent:%{uid}
default_realm = YOURDOMAIN.COM
default_realm = YOUR-REALM.COM
[realms]
YOURDOMAIN.COM = {
YOUR-REALM.COM = {
kdc = dc1.yourdomain.com
admin_server = dc1.yourdomain.com
}
[domain_realm]
yourdomain.com = YOURDOMAIN.COM
.yourdomain.com = YOURDOMAIN.COM
yourdomain.com = YOUR-REALM.COM
.yourdomain.com = YOUR-REALM.COM
```
In most cases, the realm name will probably be the same as the domain name, so you can simply replace
`YOUR-REALM.COM` with something like `YOURDOMAIN.COM`.
Once the ConfigMap is created, the container `external-dns` container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following:
@ -376,8 +380,7 @@ You'll want to configure `external-dns` similarly to the following:
```text
...
- --provider=rfc2136
- --rfc2136-gss-tsig
- --rfc2136-host=123.123.123.123
- --rfc2136-host=dns-host.yourdomain.com
- --rfc2136-port=53
- --rfc2136-zone=your-zone.com
- --rfc2136-kerberos-username=your-domain-account
@ -386,3 +389,12 @@ You'll want to configure `external-dns` similarly to the following:
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...
```
As noted above, the `--rfc2136-kerberos-realm` flag is completely optional and won't be necessary in many cases.
Most likely, you will only need it if you see errors similar to this: `KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use`.
The flag `--rfc2136-host` can be set to the host's domain name or IP address.
However, it also determines the name of the Kerberos principal which is used during authentication.
This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this:
`KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database`.
To fix this, try setting `--rfc2136-host` to the "actual" hostname of your DNS server.

View File

@ -213,8 +213,12 @@ type DNSEndpointStatus struct {
// DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns.
// The user-specified CRD should also have the status sub-resource.
// +k8s:openapi-gen=true
// +groupName=externaldns.k8s.io
// +kubebuilder:resource:path=dnsendpoints
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +versionName=v1alpha1
type DNSEndpoint struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@ -223,6 +227,7 @@ type DNSEndpoint struct {
Status DNSEndpointStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// DNSEndpointList is a list of DNSEndpoint objects
type DNSEndpointList struct {
metav1.TypeMeta `json:",inline"`

4
go.mod
View File

@ -64,8 +64,8 @@ require (
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.3.0
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
istio.io/api v0.0.0-20200529165953-72dad51d4ffc
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
istio.io/api v0.0.0-20210128181506-0c4b8e54850f
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02
k8s.io/api v0.18.8
k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.8

24
go.sum
View File

@ -92,6 +92,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357 h1:3ynCSeUh9OtJLd/OzLapM1DLDv2g+0yyDdkLqSfZCaQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
@ -127,8 +128,6 @@ github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bodgit/tsig v0.0.2 h1:seNt23SrPW8dkWoyRYzdeuqFEzr+lDc0dAJvo94xB8U=
github.com/bodgit/tsig v0.0.2/go.mod h1:0mYe0t9it36SOvDQyeFekc7bLtvljFz7H9vHS/nYbgc=
github.com/bodgit/tsig v1.1.1 h1:SViReRa8KyaweqdJ3ojdYqIE3xDyJlR3G+6wAsSbLCo=
github.com/bodgit/tsig v1.1.1/go.mod h1:8LZ3Mn7AVZHH8GN2ArvzB7msHfLjoptWsdPEJRSw/uo=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
@ -427,7 +426,9 @@ github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -510,8 +511,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.1/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZ
github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/copier v0.1.0 h1:Vh8xALtH3rrKGB/XIRe5d0yCTHPZFauWPLvdpDAbi88=
github.com/jinzhu/copier v0.1.0/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -599,8 +598,6 @@ github.com/maxatome/go-testdeep v1.4.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ys
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1 h1:kZZmnTeY2r+88mDNCVV/uCXL2gG3rkVPTN9jcYfGQcI=
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@ -690,6 +687,7 @@ github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae/go.mod h1:l6TGeqJ92D
github.com/openshift/build-machinery-go v0.0.0-20200424080330-082bf86082cc/go.mod h1:1CkcsT3aVebzRBzVTSbiKSkJMsC/CASqxesfqEMfJEc=
github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73 h1:JePLt9EpNLF/30KsSsArrzxGWPaUIvYUt8Fwnw9wlgM=
github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73/go.mod h1:+66gk3dEqw9e+WoiXjJFzWlS1KGhj9ZRHi/RI/YG/ZM=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@ -948,8 +946,6 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1062,7 +1058,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1073,9 +1068,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1225,10 +1217,10 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
istio.io/api v0.0.0-20200529165953-72dad51d4ffc h1:cR9GmbIBAz3FnY3tgs1SRn/uiznhtvG+mZBfD1p2vIA=
istio.io/api v0.0.0-20200529165953-72dad51d4ffc/go.mod h1:kyq3g5w42zl/AKlbzDGppYpGMQYMYMyZKeq0/eexML8=
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751 h1:yH62fTmV+5l1XVTWcomsc1jjH/oH9u/tTgn5NVmdIac=
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751/go.mod h1:4SGvmmus5HNFdqQsIL+uQO1PbAhjQKtSjMTqwsvYHlg=
istio.io/api v0.0.0-20210128181506-0c4b8e54850f h1:zUFsawgPj5oI9p5cf91YCExRlxLIVsEkIunN9ODUSJs=
istio.io/api v0.0.0-20210128181506-0c4b8e54850f/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64=
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 h1:ZA8Y2gKkKtEeYuKfqlEzIBDfU4IE5uIAdsXDeD41T9w=
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02/go.mod h1:oXMjFUWhxlReUSbg4i3GjKgOhSX1WgD68ZNlHQEcmQg=
istio.io/gogo-genproto v0.0.0-20190904133402-ee07f2785480/go.mod h1:uKtbae4K9k2rjjX4ToV0l6etglbc1i7gqQ94XdkshzY=
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE=
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs=

View File

@ -39,6 +39,7 @@ import (
"sigs.k8s.io/external-dns/provider/aws"
"sigs.k8s.io/external-dns/provider/awssd"
"sigs.k8s.io/external-dns/provider/azure"
"sigs.k8s.io/external-dns/provider/bluecat"
"sigs.k8s.io/external-dns/provider/cloudflare"
"sigs.k8s.io/external-dns/provider/coredns"
"sigs.k8s.io/external-dns/provider/designate"
@ -120,6 +121,7 @@ func main() {
CFUsername: cfg.CFUsername,
CFPassword: cfg.CFPassword,
ContourLoadBalancerService: cfg.ContourLoadBalancerService,
GlooNamespace: cfg.GlooNamespace,
SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion,
RequestTimeout: cfg.RequestTimeout,
}
@ -194,6 +196,8 @@ func main() {
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "bluecat":
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, domainFilter, zoneIDFilter, cfg.DryRun)
case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":

View File

@ -45,6 +45,7 @@ type Config struct {
KubeConfig string
RequestTimeout time.Duration
ContourLoadBalancerService string
GlooNamespace string
SkipperRouteGroupVersion string
Sources []string
Namespace string
@ -82,6 +83,7 @@ type Config struct {
AzureResourceGroup string
AzureSubscriptionID string
AzureUserAssignedIdentityClientID string
BluecatConfigFile string
CloudflareProxied bool
CloudflareZonesPerPage int
CoreDNSPrefix string
@ -142,6 +144,7 @@ type Config struct {
RFC2136Zone string
RFC2136Insecure bool
RFC2136GSSTSIG bool
RFC2136KerberosRealm string
RFC2136KerberosUsername string
RFC2136KerberosPassword string
RFC2136KerberosRealm string
@ -168,6 +171,7 @@ var defaultConfig = &Config{
KubeConfig: "",
RequestTimeout: time.Second * 30,
ContourLoadBalancerService: "heptio-contour/contour",
GlooNamespace: "gloo-system",
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: nil,
Namespace: "",
@ -200,6 +204,7 @@ var defaultConfig = &Config{
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
CoreDNSPrefix: "/skydns/",
@ -256,6 +261,7 @@ var defaultConfig = &Config{
RFC2136Zone: "",
RFC2136Insecure: false,
RFC2136GSSTSIG: false,
RFC2136KerberosRealm: "",
RFC2136KerberosUsername: "",
RFC2136KerberosPassword: "",
RFC2136TSIGKeyName: "",
@ -329,12 +335,14 @@ func (cfg *Config) ParseFlags(args []string) error {
// Flags related to Contour
app.Flag("contour-load-balancer", "The fully-qualified name of the Contour load balancer service. (default: heptio-contour/contour)").Default("heptio-contour/contour").StringVar(&cfg.ContourLoadBalancerService)
// Flags related to Gloo
app.Flag("gloo-namespace", "Gloo namespace. (default: gloo-system)").Default("gloo-system").StringVar(&cfg.GlooNamespace)
// Flags related to Skipper RouteGroup
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing sources
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
@ -353,7 +361,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
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)
app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
@ -376,6 +384,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure-private-dns)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
app.Flag("bluecat-config-file", "When using the Bluecat provider, specify the Bluecat configuration file (required when --provider=bluecat").Default(defaultConfig.BluecatConfigFile).StringVar(&cfg.BluecatConfigFile)
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)

View File

@ -35,6 +35,7 @@ var (
KubeConfig: "",
RequestTimeout: time.Second * 30,
ContourLoadBalancerService: "heptio-contour/contour",
GlooNamespace: "gloo-system",
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: []string{"service"},
Namespace: "",
@ -61,6 +62,7 @@ var (
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
CoreDNSPrefix: "/skydns/",
@ -114,6 +116,7 @@ var (
KubeConfig: "/some/path",
RequestTimeout: time.Second * 77,
ContourLoadBalancerService: "heptio-contour-other/contour-other",
GlooNamespace: "gloo-not-system",
SkipperRouteGroupVersion: "zalando.org/v2",
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
@ -142,6 +145,7 @@ var (
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
AzureSubscriptionID: "arg",
BluecatConfigFile: "bluecat.json",
CloudflareProxied: true,
CloudflareZonesPerPage: 20,
CoreDNSPrefix: "/coredns/",
@ -220,6 +224,7 @@ func TestParseFlags(t *testing.T) {
"--kubeconfig=/some/path",
"--request-timeout=77s",
"--contour-load-balancer=heptio-contour-other/contour-other",
"--gloo-namespace=gloo-not-system",
"--skipper-routegroup-groupversion=zalando.org/v2",
"--source=service",
"--source=ingress",
@ -236,6 +241,7 @@ func TestParseFlags(t *testing.T) {
"--azure-config-file=azure.json",
"--azure-resource-group=arg",
"--azure-subscription-id=arg",
"--bluecat-config-file=bluecat.json",
"--cloudflare-proxied",
"--cloudflare-zones-per-page=20",
"--coredns-prefix=/coredns/",
@ -317,6 +323,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
"EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system",
"EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
@ -331,6 +338,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_BLUECAT_CONFIG_FILE": "bluecat.json",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",

6
provider/bluecat/OWNERS Normal file
View File

@ -0,0 +1,6 @@
approvers:
- seanmalloy
- vinny-sabatini
reviewers:
- seanmalloy
- vinny-sabatini

969
provider/bluecat/bluecat.go Normal file
View File

@ -0,0 +1,969 @@
/*
Copyright 2020 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.
*/
// TODO: Ensure we have proper error handling/logging for API calls to Bluecat. getBluecatGatewayToken has a good example of this
package bluecat
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type bluecatConfig struct {
GatewayHost string `json:"gatewayHost"`
GatewayUsername string `json:"gatewayUsername"`
GatewayPassword string `json:"gatewayPassword"`
DNSConfiguration string `json:"dnsConfiguration"`
View string `json:"dnsView"`
RootZone string `json:"rootZone"`
}
// BluecatProvider implements the DNS provider for Bluecat DNS
type BluecatProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
RootZone string
DNSConfiguration string
View string
gatewayClient GatewayClient
}
type GatewayClient interface {
getBluecatZones(zoneName string) ([]BluecatZone, error)
getHostRecords(zone string, records *[]BluecatHostRecord) error
getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error
getHostRecord(name string, record *BluecatHostRecord) error
getCNAMERecord(name string, record *BluecatCNAMERecord) error
createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error)
createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error)
deleteHostRecord(name string) (err error)
deleteCNAMERecord(name string) (err error)
buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error)
getTXTRecords(zone string, records *[]BluecatTXTRecord) error
getTXTRecord(name string, record *BluecatTXTRecord) error
createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error)
deleteTXTRecord(name string) error
}
// GatewayClientConfig defines new client on bluecat gateway
type GatewayClientConfig struct {
Cookie http.Cookie
Token string
Host string
DNSConfiguration string
View string
RootZone string
}
// BluecatZone defines a zone to hold records
type BluecatZone struct {
ID int `json:"id"`
Name string `json:"name"`
Properties string `json:"properties"`
Type string `json:"type"`
}
// BluecatHostRecord defines dns Host record
type BluecatHostRecord struct {
ID int `json:"id"`
Name string `json:"name"`
Properties string `json:"properties"`
Type string `json:"type"`
}
// BluecatCNAMERecord defines dns CNAME record
type BluecatCNAMERecord struct {
ID int `json:"id"`
Name string `json:"name"`
Properties string `json:"properties"`
Type string `json:"type"`
}
// BluecatTXTRecord defines dns TXT record
type BluecatTXTRecord struct {
ID int `json:"id"`
Name string `json:"name"`
Text string `json:"text"`
}
type bluecatRecordSet struct {
obj interface{}
res interface{}
}
type bluecatCreateHostRecordRequest struct {
AbsoluteName string `json:"absolute_name"`
IP4Address string `json:"ip4_address"`
TTL int `json:"ttl"`
Properties string `json:"properties"`
}
type bluecatCreateCNAMERecordRequest struct {
AbsoluteName string `json:"absolute_name"`
LinkedRecord string `json:"linked_record"`
TTL int `json:"ttl"`
Properties string `json:"properties"`
}
type bluecatCreateTXTRecordRequest struct {
AbsoluteName string `json:"absolute_name"`
Text string `json:"txt"`
}
// NewBluecatProvider creates a new Bluecat provider.
//
// Returns a pointer to the provider or an error if a provider could not be created.
func NewBluecatProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*BluecatProvider, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
}
cfg := bluecatConfig{}
err = json.Unmarshal(contents, &cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
}
token, cookie, err := getBluecatGatewayToken(cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway")
}
gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone)
provider := &BluecatProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
gatewayClient: gatewayClient,
DNSConfiguration: cfg.DNSConfiguration,
View: cfg.View,
RootZone: cfg.RootZone,
}
return provider, nil
}
// NewGatewayClient creates and returns a new Bluecat gateway client
func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone string) GatewayClientConfig {
// Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user
// doesn't provide one via the config file we'll assume it's 'com'
if rootZone == "" {
rootZone = "com"
}
return GatewayClientConfig{
Cookie: cookie,
Token: token,
Host: gatewayHost,
DNSConfiguration: dnsConfiguration,
View: view,
RootZone: rootZone,
}
}
// Records fetches Host, CNAME, and TXT records from bluecat gateway
func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.zones()
if err != nil {
return nil, errors.Wrap(err, "could not fetch zones")
}
for _, zone := range zones {
log.Debugf("fetching records from zone '%s'", zone)
var resH []BluecatHostRecord
err = p.gatewayClient.getHostRecords(zone, &resH)
if err != nil {
return nil, errors.Wrapf(err, "could not fetch host records for zone: %v", zone)
}
for _, rec := range resH {
propMap := splitProperties(rec.Properties)
ips := strings.Split(propMap["addresses"], ",")
for _, ip := range ips {
ep := endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeA, ip)
endpoints = append(endpoints, ep)
}
}
var resC []BluecatCNAMERecord
err = p.gatewayClient.getCNAMERecords(zone, &resC)
if err != nil {
return nil, errors.Wrapf(err, "could not fetch CNAME records for zone: %v", zone)
}
for _, rec := range resC {
propMap := splitProperties(rec.Properties)
endpoints = append(endpoints, endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeCNAME, propMap["linkedRecordName"]))
}
var resT []BluecatTXTRecord
err = p.gatewayClient.getTXTRecords(zone, &resT)
if err != nil {
return nil, errors.Wrapf(err, "could not fetch TXT records for zone: %v", zone)
}
for _, rec := range resT {
endpoints = append(endpoints, endpoint.NewEndpoint(rec.Name, endpoint.RecordTypeTXT, rec.Text))
}
}
log.Debugf("fetched %d records from Bluecat", len(endpoints))
return endpoints, nil
}
// ApplyChanges updates necessary zones and replaces old records with new ones
//
// Returns nil upon success and err is there is an error
func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zones, err := p.zones()
if err != nil {
return err
}
log.Infof("zones is: %+v\n", zones)
log.Infof("changes: %+v\n", changes)
created, deleted := p.mapChanges(zones, changes)
log.Infof("created: %+v\n", created)
log.Infof("deleted: %+v\n", deleted)
p.deleteRecords(deleted)
p.createRecords(created)
// TODO: add bluecat deploy API call here
return nil
}
type bluecatChangeMap map[string][]*endpoint.Endpoint
func (p *BluecatProvider) mapChanges(zones []string, changes *plan.Changes) (bluecatChangeMap, bluecatChangeMap) {
created := bluecatChangeMap{}
deleted := bluecatChangeMap{}
mapChange := func(changeMap bluecatChangeMap, change *endpoint.Endpoint) {
zone := p.findZone(zones, change.DNSName)
if zone == "" {
log.Debugf("ignoring changes to '%s' because a suitable Bluecat DNS zone was not found", change.DNSName)
return
}
changeMap[zone] = append(changeMap[zone], change)
}
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
}
// findZone finds the most specific matching zone for a given record 'name' from a list of all zones
func (p *BluecatProvider) findZone(zones []string, name string) string {
var result string
for _, zone := range zones {
if strings.HasSuffix(name, "."+zone) {
if result == "" || len(zone) > len(result) {
result = zone
}
} else if strings.EqualFold(name, zone) {
if result == "" || len(zone) > len(result) {
result = zone
}
}
}
return result
}
func (p *BluecatProvider) zones() ([]string, error) {
log.Debugf("retrieving Bluecat zones for configuration: %s, view: %s", p.DNSConfiguration, p.View)
var zones []string
zonelist, err := p.gatewayClient.getBluecatZones(p.RootZone)
if err != nil {
return nil, err
}
for _, zone := range zonelist {
if !p.domainFilter.Match(zone.Name) {
continue
}
// TODO: match to absoluteName(string) not Id(int)
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
continue
}
zoneProps := splitProperties(zone.Properties)
zones = append(zones, zoneProps["absoluteName"])
}
log.Debugf("found %d zones", len(zones))
return zones, nil
}
func (p *BluecatProvider) createRecords(created bluecatChangeMap) {
for zone, endpoints := range created {
for _, ep := range endpoints {
if p.dryRun {
log.Infof("would create %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
)
continue
}
log.Infof("creating %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
)
recordSet, err := p.recordSet(ep, false)
if err != nil {
log.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
var response interface{}
switch ep.RecordType {
case endpoint.RecordTypeA:
response, err = p.gatewayClient.createHostRecord(zone, recordSet.obj.(*bluecatCreateHostRecordRequest))
case endpoint.RecordTypeCNAME:
response, err = p.gatewayClient.createCNAMERecord(zone, recordSet.obj.(*bluecatCreateCNAMERecordRequest))
case endpoint.RecordTypeTXT:
response, err = p.gatewayClient.createTXTRecord(zone, recordSet.obj.(*bluecatCreateTXTRecordRequest))
}
log.Debugf("Response from create: %v", response)
if err != nil {
log.Errorf(
"Failed to create %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
}
}
}
}
func (p *BluecatProvider) deleteRecords(deleted bluecatChangeMap) {
// run deletions first
for zone, endpoints := range deleted {
for _, ep := range endpoints {
if p.dryRun {
log.Infof("would delete %s record named '%s' for Bluecat DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
zone,
)
continue
} else {
log.Infof("deleting %s record named '%s' for Bluecat DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
zone,
)
recordSet, err := p.recordSet(ep, true)
if err != nil {
log.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]BluecatHostRecord) {
err = p.gatewayClient.deleteHostRecord(record.Name)
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]BluecatCNAMERecord) {
err = p.gatewayClient.deleteCNAMERecord(record.Name)
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]BluecatTXTRecord) {
err = p.gatewayClient.deleteTXTRecord(record.Name)
}
}
if err != nil {
log.Errorf("Failed to delete %s record named '%s' for Bluecat DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err)
}
}
}
}
}
func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (recordSet bluecatRecordSet, err error) {
switch ep.RecordType {
case endpoint.RecordTypeA:
var res []BluecatHostRecord
// TODO Allow configurable properties/ttl
obj := bluecatCreateHostRecordRequest{
AbsoluteName: ep.DNSName,
IP4Address: ep.Targets[0],
TTL: 0,
Properties: "",
}
if getObject {
var record BluecatHostRecord
err = p.gatewayClient.getHostRecord(ep.DNSName, &record)
if err != nil {
return
}
res = append(res, record)
}
recordSet = bluecatRecordSet{
obj: &obj,
res: &res,
}
case endpoint.RecordTypeCNAME:
var res []BluecatCNAMERecord
obj := bluecatCreateCNAMERecordRequest{
AbsoluteName: ep.DNSName,
LinkedRecord: ep.Targets[0],
TTL: 0,
Properties: "",
}
if getObject {
var record BluecatCNAMERecord
err = p.gatewayClient.getCNAMERecord(ep.DNSName, &record)
if err != nil {
return
}
res = append(res, record)
}
recordSet = bluecatRecordSet{
obj: &obj,
res: &res,
}
case endpoint.RecordTypeTXT:
var res []BluecatTXTRecord
obj := bluecatCreateTXTRecordRequest{
AbsoluteName: ep.DNSName,
Text: ep.Targets[0],
}
if getObject {
var record BluecatTXTRecord
err = p.gatewayClient.getTXTRecord(ep.DNSName, &record)
if err != nil {
return
}
res = append(res, record)
}
recordSet = bluecatRecordSet{
obj: &obj,
res: &res,
}
}
return
}
// getBluecatGatewayToken retrieves a Bluecat Gateway API token.
func getBluecatGatewayToken(cfg bluecatConfig) (string, http.Cookie, error) {
body, err := json.Marshal(map[string]string{
"username": cfg.GatewayUsername,
"password": cfg.GatewayPassword,
})
if err != nil {
return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config")
}
c := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // ignore self-signed SSL cert check
}}
resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body))
if err != nil {
return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
details, _ := ioutil.ReadAll(resp.Body)
return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", resp.StatusCode, string(details))
}
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", http.Cookie{}, errors.Wrap(err, "error reading get_token response from bluecat gateway")
}
resJSON := map[string]string{}
err = json.Unmarshal(res, &resJSON)
if err != nil {
return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway")
}
// Example response: {"access_token": "BAMAuthToken: abc123"}
// We only care about the actual token string - i.e. abc123
// The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least
// for now.
return strings.Split(resJSON["access_token"], " ")[1], *resp.Cookies()[0], nil
}
func (c GatewayClientConfig) getBluecatZones(zoneName string) ([]BluecatZone, error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zoneName)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return nil, errors.Wrap(err, "error building http request")
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "error retrieving zone(s) from gateway: %v, %v", url, zoneName)
}
defer resp.Body.Close()
zones := []BluecatZone{}
json.NewDecoder(resp.Body).Decode(&zones)
// Bluecat Gateway only returns subzones one level deeper than the provided zone
// so this recursion is needed to traverse subzones until none are returned
for _, zone := range zones {
zoneProps := splitProperties(zone.Properties)
subZones, err := c.getBluecatZones(zoneProps["absoluteName"])
if err != nil {
return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName)
}
zones = append(zones, subZones...)
}
return zones, nil
}
func (c GatewayClientConfig) getHostRecords(zone string, records *[]BluecatHostRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrap(err, "error building http request")
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(records)
log.Debugf("Get Host Records Response: %v", records)
return nil
}
func (c GatewayClientConfig) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrap(err, "error building http request")
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(records)
log.Debugf("Get CName Records Response: %v", records)
return nil
}
func (c GatewayClientConfig) getTXTRecords(zone string, records *[]BluecatTXTRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrap(err, "error building http request")
}
log.Debugf("Request: %v", req)
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
}
log.Debugf("Get Txt Records response: %v", resp)
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(records)
log.Debugf("Get TXT Records Body: %v", records)
return nil
}
func (c GatewayClientConfig) getHostRecord(name string, record *BluecatHostRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"host_records/" + name + "/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrapf(err, "error building http request: %v", name)
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(record)
log.Debugf("Get Host Record Response: %v", record)
return nil
}
func (c GatewayClientConfig) getCNAMERecord(name string, record *BluecatCNAMERecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"cname_records/" + name + "/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrapf(err, "error building http request: %v", name)
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(record)
log.Debugf("Get CName Record Response: %v", record)
return nil
}
func (c GatewayClientConfig) getTXTRecord(name string, record *BluecatTXTRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"text_records/" + name + "/"
req, err := c.buildHTTPRequest("GET", url, nil)
if err != nil {
return errors.Wrap(err, "error building http request")
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(record)
log.Debugf("Get TXT Record Response: %v", record)
return nil
}
func (c GatewayClientConfig) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
body, _ := json.Marshal(req)
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, errors.Wrap(err, "error building http request")
}
hreq.Header.Add("Content-Type", "application/json")
res, err = client.Do(hreq)
return
}
func (c GatewayClientConfig) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
body, _ := json.Marshal(req)
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, errors.Wrap(err, "error building http request")
}
hreq.Header.Add("Content-Type", "application/json")
res, err = client.Do(hreq)
return
}
func (c GatewayClientConfig) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (interface{}, error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
zonePath = strings.TrimSuffix(zonePath, "zones/")
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
body, _ := json.Marshal(req)
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
hreq.Header.Add("Content-Type", "application/json")
res, err := client.Do(hreq)
return res, err
}
func (c GatewayClientConfig) deleteHostRecord(name string) (err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"host_records/" + name + "/"
req, err := c.buildHTTPRequest("DELETE", url, nil)
if err != nil {
return errors.Wrapf(err, "error building http request: %v", name)
}
_, err = client.Do(req)
if err != nil {
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
}
return nil
}
func (c GatewayClientConfig) deleteCNAMERecord(name string) (err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"cname_records/" + name + "/"
req, err := c.buildHTTPRequest("DELETE", url, nil)
if err != nil {
return errors.Wrapf(err, "error building http request: %v", name)
}
_, err = client.Do(req)
if err != nil {
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
}
return nil
}
func (c GatewayClientConfig) deleteTXTRecord(name string) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
}
client := &http.Client{
Transport: transportCfg,
}
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
"text_records/" + name + "/"
req, err := c.buildHTTPRequest("DELETE", url, nil)
if err != nil {
return errors.Wrap(err, "error building http request")
}
_, err = client.Do(req)
if err != nil {
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
}
return nil
}
//buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway
func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Basic "+c.Token)
req.AddCookie(&c.Cookie)
return req, err
}
//splitProperties is a helper function to break a '|' separated string into key/value pairs
// i.e. "foo=bar|baz=mop"
func splitProperties(props string) map[string]string {
propMap := make(map[string]string)
// remove trailing | character before we split
props = strings.TrimSuffix(props, "|")
splits := strings.Split(props, "|")
for _, pair := range splits {
items := strings.Split(pair, "=")
propMap[items[0]] = items[1]
}
return propMap
}
//expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway,
//such as '/zones/com/zones/example/zones/'
func expandZone(zone string) string {
ze := "zones/"
parts := strings.Split(zone, ".")
if len(parts) > 1 {
last := len(parts) - 1
for i := range parts {
ze = ze + parts[last-i] + "/zones/"
}
} else {
ze = ze + zone + "/zones/"
}
return ze
}

View File

@ -0,0 +1,390 @@
/*
Copyright 2020 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 bluecat
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"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 mockGatewayClient struct {
mockBluecatZones *[]BluecatZone
mockBluecatHosts *[]BluecatHostRecord
mockBluecatCNAMEs *[]BluecatCNAMERecord
mockBluecatTXTs *[]BluecatTXTRecord
}
type Changes struct {
// Records that need to be created
Create []*endpoint.Endpoint
// Records that need to be updated (current data)
UpdateOld []*endpoint.Endpoint
// Records that need to be updated (desired data)
UpdateNew []*endpoint.Endpoint
// Records that need to be deleted
Delete []*endpoint.Endpoint
}
func (g mockGatewayClient) getBluecatZones(zoneName string) ([]BluecatZone, error) {
return *g.mockBluecatZones, nil
}
func (g mockGatewayClient) getHostRecords(zone string, records *[]BluecatHostRecord) error {
*records = *g.mockBluecatHosts
return nil
}
func (g mockGatewayClient) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
*records = *g.mockBluecatCNAMEs
return nil
}
func (g mockGatewayClient) getHostRecord(name string, record *BluecatHostRecord) error {
for _, currentRecord := range *g.mockBluecatHosts {
if currentRecord.Name == strings.Split(name, ".")[0] {
*record = currentRecord
return nil
}
}
return nil
}
func (g mockGatewayClient) getCNAMERecord(name string, record *BluecatCNAMERecord) error {
for _, currentRecord := range *g.mockBluecatCNAMEs {
if currentRecord.Name == strings.Split(name, ".")[0] {
*record = currentRecord
return nil
}
}
return nil
}
func (g mockGatewayClient) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) {
return nil, nil
}
func (g mockGatewayClient) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) {
return nil, nil
}
func (g mockGatewayClient) deleteHostRecord(name string) (err error) {
*g.mockBluecatHosts = nil
return nil
}
func (g mockGatewayClient) deleteCNAMERecord(name string) (err error) {
*g.mockBluecatCNAMEs = nil
return nil
}
func (g mockGatewayClient) getTXTRecords(zone string, records *[]BluecatTXTRecord) error {
*records = *g.mockBluecatTXTs
return nil
}
func (g mockGatewayClient) getTXTRecord(name string, record *BluecatTXTRecord) error {
for _, currentRecord := range *g.mockBluecatTXTs {
if currentRecord.Name == name {
*record = currentRecord
return nil
}
}
return nil
}
func (g mockGatewayClient) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error) {
return nil, nil
}
func (g mockGatewayClient) deleteTXTRecord(name string) error {
*g.mockBluecatTXTs = nil
return nil
}
func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/users", "http://some.com/api/v1"), nil)
return request, nil
}
func createMockBluecatZone(fqdn string) BluecatZone {
props := "absoluteName=" + fqdn
return BluecatZone{
Properties: props,
Name: fqdn,
ID: 3,
}
}
func createMockBluecatHostRecord(fqdn, target string) BluecatHostRecord {
props := "absoluteName=" + fqdn + "|addresses=" + target + "|"
nameParts := strings.Split(fqdn, ".")
return BluecatHostRecord{
Name: nameParts[0],
Properties: props,
ID: 3,
}
}
func createMockBluecatCNAME(alias, target string) BluecatCNAMERecord {
props := "absoluteName=" + alias + "|linkedRecordName=" + target + "|"
nameParts := strings.Split(alias, ".")
return BluecatCNAMERecord{
Name: nameParts[0],
Properties: props,
}
}
func createMockBluecatTXT(fqdn, txt string) BluecatTXTRecord {
return BluecatTXTRecord{
Name: fqdn,
Text: txt,
}
}
func newBluecatProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client GatewayClient) *BluecatProvider {
return &BluecatProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
gatewayClient: client,
}
}
type bluecatTestData []struct {
TestDescription string
Endpoints []*endpoint.Endpoint
}
var tests = bluecatTestData{
{
"first test case", // TODO: better test description
[]*endpoint.Endpoint{
{
DNSName: "example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.122"},
},
{
DNSName: "nginx.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.123"},
},
{
DNSName: "whitespace.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"123.123.123.124"},
},
{
DNSName: "hack.example.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"bluecatnetworks.com"},
},
{
DNSName: "abc.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: endpoint.Targets{"hello"},
},
},
},
}
func TestBluecatRecords(t *testing.T) {
client := mockGatewayClient{
mockBluecatZones: &[]BluecatZone{
createMockBluecatZone("example.com"),
},
mockBluecatHosts: &[]BluecatHostRecord{
createMockBluecatHostRecord("example.com", "123.123.123.122"),
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
},
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
},
mockBluecatTXTs: &[]BluecatTXTRecord{
createMockBluecatTXT("abc.example.com", "hello"),
},
}
provider := newBluecatProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.NewZoneIDFilter([]string{""}), false, client)
for _, ti := range tests {
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, ti.Endpoints)
}
}
func TestBluecatApplyChangesCreate(t *testing.T) {
client := mockGatewayClient{
mockBluecatZones: &[]BluecatZone{
createMockBluecatZone("example.com"),
},
mockBluecatHosts: &[]BluecatHostRecord{},
mockBluecatCNAMEs: &[]BluecatCNAMERecord{},
mockBluecatTXTs: &[]BluecatTXTRecord{},
}
provider := newBluecatProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.NewZoneIDFilter([]string{""}), false, client)
for _, ti := range tests {
err := provider.ApplyChanges(context.Background(), &plan.Changes{Create: ti.Endpoints})
if err != nil {
t.Fatal(err)
}
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, []*endpoint.Endpoint{})
}
}
func TestBluecatApplyChangesDelete(t *testing.T) {
client := mockGatewayClient{
mockBluecatZones: &[]BluecatZone{
createMockBluecatZone("example.com"),
},
mockBluecatHosts: &[]BluecatHostRecord{
createMockBluecatHostRecord("example.com", "123.123.123.122"),
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
},
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
},
mockBluecatTXTs: &[]BluecatTXTRecord{
createMockBluecatTXT("abc.example.com", "hello"),
},
}
provider := newBluecatProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.NewZoneIDFilter([]string{""}), false, client)
for _, ti := range tests {
err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: ti.Endpoints})
if err != nil {
t.Fatal(err)
}
actual, err := provider.Records(context.Background())
if err != nil {
t.Fatal(err)
}
validateEndpoints(t, actual, []*endpoint.Endpoint{})
}
}
// TODO: ensure mapChanges method is tested
// TODO: ensure findZone method is tested
// TODO: ensure zones method is tested
// TODO: ensure createRecords method is tested
// TODO: ensure deleteRecords method is tested
// TODO: ensure recordSet method is tested
// TODO: Figure out why recordSet.res is not being set properly
func TestBluecatRecordset(t *testing.T) {
client := mockGatewayClient{
mockBluecatZones: &[]BluecatZone{
createMockBluecatZone("example.com"),
},
mockBluecatHosts: &[]BluecatHostRecord{
createMockBluecatHostRecord("example.com", "123.123.123.122"),
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
},
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
},
mockBluecatTXTs: &[]BluecatTXTRecord{
createMockBluecatTXT("abc.example.com", "hello"),
},
}
provider := newBluecatProvider(
endpoint.NewDomainFilter([]string{"example.com"}),
provider.NewZoneIDFilter([]string{""}), false, client)
// Test txt records for recordSet function
testTxtEndpoint := endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeTXT, "hello")
txtObj := bluecatCreateTXTRecordRequest{
AbsoluteName: testTxtEndpoint.DNSName,
Text: testTxtEndpoint.Targets[0],
}
txtRecords := []BluecatTXTRecord{
createMockBluecatTXT("abc.example.com", "hello"),
}
expected := bluecatRecordSet{
obj: &txtObj,
res: &txtRecords,
}
actual, err := provider.recordSet(testTxtEndpoint, true)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, actual.obj, expected.obj)
assert.Equal(t, actual.res, expected.res)
// Test a records for recordSet function
testHostEndpoint := endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124")
hostObj := bluecatCreateHostRecordRequest{
AbsoluteName: testHostEndpoint.DNSName,
IP4Address: testHostEndpoint.Targets[0],
}
hostRecords := []BluecatHostRecord{
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
}
hostExpected := bluecatRecordSet{
obj: &hostObj,
res: &hostRecords,
}
hostActual, err := provider.recordSet(testHostEndpoint, true)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, hostActual.obj, hostExpected.obj)
assert.Equal(t, hostActual.res, hostExpected.res)
// Test CName records for recordSet function
testCnameEndpoint := endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "bluecatnetworks.com")
cnameObj := bluecatCreateCNAMERecordRequest{
AbsoluteName: testCnameEndpoint.DNSName,
LinkedRecord: testCnameEndpoint.Targets[0],
}
cnameRecords := []BluecatCNAMERecord{
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
}
cnameExpected := bluecatRecordSet{
obj: &cnameObj,
res: &cnameRecords,
}
cnameActual, err := provider.recordSet(testCnameEndpoint, true)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, cnameActual.obj, cnameExpected.obj)
assert.Equal(t, cnameActual.res, cnameExpected.res)
}
func validateEndpoints(t *testing.T, actual, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(actual, expected), "actual and expected endpoints don't match. %s:%s", actual, expected)
}

View File

@ -91,6 +91,10 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
}
if krb5Realm == "" {
krb5Realm = strings.ToUpper(zoneName)
}
r := &rfc2136Provider{
nameserver: net.JoinHostPort(host, strconv.Itoa(port)),
zoneName: dns.Fqdn(zoneName),

200
source/gloo.go Normal file
View File

@ -0,0 +1,200 @@
/*
Copyright 2020n 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 source
import (
"context"
"encoding/json"
"strings"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/external-dns/endpoint"
)
var (
proxyGVR = schema.GroupVersionResource{
Group: "gloo.solo.io",
Version: "v1",
Resource: "proxies",
}
virtualServiceGVR = schema.GroupVersionResource{
Group: "gateway.solo.io",
Version: "v1",
Resource: "virtualservices",
}
)
// Basic redefinition of "Proxy" CRD : https://github.com/solo-io/gloo/blob/v1.4.6/projects/gloo/pkg/api/v1/proxy.pb.go
type proxy struct {
metav1.TypeMeta `json:",inline"`
Metadata metav1.ObjectMeta `json:"metadata,omitempty"`
Spec proxySpec `json:"spec,omitempty"`
}
type proxySpec struct {
Listeners []proxySpecListener `json:"listeners,omitempty"`
}
type proxySpecListener struct {
HTTPListener proxySpecHTTPListener `json:"httpListener,omitempty"`
}
type proxySpecHTTPListener struct {
VirtualHosts []proxyVirtualHost `json:"virtualHosts,omitempty"`
}
type proxyVirtualHost struct {
Domains []string `json:"domains,omitempty"`
Metadata proxyVirtualHostMetadata `json:"metadata,omitempty"`
}
type proxyVirtualHostMetadata struct {
Source []proxyVirtualHostMetadataSource `json:"sources,omitempty"`
}
type proxyVirtualHostMetadataSource struct {
Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
type glooSource struct {
dynamicKubeClient dynamic.Interface
kubeClient kubernetes.Interface
glooNamespace string
}
// NewGlooSource creates a new glooSource with the given config
func NewGlooSource(dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, glooNamespace string) (Source, error) {
return &glooSource{
dynamicKubeClient,
kubeClient,
glooNamespace,
}, nil
}
func (gs *glooSource) AddEventHandler(ctx context.Context, handler func()) {
}
// Endpoints returns endpoint objects
func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
endpoints := []*endpoint.Endpoint{}
proxies, err := gs.dynamicKubeClient.Resource(proxyGVR).Namespace(gs.glooNamespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, obj := range proxies.Items {
proxy := proxy{}
jsonString, err := obj.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonString, &proxy)
if err != nil {
return nil, err
}
log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name)
proxyTargets, err := gs.proxyTargets(ctx, proxy.Metadata.Name)
if err != nil {
return nil, err
}
log.Debugf("Gloo[%s]: Find %d target(s) (%+v)", proxy.Metadata.Name, len(proxyTargets), proxyTargets)
proxyEndpoints, err := gs.generateEndpointsFromProxy(ctx, &proxy, proxyTargets)
if err != nil {
return nil, err
}
log.Debugf("Gloo[%s]: Generate %d endpoint(s)", proxy.Metadata.Name, len(proxyEndpoints))
endpoints = append(endpoints, proxyEndpoints...)
}
return endpoints, nil
}
func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *proxy, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
endpoints := []*endpoint.Endpoint{}
for _, listener := range proxy.Spec.Listeners {
for _, virtualHost := range listener.HTTPListener.VirtualHosts {
annotations, err := gs.annotationsFromProxySource(ctx, virtualHost)
if err != nil {
return nil, err
}
ttl, err := getTTLFromAnnotations(annotations)
if err != nil {
return nil, err
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations)
for _, domain := range virtualHost.Domains {
endpoints = append(endpoints, endpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier)...)
}
}
}
return endpoints, nil
}
func (gs *glooSource) annotationsFromProxySource(ctx context.Context, virtualHost proxyVirtualHost) (map[string]string, error) {
annotations := map[string]string{}
for _, src := range virtualHost.Metadata.Source {
kind := sourceKind(src.Kind)
if kind != nil {
source, err := gs.dynamicKubeClient.Resource(*kind).Namespace(src.Namespace).Get(ctx, src.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
for key, value := range source.GetAnnotations() {
annotations[key] = value
}
}
}
return annotations, nil
}
func (gs *glooSource) proxyTargets(ctx context.Context, name string) (endpoint.Targets, error) {
svc, err := gs.kubeClient.CoreV1().Services(gs.glooNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, err
}
var targets endpoint.Targets
switch svc.Spec.Type {
case corev1.ServiceTypeLoadBalancer:
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
default:
log.WithField("gateway", name).WithField("service", svc).Warn("Gloo: Proxy service type not supported")
}
return targets, nil
}
func sourceKind(kind string) *schema.GroupVersionResource {
switch kind {
case "*v1.VirtualService":
return &virtualServiceGVR
}
return nil
}

320
source/gloo_test.go Normal file
View File

@ -0,0 +1,320 @@
/*
Copyright 2020n 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 source
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
fakeDynamic "k8s.io/client-go/dynamic/fake"
fakeKube "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
)
// This is a compile-time validation that glooSource is a Source.
var _ Source = &glooSource{}
const defaultGlooNamespace = "gloo-system"
// Internal proxy test
var internalProxy = proxy{
TypeMeta: metav1.TypeMeta{
APIVersion: proxyGVR.GroupVersion().String(),
Kind: "Proxy",
},
Metadata: metav1.ObjectMeta{
Name: "internal",
Namespace: defaultGlooNamespace,
},
Spec: proxySpec{
Listeners: []proxySpecListener{
{
HTTPListener: proxySpecHTTPListener{
VirtualHosts: []proxyVirtualHost{
{
Domains: []string{"a.test", "b.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.Unknown",
Name: "my-unknown-svc",
Namespace: "unknown",
},
},
},
},
{
Domains: []string{"c.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.VirtualService",
Name: "my-internal-svc",
Namespace: "internal",
},
},
},
},
},
},
},
},
},
}
var internalProxySvc = corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: internalProxy.Metadata.Name,
Namespace: internalProxy.Metadata.Namespace,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
corev1.LoadBalancerIngress{
IP: "203.0.113.1",
},
corev1.LoadBalancerIngress{
IP: "203.0.113.2",
},
corev1.LoadBalancerIngress{
IP: "203.0.113.3",
},
},
},
},
}
var internalProxySource = metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: virtualServiceGVR.GroupVersion().String(),
Kind: "VirtualService",
},
ObjectMeta: metav1.ObjectMeta{
Name: internalProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Name,
Namespace: internalProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Namespace,
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ttl": "42",
"external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "LU",
"external-dns.alpha.kubernetes.io/set-identifier": "identifier",
},
},
}
// External proxy test
var externalProxy = proxy{
TypeMeta: metav1.TypeMeta{
APIVersion: proxyGVR.GroupVersion().String(),
Kind: "Proxy",
},
Metadata: metav1.ObjectMeta{
Name: "external",
Namespace: defaultGlooNamespace,
},
Spec: proxySpec{
Listeners: []proxySpecListener{
{
HTTPListener: proxySpecHTTPListener{
VirtualHosts: []proxyVirtualHost{
{
Domains: []string{"d.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.Unknown",
Name: "my-unknown-svc",
Namespace: "unknown",
},
},
},
},
{
Domains: []string{"e.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.VirtualService",
Name: "my-external-svc",
Namespace: "external",
},
},
},
},
},
},
},
},
},
}
var externalProxySvc = corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: externalProxy.Metadata.Name,
Namespace: externalProxy.Metadata.Namespace,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
corev1.LoadBalancerIngress{
Hostname: "a.example.org",
},
corev1.LoadBalancerIngress{
Hostname: "b.example.org",
},
corev1.LoadBalancerIngress{
Hostname: "c.example.org",
},
},
},
},
}
var externalProxySource = metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: virtualServiceGVR.GroupVersion().String(),
Kind: "VirtualService",
},
ObjectMeta: metav1.ObjectMeta{
Name: externalProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Name,
Namespace: externalProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Namespace,
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ttl": "24",
"external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "JP",
"external-dns.alpha.kubernetes.io/set-identifier": "identifier-external",
},
},
}
func TestGlooSource(t *testing.T) {
fakeKubernetesClient := fakeKube.NewSimpleClientset()
fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(runtime.NewScheme())
source, err := NewGlooSource(fakeDynamicClient, fakeKubernetesClient, defaultGlooNamespace)
assert.NoError(t, err)
assert.NotNil(t, source)
internalProxyUnstructured := unstructured.Unstructured{}
externalProxyUnstructured := unstructured.Unstructured{}
internalProxySourceUnstructured := unstructured.Unstructured{}
externalProxySourceUnstructured := unstructured.Unstructured{}
internalProxyAsJSON, err := json.Marshal(internalProxy)
assert.NoError(t, err)
externalProxyAsJSON, err := json.Marshal(externalProxy)
assert.NoError(t, err)
internalProxySvcAsJSON, err := json.Marshal(internalProxySource)
assert.NoError(t, err)
externalProxySvcAsJSON, err := json.Marshal(externalProxySource)
assert.NoError(t, err)
assert.NoError(t, internalProxyUnstructured.UnmarshalJSON(internalProxyAsJSON))
assert.NoError(t, externalProxyUnstructured.UnmarshalJSON(externalProxyAsJSON))
assert.NoError(t, internalProxySourceUnstructured.UnmarshalJSON(internalProxySvcAsJSON))
assert.NoError(t, externalProxySourceUnstructured.UnmarshalJSON(externalProxySvcAsJSON))
// Create proxy resources
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &internalProxyUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &externalProxyUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy source
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(internalProxySource.Namespace).Create(context.Background(), &internalProxySourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(externalProxySource.Namespace).Create(context.Background(), &externalProxySourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy service resources
_, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeKubernetesClient.CoreV1().Services(externalProxySvc.GetNamespace()).Create(context.Background(), &externalProxySvc, metav1.CreateOptions{})
assert.NoError(t, err)
endpoints, err := source.Endpoints(context.Background())
assert.NoError(t, err)
assert.Len(t, endpoints, 5)
assert.Equal(t, endpoints, []*endpoint.Endpoint{
&endpoint.Endpoint{
DNSName: "a.test",
Targets: []string{internalProxySvc.Status.LoadBalancer.Ingress[0].IP, internalProxySvc.Status.LoadBalancer.Ingress[1].IP, internalProxySvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
&endpoint.Endpoint{
DNSName: "b.test",
Targets: []string{internalProxySvc.Status.LoadBalancer.Ingress[0].IP, internalProxySvc.Status.LoadBalancer.Ingress[1].IP, internalProxySvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
&endpoint.Endpoint{
DNSName: "c.test",
Targets: []string{internalProxySvc.Status.LoadBalancer.Ingress[0].IP, internalProxySvc.Status.LoadBalancer.Ingress[1].IP, internalProxySvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "identifier",
RecordTTL: 42,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "aws/geolocation-country-code",
Value: "LU",
},
},
},
&endpoint.Endpoint{
DNSName: "d.test",
Targets: []string{externalProxySvc.Status.LoadBalancer.Ingress[0].Hostname, externalProxySvc.Status.LoadBalancer.Ingress[1].Hostname, externalProxySvc.Status.LoadBalancer.Ingress[2].Hostname},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 0,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
&endpoint.Endpoint{
DNSName: "e.test",
Targets: []string{externalProxySvc.Status.LoadBalancer.Ingress[0].Hostname, externalProxySvc.Status.LoadBalancer.Ingress[1].Hostname, externalProxySvc.Status.LoadBalancer.Ingress[2].Hostname},
RecordType: endpoint.RecordTypeCNAME,
SetIdentifier: "identifier-external",
RecordTTL: 24,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "aws/geolocation-country-code",
Value: "JP",
},
},
},
})
}

View File

@ -61,6 +61,7 @@ type Config struct {
CFUsername string
CFPassword string
ContourLoadBalancerService string
GlooNamespace string
SkipperRouteGroupVersion string
RequestTimeout time.Duration
}
@ -239,6 +240,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
return nil, err
}
return NewContourHTTPProxySource(dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
case "gloo-proxy":
kubernetesClient, err := p.KubeClient()
if err != nil {
return nil, err
}
dynamicClient, err := p.DynamicKubernetesClient()
if err != nil {
return nil, err
}
return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespace)
case "openshift-route":
ocpClient, err := p.OpenShiftClient()
if err != nil {