diff --git a/README.md b/README.md index 8bc505cdb..d77f89ff0 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ ExternalDNS' allows you to keep selected zones (via `--domain-filter`) synchroni * [GoDaddy](https://www.godaddy.com) * [Gandi](https://www.gandi.net) * [UKFast SafeDNS](https://my.ukfast.co.uk/safedns/) +* [IBM Cloud DNS](https://www.ibm.com/cloud/dns) From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. @@ -109,6 +110,7 @@ The following table clarifies the current status of the providers according to t | GoDaddy | Alpha | | | Gandi | Alpha | @packi | | SafeDNS | Alpha | @assureddt | +| IBMCloud | Alpha | @hughhuangzh | ## Kubernetes version compatibility @@ -175,6 +177,7 @@ The following tutorials are provided: * [GoDaddy](docs/tutorials/godaddy.md) * [Gandi](docs/tutorials/gandi.md) * [SafeDNS](docs/tutorials/safedns.md) +* [IBM Cloud](docs/tutorials/ibmcloud.md) * [Nodes as source](docs/tutorials/nodes.md) ### Running Locally diff --git a/docs/tutorials/ibmcloud.md b/docs/tutorials/ibmcloud.md new file mode 100644 index 000000000..fe89846df --- /dev/null +++ b/docs/tutorials/ibmcloud.md @@ -0,0 +1,250 @@ +# Setting up ExternalDNS for Services on IBMCloud + +This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using IBMCloud DNS. + +This tutorial uses [IBMCloud CLI](https://cloud.ibm.com/docs/cli?topic=cli-getting-started) for all +IBM Cloud commands and assumes that the Kubernetes cluster was created via IBM Cloud Kubernetes Service and `kubectl` commands +are being run on an orchestration node. + +## Creating a IBMCloud DNS zone +The IBMCloud provider for ExternalDNS will find suitable zones for domains it manages; it will +not automatically create zones. +For public zone, This tutorial assume that the [IBMCloud Internet Services](https://cloud.ibm.com/catalog/services/internet-services) was provisioned and the [cis cli plugin](https://cloud.ibm.com/docs/cis?topic=cis-cli-plugin-cis-cli) was installed with IBMCloud CLI +For private zone, This tutorial assume that the [IBMCloud DNS Services](https://cloud.ibm.com/catalog/services/dns-services) was provisioned and the [dns cli plugin](https://cloud.ibm.com/docs/dns-svcs?topic=dns-svcs-cli-plugin-dns-services-cli-commands) was installed with IBMCloud CLI + +### Public Zone +For this tutorial, we create public zone named `example.com` on IBMCloud Internet Services instance `external-dns-public` +``` +$ ibmcloud cis domain-add example.com -i external-dns-public +``` +Follow [step](https://cloud.ibm.com/docs/cis?topic=cis-getting-started#configure-your-name-servers-with-the-registrar-or-existing-dns-provider) to active your zone + +### Private Zone +For this tutorial, we create private zone named `example.com` on IBMCloud DNS Services instance `external-dns-private` +``` +$ ibmcloud dns zone-create example.com -i external-dns-private +``` + +## Creating configuration file + +The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: + +``` +{ + "apiKey": "1234567890abcdefghijklmnopqrstuvwxyz", + "instanceCrn": "crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:b950da8a-5be6-4691-810e-36388c77b0a3::" +} +``` + +You can create or find the `apiKey` in your ibmcloud IAM --> [API Keys page](https://cloud.ibm.com/iam/apikeys) + +You can find the `instanceCrn` in your service instance details + +Now you can create a file named 'ibmcloud.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: +``` +$ kubectl create secret generic ibmcloud-config-file --from-file=/local/path/to/ibmcloud.json +``` +## Deploy ExternalDNS + +Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +Then apply one of the following manifests file to deploy ExternalDNS. + +### Manifest (for clusters without RBAC enabled) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: k8s.gcr.io/external-dns/external-dns:v0.10.2 + args: + - --source=service # ingress is also possible + - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. + - --provider=ibmcloud + - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud + volumeMounts: + - name: ibmcloud-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: ibmcloud-config-file + secret: + secretName: ibmcloud-config-file + items: + - key: externaldns-config.json + path: ibmcloud.json +``` + +### Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions","networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: k8s.gcr.io/external-dns/external-dns:v0.10.2 + args: + - --source=service # ingress is also possible + - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. + - --provider=ibmcloud + - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud public zone + volumeMounts: + - name: ibmcloud-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: ibmcloud-config-file + secret: + secretName: ibmcloud-config-file + items: + - key: externaldns-config.json + path: ibmcloud.json +``` + +## Deploying an Nginx Service + +Create a service file called `nginx.yaml` with the following contents: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + annotations: + external-dns.alpha.kubernetes.io/hostname: www.example.com + external-dns.alpha.kubernetes.io/ttl: "120" #optional +spec: + selector: + app: nginx + type: LoadBalancer + ports: + - protocol: TCP + port: 80 + targetPort: 80 +``` + +Note the annotation on the service; use the hostname as the IBMCloud DNS zone created above. The annotation may also be a subdomain +of the DNS zone (e.g. 'www.example.com'). + +By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. +This annotation is optional, if you won't set it, it will be 1 (automatic) which is 300. + +ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation +will cause ExternalDNS to remove the corresponding DNS records. + +Create the deployment and service: + +``` +$ kubectl create -f nginx.yaml +``` + +Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. + +Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize +the IBMCloud DNS records. + +## Verifying Cloudflare DNS records + +Check your [Cloudflare dashboard](https://www.cloudflare.com/a/dns/example.com) to view the records for your Cloudflare DNS zone. + +Substitute the zone for the one created above if a different domain was used. + +This should show the external IP address of the service as the A record for your domain. + +## Cleanup + +Now that we have verified that ExternalDNS will automatically manage IBMCloud DNS records, we can delete the tutorial's example: + +``` +$ kubectl delete -f nginx.yaml +$ kubectl delete -f externaldns.yaml +``` + +## Setting proxied records on public zone + +Using the `external-dns.alpha.kubernetes.io/ibmcloud-proxied: "true"` annotation on your ingress or service, you can specify if the proxy feature of IBMCloud public DNS should be enabled for that record. This setting will override the global `--ibmcloud-proxied` setting. + +## Active priviate zone with VPC allocated + +By default, IBMCloud DNS Services don't active your private zone with new zone added, with externale DNS, you can use `external-dns.alpha.kubernetes.io/ibmcloud-vpc: "crn:v1:bluemix:public:is:us-south:a/bcf1865e99742d38d2d5fc3fb80a5496::vpc:r006-74353823-a60d-42e4-97c5-5e2551278435"` annotation on your ingress or service, it will active your private zone with in specific VPC for that record created in. this setting won't work if the private zone was active already. + +Note: the annotaion value is the VPC CRN, every IBM Cloud service have a valid CRN. \ No newline at end of file diff --git a/go.mod b/go.mod index 49b511c54..64e37a644 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,13 @@ require ( github.com/Azure/go-autorest/autorest v0.11.21 github.com/Azure/go-autorest/autorest/adal v0.9.16 github.com/Azure/go-autorest/autorest/to v0.4.0 + github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.8.1 + github.com/IBM/go-sdk-core/v5 v5.6.5 + github.com/IBM/networking-go-sdk v0.24.0 github.com/StackExchange/dnscontrol v0.2.8 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 + github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect + github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/kingpin v2.2.5+incompatible github.com/aliyun/alibaba-cloud-sdk-go v1.61.1483 github.com/aws/aws-sdk-go v1.42.52 @@ -34,6 +39,7 @@ require ( github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1 github.com/nesv/go-dynect v0.6.0 github.com/nic-at/rc0go v1.1.1 + github.com/onsi/ginkgo v1.16.5 github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73 github.com/oracle/oci-go-sdk v24.3.0+incompatible @@ -72,13 +78,12 @@ require ( github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Masterminds/semver v1.4.2 // indirect - github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect - github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/coreos/go-semver v0.3.0 // indirect @@ -86,9 +91,13 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-openapi/errors v0.19.8 // indirect + github.com/go-openapi/strfmt v0.20.1 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.0.0 // indirect @@ -113,13 +122,15 @@ require ( github.com/jcmturner/rpc/v2 v2.0.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.1.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -133,6 +144,7 @@ require ( github.com/terra-farm/udnssdk v1.3.5 // indirect github.com/ukfast/go-durationstring v1.1.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect + go.mongodb.org/mongo-driver v1.5.1 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect @@ -146,10 +158,11 @@ require ( google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect google.golang.org/grpc v1.45.0 // indirect google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/go-playground/validator.v9 v9.27.0 // indirect + gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/resty.v1 v1.12.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect k8s.io/klog/v2 v2.30.0 // indirect diff --git a/go.sum b/go.sum index 6bb0f4040..dfd385e35 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.8.1 h1:Nvwzsp7e0j9hj7JQ0ZJ0UkD6XMaryA/G4zDbJsRmGnw= +github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.8.1/go.mod h1:RiUvKuHKTBmBApDMUQzBL14pQUGKcx/IioKQPIcRQjs= +github.com/IBM/go-sdk-core/v5 v5.6.5 h1:zXlt5x/udqZTD617SKUQzPDON4x5y0+22vNxsdCGYpg= +github.com/IBM/go-sdk-core/v5 v5.6.5/go.mod h1:tt/B9rxLkRtglE7pvqLuYikgCXaZFL3btdruJaoUeek= +github.com/IBM/networking-go-sdk v0.24.0 h1:3AE23TBbcsB/2c15kuHuAnXlUom5FHMqxGxBRA94WS8= +github.com/IBM/networking-go-sdk v0.24.0/go.mod h1:vX/4URo6J6e6QCDhsntk6OAA4G27jp+v3+ZMb9WyBQY= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -188,11 +194,14 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.40.14/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.52 h1:/+TZ46+0qu9Ph/UwjVrU3SG8OBi87uJLrLiYRNZKbHQ= github.com/aws/aws-sdk-go v1.42.52/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= @@ -442,6 +451,8 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= @@ -473,6 +484,8 @@ github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pL github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.20.1 h1:1VgxvehFne1mbChGeCmZ5pc0LxUf6yaACVSIYAR91Xc= +github.com/go-openapi/strfmt v0.20.1/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= @@ -483,30 +496,56 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.2/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -777,6 +816,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -798,6 +838,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -805,6 +847,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -824,8 +867,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -851,7 +895,9 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= @@ -905,6 +951,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= @@ -923,6 +970,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/crd-schema-fuzz v1.0.0/go.mod h1:4z/rcm37JxUkSsExFcLL6ZIT1SgDRdLiu7qq1evdVS0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -950,6 +998,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -964,6 +1013,7 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -976,6 +1026,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= @@ -1022,6 +1073,7 @@ github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible/go.mod h1:xlUlxe/2It github.com/pavel-v-chernykh/keystore-go/v4 v4.1.0/go.mod h1:2ejgys4qY+iNVW1IittZhyRYA6MNv8TgM6VHqojbB9g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -1093,6 +1145,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -1121,6 +1174,7 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -1189,6 +1243,7 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1219,6 +1274,9 @@ github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 h1:UbVjBjgJUY github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= github.com/vultr/govultr/v2 v2.14.1 h1:Z4nd9mXNQ5wd63aw0MZOalFeTkJ8L6Sed3PTqagp4TA= github.com/vultr/govultr/v2 v2.14.1/go.mod h1:JjUljQdSZx+MELCAJvZ/JH32bJotmflnsyS0NOjb8Jg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= @@ -1229,6 +1287,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1263,6 +1322,8 @@ go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVd go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1322,6 +1383,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1333,6 +1395,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1423,6 +1486,7 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1473,6 +1537,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1500,11 +1565,13 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1627,9 +1694,13 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1893,8 +1964,9 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.27.0 h1:wCg/0hk9RzcB0CYw8pYV6FiBYug1on0cpco9YZF8jqA= gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= @@ -1929,6 +2001,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index c156edd8b..5202dceb8 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ import ( "sigs.k8s.io/external-dns/provider/gandi" "sigs.k8s.io/external-dns/provider/godaddy" "sigs.k8s.io/external-dns/provider/google" + "sigs.k8s.io/external-dns/provider/ibmcloud" "sigs.k8s.io/external-dns/provider/infoblox" "sigs.k8s.io/external-dns/provider/inmemory" "sigs.k8s.io/external-dns/provider/linode" @@ -322,6 +323,8 @@ func main() { p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun) case "gandi": p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun) + case "ibmcloud": + p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun) case "safedns": p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun) default: diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f559c84d5..dad5e778d 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -185,6 +185,8 @@ type Config struct { GoDaddyTTL int64 GoDaddyOTE bool OCPRouterName string + IBMCloudProxied bool + IBMCloudConfigFile string } var defaultConfig = &Config{ @@ -312,6 +314,8 @@ var defaultConfig = &Config{ GoDaddySecretKey: "", GoDaddyTTL: 600, GoDaddyOTE: false, + IBMCloudProxied: false, + IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", } // NewConfig returns new Config object @@ -397,7 +401,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets) // 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, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "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", "bluecat", "gandi", "safedns") + 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, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns") 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("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter) @@ -471,6 +475,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL) app.Flag("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.NS1MinTTLSeconds) app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize) + app.Flag("ibmcloud-config-file", "When using the IBM Cloud provider, specify the IBM Cloud configuration file (required when --provider=ibmcloud").Default(defaultConfig.IBMCloudConfigFile).StringVar(&cfg.IBMCloudConfigFile) + app.Flag("ibmcloud-proxied", "When using the IBM provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.IBMCloudProxied) // GoDaddy flags app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey) app.Flag("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)").Default(defaultConfig.GoDaddySecretKey).StringVar(&cfg.GoDaddySecretKey) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 7d0994c7e..06d1a9482 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -124,6 +124,8 @@ var ( ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, RFC2136BatchChangeSize: 50, OCPRouterName: "default", + IBMCloudProxied: false, + IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", } overriddenConfig = &Config{ @@ -227,6 +229,8 @@ var ( DigitalOceanAPIPageSize: 100, ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, RFC2136BatchChangeSize: 100, + IBMCloudProxied: true, + IBMCloudConfigFile: "ibmcloud.json", } ) @@ -358,6 +362,8 @@ func TestParseFlags(t *testing.T) { "--managed-record-types=CNAME", "--managed-record-types=NS", "--rfc2136-batch-change-size=100", + "--ibmcloud-proxied", + "--ibmcloud-config-file=ibmcloud.json", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -466,6 +472,8 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100", "EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nCNAME\nNS", "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", + "EXTERNAL_DNS_IBMCLOUD_PROXIED": "1", + "EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json", }, expected: overriddenConfig, }, diff --git a/provider/ibmcloud/ibmcloud.go b/provider/ibmcloud/ibmcloud.go new file mode 100644 index 000000000..0e8013075 --- /dev/null +++ b/provider/ibmcloud/ibmcloud.go @@ -0,0 +1,1015 @@ +/* +Copyright 2022 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 ibmcloud + +import ( + "context" + "fmt" + "io/ioutil" + "reflect" + "strconv" + "strings" + + "github.com/IBM-Cloud/ibm-cloud-cli-sdk/bluemix/crn" + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/networking-go-sdk/dnsrecordsv1" + "github.com/IBM/networking-go-sdk/dnssvcsv1" + "github.com/IBM/networking-go-sdk/zonesv1" + "gopkg.in/yaml.v2" + + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/source" +) + +var proxyTypeNotSupported = map[string]bool{ + "LOC": true, + "MX": true, + "NS": true, + "SPF": true, + "TXT": true, + "SRV": true, +} + +var privateTypeSupported = map[string]bool{ + "A": true, + "CNAME": true, + "TXT": true, +} + +const ( + // recordCreate is a ChangeAction enum value + recordCreate = "CREATE" + // recordDelete is a ChangeAction enum value + recordDelete = "DELETE" + // recordUpdate is a ChangeAction enum value + recordUpdate = "UPDATE" + // defaultPublicRecordTTL 1 = automatic + defaultPublicRecordTTL = 1 + + PROXY_FILTER = "ibmcloud-proxied" + VPC_FILTER = "ibmcloud-vpc" + ZONE_STATE_PENDING_NETWORK = "PENDING_NETWORK_ADD" + ZONE_STATE_ACTIVE = "ACTIVE" +) + +// Source shadow the interface source.Source. used primarily for unit testing. +type Source interface { + Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) + AddEventHandler(context.Context, func()) +} + +// ibmcloudClient is a minimal implementation of DNS API that we actually use, used primarily for unit testing. +type ibmcloudClient interface { + ListAllDnsRecordsWithContext(ctx context.Context, listAllDnsRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) + CreateDnsRecordWithContext(ctx context.Context, createDnsRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) + DeleteDnsRecordWithContext(ctx context.Context, deleteDnsRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) + UpdateDnsRecordWithContext(ctx context.Context, updateDnsRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) + ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) + GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) + CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) + ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) + CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) + DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) + UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) + NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) + NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) + NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) + NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) + NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) + NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) +} + +type ibmcloudService struct { + publicZonesService *zonesv1.ZonesV1 + publicRecordsService *dnsrecordsv1.DnsRecordsV1 + privateDnsService *dnssvcsv1.DnsSvcsV1 +} + +func (i ibmcloudService) ListAllDnsRecordsWithContext(ctx context.Context, listAllDnsRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) { + return i.publicRecordsService.ListAllDnsRecordsWithContext(ctx, listAllDnsRecordsOptions) +} + +func (i ibmcloudService) CreateDnsRecordWithContext(ctx context.Context, createDnsRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { + return i.publicRecordsService.CreateDnsRecordWithContext(ctx, createDnsRecordOptions) +} + +func (i ibmcloudService) DeleteDnsRecordWithContext(ctx context.Context, deleteDnsRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) { + return i.publicRecordsService.DeleteDnsRecordWithContext(ctx, deleteDnsRecordOptions) +} + +func (i ibmcloudService) UpdateDnsRecordWithContext(ctx context.Context, updateDnsRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { + return i.publicRecordsService.UpdateDnsRecordWithContext(ctx, updateDnsRecordOptions) +} + +func (i ibmcloudService) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) { + return i.privateDnsService.ListDnszonesWithContext(ctx, listDnszonesOptions) +} + +func (i ibmcloudService) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) { + return i.privateDnsService.GetDnszoneWithContext(ctx, getDnszoneOptions) +} + +func (i ibmcloudService) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) { + return i.privateDnsService.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) +} + +func (i ibmcloudService) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) { + return i.privateDnsService.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) +} + +func (i ibmcloudService) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { + return i.privateDnsService.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) +} + +func (i ibmcloudService) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) { + return i.privateDnsService.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) +} + +func (i ibmcloudService) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { + return i.privateDnsService.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) +} + +func (i ibmcloudService) NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) { + return i.privateDnsService.NewResourceRecordInputRdataRdataARecord(ip) +} + +func (i ibmcloudService) NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) { + return i.privateDnsService.NewResourceRecordInputRdataRdataCnameRecord(cname) +} + +func (i ibmcloudService) NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) { + return i.privateDnsService.NewResourceRecordInputRdataRdataTxtRecord(text) +} + +func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) { + return i.privateDnsService.NewResourceRecordUpdateInputRdataRdataARecord(ip) +} + +func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) { + return i.privateDnsService.NewResourceRecordUpdateInputRdataRdataCnameRecord(cname) +} + +func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) { + return i.privateDnsService.NewResourceRecordUpdateInputRdataRdataTxtRecord(text) +} + +// IBMCloudProvider is an implementation of Provider for IBM Cloud DNS. +type IBMCloudProvider struct { + provider.BaseProvider + source Source + Client ibmcloudClient + // only consider hosted zones managing domains ending in this suffix + domainFilter endpoint.DomainFilter + zoneIDFilter provider.ZoneIDFilter + instanceID string + privateZone bool + proxiedByDefault bool + DryRun bool +} + +type ibmcloudConfig struct { + Endpoint string `json:"endpoint" yaml:"endpoint"` + APIKey string `json:"apiKey" yaml:"apiKey"` + CRN string `json:"instanceCrn" yaml:"instanceCrn"` + IAMURL string `json:"iamUrl" yaml:"iamUrl"` + InstanceID string `json:"-" yaml:"-"` +} + +// ibmcloudChange differentiates between ChangActions +type ibmcloudChange struct { + Action string + PublicResourceRecord dnsrecordsv1.DnsrecordDetails + PrivateResourceRecord dnssvcsv1.ResourceRecord +} + +func getConfig(configFile string) (*ibmcloudConfig, error) { + contents, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err) + } + cfg := &ibmcloudConfig{} + err = yaml.Unmarshal(contents, &cfg) + if err != nil { + return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err) + } + + return cfg, nil +} + +func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter) (ibmcloudService, bool, error) { + var service ibmcloudService + isPrivate := false + log.Debugf("filters: %v, %v", domainFilter.Filters, zoneIDFilter.ZoneIDs) + if domainFilter.Filters[0] == "" && zoneIDFilter.ZoneIDs[0] == "" { + return service, isPrivate, fmt.Errorf("at lease one of filters: 'domain-filter', 'zone-id-filter' needed") + } + + crn, err := crn.Parse(c.CRN) + if err != nil { + return service, isPrivate, err + } + log.Infof("IBM Cloud Service: %s", crn.ServiceName) + c.InstanceID = crn.ServiceInstance + + switch { + case strings.Contains(crn.ServiceName, "internet-svcs"): + if len(domainFilter.Filters) > 1 || len(zoneIDFilter.ZoneIDs) > 1 { + return service, isPrivate, fmt.Errorf("for public zone, only one domain id filter or domain name filter allowed") + } + var zoneID string + // Public DNS service + service.publicZonesService, err = zonesv1.NewZonesV1(&zonesv1.ZonesV1Options{ + Authenticator: authenticator, + Crn: core.StringPtr(c.CRN), + }) + if err != nil { + return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public zones client: %v", err) + } + if c.Endpoint != "" { + service.publicZonesService.SetServiceURL(c.Endpoint) + } + + zonesResp, _, err := service.publicZonesService.ListZones(&zonesv1.ListZonesOptions{}) + if err != nil { + return service, isPrivate, fmt.Errorf("failed to list ibmcloud public zones: %v", err) + } + for _, zone := range zonesResp.Result { + log.Debugf("zoneName: %s, zoneID: %s", *zone.Name, *zone.ID) + if len(domainFilter.Filters[0]) != 0 && domainFilter.Match(*zone.Name) { + log.Debugf("zone %s found.", *zone.ID) + zoneID = *zone.ID + break + } + if len(zoneIDFilter.ZoneIDs[0]) != 0 && zoneIDFilter.Match(*zone.ID) { + log.Debugf("zone %s found.", *zone.ID) + zoneID = *zone.ID + break + } + } + if len(zoneID) == 0 { + return service, isPrivate, fmt.Errorf("no matched zone found") + } + + service.publicRecordsService, err = dnsrecordsv1.NewDnsRecordsV1(&dnsrecordsv1.DnsRecordsV1Options{ + Authenticator: authenticator, + Crn: core.StringPtr(c.CRN), + ZoneIdentifier: core.StringPtr(zoneID), + }) + if err != nil { + return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public records client: %v", err) + } + if c.Endpoint != "" { + service.publicRecordsService.SetServiceURL(c.Endpoint) + } + case strings.Contains(crn.ServiceName, "dns-svcs"): + isPrivate = true + // Private DNS service + service.privateDnsService, err = dnssvcsv1.NewDnsSvcsV1(&dnssvcsv1.DnsSvcsV1Options{ + Authenticator: authenticator, + }) + if err != nil { + return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud private records client: %v", err) + } + if c.Endpoint != "" { + service.privateDnsService.SetServiceURL(c.Endpoint) + } + default: + return service, isPrivate, fmt.Errorf("IBM Cloud instance crn is not provided or invalid dns crn : %s", c.CRN) + } + + return service, isPrivate, nil +} + +// NewIBMCloudProvider creates a new IBMCloud provider. +// +// Returns the provider or an error if a provider could not be created. +func NewIBMCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, source source.Source, proxiedByDefault bool, dryRun bool) (*IBMCloudProvider, error) { + cfg, err := getConfig(configFile) + if err != nil { + return nil, err + } + + authenticator := &core.IamAuthenticator{ + ApiKey: cfg.APIKey, + } + if cfg.IAMURL != "" { + authenticator = &core.IamAuthenticator{ + ApiKey: cfg.APIKey, + URL: cfg.IAMURL, + } + } + + client, isPrivate, err := cfg.Validate(authenticator, domainFilter, zoneIDFilter) + if err != nil { + return nil, err + } + + provider := &IBMCloudProvider{ + Client: client, + source: source, + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + instanceID: cfg.InstanceID, + privateZone: isPrivate, + proxiedByDefault: proxiedByDefault, + DryRun: dryRun, + } + return provider, nil +} + +// Records gets the current records. +// +// Returns the current records or an error if the operation failed. +func (p *IBMCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { + if p.privateZone { + endpoints, err = p.privateRecords(ctx) + } else { + endpoints, err = p.publicRecords(ctx) + } + return endpoints, err +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *IBMCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + log.Debugln("applying change...") + ibmcloudChanges := []*ibmcloudChange{} + for _, endpoint := range changes.Create { + for _, target := range endpoint.Targets { + ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, endpoint, target)) + } + } + + for i, desired := range changes.UpdateNew { + current := changes.UpdateOld[i] + + add, remove, leave := provider.Difference(current.Targets, desired.Targets) + + log.Debugf("add: %v, remove: %v, leave: %v", add, remove, leave) + for _, a := range add { + ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, desired, a)) + } + + for _, a := range leave { + ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordUpdate, desired, a)) + } + + for _, a := range remove { + ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, current, a)) + } + + } + + for _, endpoint := range changes.Delete { + for _, target := range endpoint.Targets { + ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, endpoint, target)) + } + } + + return p.submitChanges(ctx, ibmcloudChanges) +} + +func (p *IBMCloudProvider) PropertyValuesEqual(name string, previous string, current string) bool { + if name == PROXY_FILTER { + return plan.CompareBoolean(p.proxiedByDefault, name, previous, current) + } + + return p.BaseProvider.PropertyValuesEqual(name, previous, current) +} + +// AdjustEndpoints modifies the endpoints as needed by the specific provider +func (p *IBMCloudProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { + adjustedEndpoints := []*endpoint.Endpoint{} + for _, e := range endpoints { + log.Debugf("adjusting endpont: %v", *e) + if shouldBeProxied(e, p.proxiedByDefault) { + e.RecordTTL = 0 + } + + adjustedEndpoints = append(adjustedEndpoints, e) + } + return adjustedEndpoints +} + +// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. +func (p *IBMCloudProvider) submitChanges(ctx context.Context, changes []*ibmcloudChange) error { + // return early if there is nothing to change + if len(changes) == 0 { + return nil + } + + log.Debugln("submmiting change...") + if p.privateZone { + return p.submitChangesForPrivateDNS(ctx, changes) + } + return p.submitChangesForPublicDNS(ctx, changes) +} + +// submitChangesForPublicDNS takes a zone and a collection of Changes and sends them as a single transaction on public dns. +func (p *IBMCloudProvider) submitChangesForPublicDNS(ctx context.Context, changes []*ibmcloudChange) error { + records, err := p.listAllPublicRecords(ctx) + if err != nil { + return err + } + + for _, change := range changes { + logFields := log.Fields{ + "record": *change.PublicResourceRecord.Name, + "type": *change.PublicResourceRecord.Type, + "ttl": *change.PublicResourceRecord.TTL, + "action": change.Action, + } + + if p.DryRun { + continue + } + + log.WithFields(logFields).Info("Changing record.") + + if change.Action == recordUpdate { + recordID := p.getPublicRecordID(records, change.PublicResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) + continue + } + p.updateRecord(ctx, "", recordID, change) + } else if change.Action == recordDelete { + recordID := p.getPublicRecordID(records, change.PublicResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) + continue + } + p.deleteRecord(ctx, "", recordID) + } else if change.Action == recordCreate { + p.createRecord(ctx, "", change) + } + } + + return nil +} + +// submitChangesForPrivateDNS takes a zone and a collection of Changes and sends them as a single transaction on private dns. +func (p *IBMCloudProvider) submitChangesForPrivateDNS(ctx context.Context, changes []*ibmcloudChange) error { + zones, err := p.privateZones(ctx) + if err != nil { + return err + } + // separate into per-zone change sets to be passed to the API. + changesByPrivateZone := p.changesByPrivateZone(ctx, zones, changes) + + for zoneID, changes := range changesByPrivateZone { + records, err := p.listAllPrivateRecords(ctx, zoneID) + if err != nil { + return err + } + + for _, change := range changes { + + logFields := log.Fields{ + "record": *change.PrivateResourceRecord.Name, + "type": *change.PrivateResourceRecord.Type, + "ttl": *change.PrivateResourceRecord.TTL, + "action": change.Action, + } + + log.WithFields(logFields).Info("Changing record.") + + if p.DryRun { + continue + } + + if change.Action == recordUpdate { + recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) + continue + } + p.updateRecord(ctx, zoneID, recordID, change) + } else if change.Action == recordDelete { + recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) + if recordID == "" { + log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) + continue + } + p.deleteRecord(ctx, zoneID, recordID) + } else if change.Action == recordCreate { + p.createRecord(ctx, zoneID, change) + } + } + } + + return nil +} + +// privateZones return zones in private dns +func (p *IBMCloudProvider) privateZones(ctx context.Context) ([]dnssvcsv1.Dnszone, error) { + result := []dnssvcsv1.Dnszone{} + // if there is a zoneIDfilter configured + // && if the filter isn't just a blank string (used in tests) + if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" { + log.Debugln("zoneIDFilter configured. only looking up zone IDs defined") + for _, zoneID := range p.zoneIDFilter.ZoneIDs { + log.Debugf("looking up zone %s", zoneID) + detailResponse, _, err := p.Client.GetDnszoneWithContext(ctx, &dnssvcsv1.GetDnszoneOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + }) + if err != nil { + log.Errorf("zone %s lookup failed, %v", zoneID, err) + continue + } + log.WithFields(log.Fields{ + "zoneName": *detailResponse.Name, + "zoneID": *detailResponse.ID, + }).Debugln("adding zone for consideration") + result = append(result, *detailResponse) + } + return result, nil + } + + log.Debugln("no zoneIDFilter configured, looking at all zones") + + zonesResponse, _, err := p.Client.ListDnszonesWithContext(ctx, &dnssvcsv1.ListDnszonesOptions{ + InstanceID: core.StringPtr(p.instanceID), + }) + if err != nil { + return nil, err + } + + for _, zone := range zonesResponse.Dnszones { + if !p.domainFilter.Match(*zone.Name) { + log.Debugf("zone %s not in domain filter", *zone.Name) + continue + } + result = append(result, zone) + } + + return result, nil +} + +// activePrivateZone active zone with new records add if not active +func (p *IBMCloudProvider) activePrivateZone(ctx context.Context, zoneID, vpc string) { + permittedNetworkVpc := &dnssvcsv1.PermittedNetworkVpc{ + VpcCrn: core.StringPtr(vpc), + } + createPermittedNetworkOptions := &dnssvcsv1.CreatePermittedNetworkOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + PermittedNetwork: permittedNetworkVpc, + Type: core.StringPtr("vpc"), + } + _, _, err := p.Client.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) + if err != nil { + log.Errorf("failed to active zone %s in VPC %s with error: %v", zoneID, vpc, err) + } +} + +// changesByPrivateZone separates a multi-zone change into a single change per zone. +func (p *IBMCloudProvider) changesByPrivateZone(ctx context.Context, zones []dnssvcsv1.Dnszone, changeSet []*ibmcloudChange) map[string][]*ibmcloudChange { + changes := make(map[string][]*ibmcloudChange) + zoneNameIDMapper := provider.ZoneIDName{} + for _, z := range zones { + zoneNameIDMapper.Add(*z.ID, *z.Name) + changes[*z.ID] = []*ibmcloudChange{} + } + + for _, c := range changeSet { + zoneID, _ := zoneNameIDMapper.FindZone(*c.PrivateResourceRecord.Name) + if zoneID == "" { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", *c.PrivateResourceRecord.Name) + continue + } + changes[zoneID] = append(changes[zoneID], c) + } + + return changes +} + +func (p *IBMCloudProvider) publicRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { + log.Debugf("Listing records on public zone") + dnsRecords, err := p.listAllPublicRecords(ctx) + if err != nil { + return nil, err + } + return p.groupPublicRecords(dnsRecords), nil +} + +func (p *IBMCloudProvider) listAllPublicRecords(ctx context.Context) ([]dnsrecordsv1.DnsrecordDetails, error) { + var dnsRecords []dnsrecordsv1.DnsrecordDetails + page := 1 +GETRECORDS: + listAllDnsRecordsOptions := &dnsrecordsv1.ListAllDnsRecordsOptions{ + Page: core.Int64Ptr(int64(page)), + } + records, _, err := p.Client.ListAllDnsRecordsWithContext(ctx, listAllDnsRecordsOptions) + if err != nil { + return dnsRecords, err + } + dnsRecords = append(dnsRecords, records.Result...) + // Loop if more records exist + if *records.ResultInfo.TotalCount > int64(page*100) { + page = page + 1 + log.Debugf("More than one pages records found, page: %d", page) + goto GETRECORDS + } + return dnsRecords, nil +} + +func (p *IBMCloudProvider) groupPublicRecords(records []dnsrecordsv1.DnsrecordDetails) []*endpoint.Endpoint { + endpoints := []*endpoint.Endpoint{} + + // group supported records by name and type + groups := map[string][]dnsrecordsv1.DnsrecordDetails{} + + for _, r := range records { + if !provider.SupportedRecordType(*r.Type) { + continue + } + + groupBy := *r.Name + *r.Type + if _, ok := groups[groupBy]; !ok { + groups[groupBy] = []dnsrecordsv1.DnsrecordDetails{} + } + + groups[groupBy] = append(groups[groupBy], r) + } + + // create single endpoint with all the targets for each name/type + for _, records := range groups { + targets := make([]string, len(records)) + for i, record := range records { + targets[i] = *record.Content + } + + ep := endpoint.NewEndpointWithTTL( + *records[0].Name, + *records[0].Type, + endpoint.TTL(*records[0].TTL), + targets...).WithProviderSpecific(PROXY_FILTER, strconv.FormatBool(*records[0].Proxied)) + + log.Debugf( + "Found %s record for '%s' with target '%s'.", + ep.RecordType, + ep.DNSName, + ep.Targets, + ) + + endpoints = append(endpoints, ep) + } + return endpoints +} + +func (p *IBMCloudProvider) privateRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { + log.Debugf("Listing records on private zone") + var vpc string + zones, err := p.privateZones(ctx) + if err != nil { + return nil, err + } + sources, err := p.source.Endpoints(ctx) + if err != nil { + return nil, err + } + // Filter VPC annoation for private zone active + for _, source := range sources { + vpc = checkVPCAnnotation(source) + if len(vpc) > 0 { + log.Debugf("VPC found: %s", vpc) + break + } + } + + endpoints := []*endpoint.Endpoint{} + for _, zone := range zones { + if len(vpc) > 0 && *zone.State == ZONE_STATE_PENDING_NETWORK { + log.Debugf("active zone: %s", *zone.ID) + p.activePrivateZone(ctx, *zone.ID, vpc) + } + + dnsRecords, err := p.listAllPrivateRecords(ctx, *zone.ID) + if err != nil { + return nil, err + } + endpoints = append(endpoints, p.groupPrivateRecords(dnsRecords)...) + } + + return endpoints, nil +} + +func (p *IBMCloudProvider) listAllPrivateRecords(ctx context.Context, zoneID string) ([]dnssvcsv1.ResourceRecord, error) { + var dnsRecords []dnssvcsv1.ResourceRecord + offset := 0 +GETRECORDS: + listResourceRecordsOptions := &dnssvcsv1.ListResourceRecordsOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + Offset: core.Int64Ptr(int64(offset)), + } + records, _, err := p.Client.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) + if err != nil { + return dnsRecords, err + } + oRecords := records.ResourceRecords + dnsRecords = append(dnsRecords, oRecords...) + // Loop if more records exist + if int64(offset+1) < *records.TotalCount && int64(offset+200) < *records.TotalCount { + offset = offset + 200 + log.Debugf("More than one pages records found, page: %d", offset/200+1) + goto GETRECORDS + } + return dnsRecords, nil +} + +func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecord) []*endpoint.Endpoint { + endpoints := []*endpoint.Endpoint{} + // group supported records by name and type + groups := map[string][]dnssvcsv1.ResourceRecord{} + for _, r := range records { + if !provider.SupportedRecordType(*r.Type) || !privateTypeSupported[*r.Type] { + continue + } + rname := *r.Name + rtype := *r.Type + groupBy := rname + rtype + if _, ok := groups[groupBy]; !ok { + groups[groupBy] = []dnssvcsv1.ResourceRecord{} + } + + groups[groupBy] = append(groups[groupBy], r) + } + + // create single endpoint with all the targets for each name/type + for _, records := range groups { + targets := make([]string, len(records)) + for i, record := range records { + data := record.Rdata.(map[string]interface{}) + log.Debugf("record data: %v", data) + switch *record.Type { + case "A": + if !isNil(data["ip"]) { + targets[i] = data["ip"].(string) + } + case "CNAME": + if !isNil(data["cname"]) { + targets[i] = data["cname"].(string) + } + case "TXT": + if !isNil(data["text"]) { + targets[i] = data["text"].(string) + } + log.Debugf("text record data: %v", targets[i]) + } + } + + ep := endpoint.NewEndpointWithTTL( + *records[0].Name, + *records[0].Type, + endpoint.TTL(*records[0].TTL), targets...) + + log.Debugf( + "Found %s record for '%s' with target '%s'.", + ep.RecordType, + ep.DNSName, + ep.Targets, + ) + + endpoints = append(endpoints, ep) + } + return endpoints +} + +func (p *IBMCloudProvider) getPublicRecordID(records []dnsrecordsv1.DnsrecordDetails, record dnsrecordsv1.DnsrecordDetails) string { + for _, zoneRecord := range records { + if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type && *zoneRecord.Content == *record.Content { + return *zoneRecord.ID + } + } + return "" +} + +func (p *IBMCloudProvider) getPrivateRecordID(records []dnssvcsv1.ResourceRecord, record dnssvcsv1.ResourceRecord) string { + for _, zoneRecord := range records { + if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type { + return *zoneRecord.ID + } + } + return "" +} + +func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.Endpoint, target string) *ibmcloudChange { + ttl := defaultPublicRecordTTL + proxied := shouldBeProxied(endpoint, p.proxiedByDefault) + + if endpoint.RecordTTL.IsConfigured() { + ttl = int(endpoint.RecordTTL) + } + + if p.privateZone { + var rData interface{} + switch endpoint.RecordType { + case "A": + rData = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ + Ip: core.StringPtr(target), + } + case "CNAME": + rData = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ + Cname: core.StringPtr(target), + } + case "TXT": + rData = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ + Text: core.StringPtr(target), + } + } + return &ibmcloudChange{ + Action: action, + PrivateResourceRecord: dnssvcsv1.ResourceRecord{ + Name: core.StringPtr(endpoint.DNSName), + TTL: core.Int64Ptr(int64(ttl)), + Type: core.StringPtr(endpoint.RecordType), + Rdata: rData, + }, + } + } + + return &ibmcloudChange{ + Action: action, + PublicResourceRecord: dnsrecordsv1.DnsrecordDetails{ + Name: core.StringPtr(endpoint.DNSName), + TTL: core.Int64Ptr(int64(ttl)), + Proxied: core.BoolPtr(proxied), + Type: core.StringPtr(endpoint.RecordType), + Content: core.StringPtr(target), + }, + } +} + +func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, change *ibmcloudChange) { + if p.privateZone { + createResourceRecordOptions := &dnssvcsv1.CreateResourceRecordOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + Name: change.PrivateResourceRecord.Name, + Type: change.PrivateResourceRecord.Type, + TTL: change.PrivateResourceRecord.TTL, + } + switch *change.PrivateResourceRecord.Type { + case "A": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) + aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip) + createResourceRecordOptions.SetRdata(aData) + case "CNAME": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) + cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname) + createResourceRecordOptions.SetRdata(cnameData) + case "TXT": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) + txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text) + createResourceRecordOptions.SetRdata(txtData) + } + _, _, err := p.Client.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) + if err != nil { + log.Errorf("failed to create %s type record named %s: %v", *change.PrivateResourceRecord.Type, *change.PrivateResourceRecord.Name, err) + } + } else { + createDnsRecordOptions := &dnsrecordsv1.CreateDnsRecordOptions{ + Name: change.PublicResourceRecord.Name, + Type: change.PublicResourceRecord.Type, + TTL: change.PublicResourceRecord.TTL, + Content: change.PublicResourceRecord.Content, + } + _, _, err := p.Client.CreateDnsRecordWithContext(ctx, createDnsRecordOptions) + if err != nil { + log.Errorf("failed to create %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) + } + } +} + +func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID string, change *ibmcloudChange) { + if p.privateZone { + updateResourceRecordOptions := &dnssvcsv1.UpdateResourceRecordOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + RecordID: core.StringPtr(recordID), + Name: change.PrivateResourceRecord.Name, + TTL: change.PrivateResourceRecord.TTL, + } + switch *change.PrivateResourceRecord.Type { + case "A": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) + aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip) + updateResourceRecordOptions.SetRdata(aData) + case "CNAME": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) + cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname) + updateResourceRecordOptions.SetRdata(cnameData) + case "TXT": + data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) + txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text) + updateResourceRecordOptions.SetRdata(txtData) + } + _, _, err := p.Client.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) + if err != nil { + log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) + } + } else { + updateDnsRecordOptions := &dnsrecordsv1.UpdateDnsRecordOptions{ + DnsrecordIdentifier: &recordID, + Name: change.PublicResourceRecord.Name, + Type: change.PublicResourceRecord.Type, + TTL: change.PublicResourceRecord.TTL, + Content: change.PublicResourceRecord.Content, + Proxied: change.PublicResourceRecord.Proxied, + } + _, _, err := p.Client.UpdateDnsRecordWithContext(ctx, updateDnsRecordOptions) + if err != nil { + log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) + } + } +} + +func (p *IBMCloudProvider) deleteRecord(ctx context.Context, zoneID, recordID string) { + if p.privateZone { + deleteResourceRecordOptions := &dnssvcsv1.DeleteResourceRecordOptions{ + InstanceID: core.StringPtr(p.instanceID), + DnszoneID: core.StringPtr(zoneID), + RecordID: core.StringPtr(recordID), + } + _, err := p.Client.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) + if err != nil { + log.Errorf("failed to delete record %s: %v", recordID, err) + } + } else { + deleteDnsRecordOptions := &dnsrecordsv1.DeleteDnsRecordOptions{ + DnsrecordIdentifier: &recordID, + } + _, _, err := p.Client.DeleteDnsRecordWithContext(ctx, deleteDnsRecordOptions) + if err != nil { + log.Errorf("failed to delete record %s: %v", recordID, err) + } + } +} + +func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { + proxied := proxiedByDefault + + for _, v := range endpoint.ProviderSpecific { + if v.Name == PROXY_FILTER { + b, err := strconv.ParseBool(v.Value) + if err != nil { + log.Errorf("Failed to parse annotation [%s]: %v", PROXY_FILTER, err) + } else { + proxied = b + } + break + } + } + + if proxyTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") { + proxied = false + } + return proxied +} + +func checkVPCAnnotation(endpoint *endpoint.Endpoint) string { + var vpc string + for _, v := range endpoint.ProviderSpecific { + if v.Name == VPC_FILTER { + vpcCrn, err := crn.Parse(v.Value) + if vpcCrn.ResourceType != "vpc" || err != nil { + log.Errorf("Failed to parse vpc [%s]: %v", v.Value, err) + } else { + vpc = v.Value + } + break + } + } + return vpc +} + +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} diff --git a/provider/ibmcloud/ibmcloud_test.go b/provider/ibmcloud/ibmcloud_test.go new file mode 100644 index 000000000..d04f2d5c8 --- /dev/null +++ b/provider/ibmcloud/ibmcloud_test.go @@ -0,0 +1,942 @@ +/* +Copyright 2022 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 ibmcloud + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/networking-go-sdk/dnsrecordsv1" + + "github.com/IBM/networking-go-sdk/dnssvcsv1" + + . "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +func NewMockIBMCloudDNSAPI() *mockIbmcloudClientInterface { + // Setup public example responses + firstPublicRecord := dnsrecordsv1.DnsrecordDetails{ + ID: core.StringPtr("123"), + Name: core.StringPtr("test.example.com"), + Type: core.StringPtr("A"), + Content: core.StringPtr("1.2.3.4"), + Proxied: core.BoolPtr(true), + TTL: core.Int64Ptr(int64(120)), + } + secondPublicRecord := dnsrecordsv1.DnsrecordDetails{ + ID: core.StringPtr("456"), + Name: core.StringPtr("test.example.com"), + Type: core.StringPtr("TXT"), + Proxied: core.BoolPtr(false), + Content: core.StringPtr("\"heritage=external-dns,external-dns/owner=tower-pdns\""), + TTL: core.Int64Ptr(int64(120)), + } + publicRecordsResult := []dnsrecordsv1.DnsrecordDetails{firstPublicRecord, secondPublicRecord} + publicRecordsResultInfo := &dnsrecordsv1.ResultInfo{ + Page: core.Int64Ptr(int64(1)), + TotalCount: core.Int64Ptr(int64(1)), + } + + publicRecordsResp := &dnsrecordsv1.ListDnsrecordsResp{ + Result: publicRecordsResult, + ResultInfo: publicRecordsResultInfo, + } + // Setup private example responses + firstPrivateZone := dnssvcsv1.Dnszone{ + ID: core.StringPtr("123"), + Name: core.StringPtr("example.com"), + State: core.StringPtr(ZONE_STATE_PENDING_NETWORK), + } + + secondPrivateZone := dnssvcsv1.Dnszone{ + ID: core.StringPtr("456"), + Name: core.StringPtr("example1.com"), + State: core.StringPtr(ZONE_STATE_ACTIVE), + } + privateZones := []dnssvcsv1.Dnszone{firstPrivateZone, secondPrivateZone} + listZonesResp := &dnssvcsv1.ListDnszones{ + Dnszones: privateZones, + } + firstPrivateRecord := dnssvcsv1.ResourceRecord{ + ID: core.StringPtr("123"), + Name: core.StringPtr("test.example.com"), + Type: core.StringPtr("A"), + Rdata: map[string]interface{}{"ip": "1.2.3.4"}, + TTL: core.Int64Ptr(int64(120)), + } + secondPrivateRecord := dnssvcsv1.ResourceRecord{ + ID: core.StringPtr("456"), + Name: core.StringPtr("testCNAME.example.com"), + Type: core.StringPtr("CNAME"), + Rdata: map[string]interface{}{"cname": "test.example.com"}, + TTL: core.Int64Ptr(int64(120)), + } + thirdPrivateRecord := dnssvcsv1.ResourceRecord{ + ID: core.StringPtr("789"), + Name: core.StringPtr("test.example.com"), + Type: core.StringPtr("TXT"), + Rdata: map[string]interface{}{"text": "\"heritage=external-dns,external-dns/owner=tower-pdns\""}, + TTL: core.Int64Ptr(int64(120)), + } + privateRecords := []dnssvcsv1.ResourceRecord{firstPrivateRecord, secondPrivateRecord, thirdPrivateRecord} + privateRecordsResop := &dnssvcsv1.ListResourceRecords{ + ResourceRecords: privateRecords, + Offset: core.Int64Ptr(int64(0)), + TotalCount: core.Int64Ptr(int64(1)), + } + + // Setup record rData + inputARecord := &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ + Ip: core.StringPtr("1.2.3.4"), + } + inputCnameRecord := &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ + Cname: core.StringPtr("test.example.com"), + } + inputTxtRecord := &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ + Text: core.StringPtr("test"), + } + + updateARecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord{ + Ip: core.StringPtr("1.2.3.4"), + } + updateCnameRecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord{ + Cname: core.StringPtr("test.example.com"), + } + updateTxtRecord := &dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord{ + Text: core.StringPtr("test"), + } + + // Setup mock services + mockDNSClient := &mockIbmcloudClientInterface{} + mockDNSClient.On("CreateDnsRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("UpdateDnsRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("DeleteDnsRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("ListAllDnsRecordsWithContext", mock.Anything, mock.Anything).Return(publicRecordsResp, nil, nil) + mockDNSClient.On("ListDnszonesWithContext", mock.Anything, mock.Anything).Return(listZonesResp, nil, nil) + mockDNSClient.On("GetDnszoneWithContext", mock.Anything, mock.Anything).Return(&firstPrivateZone, nil, nil) + mockDNSClient.On("ListResourceRecordsWithContext", mock.Anything, mock.Anything).Return(privateRecordsResop, nil, nil) + mockDNSClient.On("CreatePermittedNetworkWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("CreateResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("DeleteResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("UpdateResourceRecordWithContext", mock.Anything, mock.Anything).Return(nil, nil, nil) + mockDNSClient.On("NewResourceRecordInputRdataRdataARecord", mock.Anything).Return(inputARecord, nil) + mockDNSClient.On("NewResourceRecordInputRdataRdataCnameRecord", mock.Anything).Return(inputCnameRecord, nil) + mockDNSClient.On("NewResourceRecordInputRdataRdataTxtRecord", mock.Anything).Return(inputTxtRecord, nil) + mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataARecord", mock.Anything).Return(updateARecord, nil) + mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataCnameRecord", mock.Anything).Return(updateCnameRecord, nil) + mockDNSClient.On("NewResourceRecordUpdateInputRdataRdataTxtRecord", mock.Anything).Return(updateTxtRecord, nil) + + return mockDNSClient +} + +func newTestIBMCloudProvider(private bool) *IBMCloudProvider { + mockSource := &mockSource{} + endpoints := []*endpoint.Endpoint{ + { + DNSName: "new.example.com", + Targets: endpoint.Targets{"4.3.2.1"}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-vpc", + Value: "crn:v1:staging:public:is:us-south:a/0821fa9f9ebcc7b7c9a0d6e9bf9442a4::vpc:be33cdad-9a03-4bfa-82ca-eadb9f1de688", + }, + }, + }, + } + mockSource.On("Endpoints", mock.Anything).Return(endpoints, nil, nil) + + domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) + + return &IBMCloudProvider{ + Client: NewMockIBMCloudDNSAPI(), + source: mockSource, + domainFilter: domainFilterTest, + DryRun: false, + instanceID: "test123", + privateZone: private, + } +} + +func TestPublic_Records(t *testing.T) { + p := newTestIBMCloudProvider(false) + endpoints, err := p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %++v", *endpoint) + } + } +} + +func TestPrivate_Records(t *testing.T) { + p := newTestIBMCloudProvider(true) + endpoints, err := p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 3 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %++v", *endpoint) + } + } +} + +func TestPublic_ApplyChanges(t *testing.T) { + p := newTestIBMCloudProvider(false) + + changes := plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "newA.example.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("4.3.2.1"), + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-proxied", + Value: "false", + }, + }, + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "A", + RecordTTL: 180, + Targets: endpoint.NewTargets("1.2.3.4"), + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-proxied", + Value: "false", + }, + }, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "A", + RecordTTL: 180, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-proxied", + Value: "true", + }, + }, + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), + }, + }, + } + ctx := context.Background() + err := p.ApplyChanges(ctx, &changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } +} + +func TestPrivate_ApplyChanges(t *testing.T) { + p := newTestIBMCloudProvider(true) + + changes := plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "newA.example.com", + RecordType: "A", + RecordTTL: 120, + Targets: endpoint.NewTargets("4.3.2.1"), + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-vpc", + Value: "crn:v1:staging:public:is:us-south:a/0821fa9f9ebcc7b7c9a0d6e9bf9442a4::vpc:be33cdad-9a03-4bfa-82ca-eadb9f1de688", + }, + }, + }, + { + DNSName: "newCNAME.example.com", + RecordType: "CNAME", + RecordTTL: 180, + Targets: endpoint.NewTargets("newA.example.com"), + }, + { + DNSName: "newTXT.example.com", + RecordType: "TXT", + RecordTTL: 240, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "A", + RecordTTL: 180, + Targets: endpoint.NewTargets("1.2.3.4"), + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "A", + RecordTTL: 180, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=tower-pdns\""), + }, + }, + } + ctx := context.Background() + err := p.ApplyChanges(ctx, &changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } +} + +func TestAdjustEndpoints(t *testing.T) { + p := newTestIBMCloudProvider(false) + endpoints := []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: 300, + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "ibmcloud-proxied", + Value: "true", + }, + }, + }, + } + + ep := p.AdjustEndpoints(endpoints) + + assert.Equal(t, endpoint.TTL(0), ep[0].RecordTTL) + assert.Equal(t, "test.example.com", ep[0].DNSName) +} + +func TestPrivateZone_withFilterID(t *testing.T) { + p := newTestIBMCloudProvider(true) + p.zoneIDFilter = provider.NewZoneIDFilter([]string{"123", "456"}) + + zones, err := p.privateZones(context.Background()) + if err != nil { + t.Errorf("should not fail, %s", err) + } else { + if len(zones) != 2 { + t.Errorf("Incorrect number of zones: %d", len(zones)) + } + for _, zone := range zones { + t.Logf("zone %s", *zone.ID) + } + } +} + +func TestPublicConfig_Validate(t *testing.T) { + // mock http server + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + defer GinkgoRecover() + time.Sleep(0) + + // Set mock response + res.Header().Set("Content-type", "application/json") + res.WriteHeader(200) + fmt.Fprintf(res, "%s", `{"success": true, "errors": [["Errors"]], "messages": [["Messages"]], "result": [{"id": "123", "created_on": "2014-01-01T05:20:00.12345Z", "modified_on": "2014-01-01T05:20:00.12345Z", "name": "example.com", "original_registrar": "GoDaddy", "original_dnshost": "NameCheap", "status": "active", "paused": false, "original_name_servers": ["ns1.originaldnshost.com"], "name_servers": ["ns001.name.cloud.ibm.com"]}], "result_info": {"page": 1, "per_page": 20, "count": 1, "total_count": 2000}}`) + })) + zoneIDFilterTest := provider.NewZoneIDFilter([]string{"123"}) + domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) + cfg := &ibmcloudConfig{ + Endpoint: testServer.URL, + CRN: "crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:a6338168-9510-4951-9d67-425612de96f0::", + } + crn := cfg.CRN + authenticator := &core.NoAuthAuthenticator{} + service, isPrivate, err := cfg.Validate(authenticator, domainFilterTest, provider.NewZoneIDFilter([]string{""})) + assert.NoError(t, err) + assert.Equal(t, false, isPrivate) + assert.Equal(t, crn, *service.publicRecordsService.Crn) + assert.Equal(t, "123", *service.publicRecordsService.ZoneIdentifier) + + service, isPrivate, err = cfg.Validate(authenticator, endpoint.NewDomainFilter([]string{""}), zoneIDFilterTest) + assert.NoError(t, err) + assert.Equal(t, false, isPrivate) + assert.Equal(t, crn, *service.publicRecordsService.Crn) + assert.Equal(t, "123", *service.publicRecordsService.ZoneIdentifier) + + testServer.Close() +} + +func TestPrivateConfig_Validate(t *testing.T) { + zoneIDFilterTest := provider.NewZoneIDFilter([]string{"123"}) + domainFilterTest := endpoint.NewDomainFilter([]string{"example.com"}) + authenticator := &core.NoAuthAuthenticator{} + cfg := &ibmcloudConfig{ + Endpoint: "XXX", + CRN: "crn:v1:bluemix:public:dns-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:a6338168-9510-4951-9d67-425612de96f0::", + } + _, isPrivate, err := cfg.Validate(authenticator, domainFilterTest, zoneIDFilterTest) + assert.NoError(t, err) + assert.Equal(t, true, isPrivate) +} + +// mockIbmcloudClientInterface is an autogenerated mock type for the ibmcloudClient type +type mockIbmcloudClientInterface struct { + mock.Mock +} + +// CreateDnsRecordWithContext provides a mock function with given fields: ctx, createDnsRecordOptions +func (_m *mockIbmcloudClientInterface) CreateDnsRecordWithContext(ctx context.Context, createDnsRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (*dnsrecordsv1.DnsrecordResp, *core.DetailedResponse, error) { + ret := _m.Called(ctx, createDnsRecordOptions) + + var r0 *dnsrecordsv1.DnsrecordResp + if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) *dnsrecordsv1.DnsrecordResp); ok { + r0 = rf(ctx, createDnsRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnsrecordsv1.DnsrecordResp) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, createDnsRecordOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.CreateDnsRecordOptions) error); ok { + r2 = rf(ctx, createDnsRecordOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CreatePermittedNetworkWithContext provides a mock function with given fields: ctx, createPermittedNetworkOptions +func (_m *mockIbmcloudClientInterface) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (*dnssvcsv1.PermittedNetwork, *core.DetailedResponse, error) { + ret := _m.Called(ctx, createPermittedNetworkOptions) + + var r0 *dnssvcsv1.PermittedNetwork + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) *dnssvcsv1.PermittedNetwork); ok { + r0 = rf(ctx, createPermittedNetworkOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.PermittedNetwork) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, createPermittedNetworkOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.CreatePermittedNetworkOptions) error); ok { + r2 = rf(ctx, createPermittedNetworkOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CreateResourceRecordWithContext provides a mock function with given fields: ctx, createResourceRecordOptions +func (_m *mockIbmcloudClientInterface) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (*dnssvcsv1.ResourceRecord, *core.DetailedResponse, error) { + ret := _m.Called(ctx, createResourceRecordOptions) + + var r0 *dnssvcsv1.ResourceRecord + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) *dnssvcsv1.ResourceRecord); ok { + r0 = rf(ctx, createResourceRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecord) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, createResourceRecordOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.CreateResourceRecordOptions) error); ok { + r2 = rf(ctx, createResourceRecordOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// DeleteDnsRecordWithContext provides a mock function with given fields: ctx, deleteDnsRecordOptions +func (_m *mockIbmcloudClientInterface) DeleteDnsRecordWithContext(ctx context.Context, deleteDnsRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (*dnsrecordsv1.DeleteDnsrecordResp, *core.DetailedResponse, error) { + ret := _m.Called(ctx, deleteDnsRecordOptions) + + var r0 *dnsrecordsv1.DeleteDnsrecordResp + if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) *dnsrecordsv1.DeleteDnsrecordResp); ok { + r0 = rf(ctx, deleteDnsRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnsrecordsv1.DeleteDnsrecordResp) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, deleteDnsRecordOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.DeleteDnsRecordOptions) error); ok { + r2 = rf(ctx, deleteDnsRecordOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// DeleteResourceRecordWithContext provides a mock function with given fields: ctx, deleteResourceRecordOptions +func (_m *mockIbmcloudClientInterface) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (*core.DetailedResponse, error) { + ret := _m.Called(ctx, deleteResourceRecordOptions) + + var r0 *core.DetailedResponse + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.DeleteResourceRecordOptions) *core.DetailedResponse); ok { + r0 = rf(ctx, deleteResourceRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.DetailedResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.DeleteResourceRecordOptions) error); ok { + r1 = rf(ctx, deleteResourceRecordOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetDnszoneWithContext provides a mock function with given fields: ctx, getDnszoneOptions +func (_m *mockIbmcloudClientInterface) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (*dnssvcsv1.Dnszone, *core.DetailedResponse, error) { + ret := _m.Called(ctx, getDnszoneOptions) + + var r0 *dnssvcsv1.Dnszone + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) *dnssvcsv1.Dnszone); ok { + r0 = rf(ctx, getDnszoneOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.Dnszone) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, getDnszoneOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.GetDnszoneOptions) error); ok { + r2 = rf(ctx, getDnszoneOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListAllDnsRecordsWithContext provides a mock function with given fields: ctx, listAllDnsRecordsOptions +func (_m *mockIbmcloudClientInterface) ListAllDnsRecordsWithContext(ctx context.Context, listAllDnsRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (*dnsrecordsv1.ListDnsrecordsResp, *core.DetailedResponse, error) { + ret := _m.Called(ctx, listAllDnsRecordsOptions) + + var r0 *dnsrecordsv1.ListDnsrecordsResp + if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) *dnsrecordsv1.ListDnsrecordsResp); ok { + r0 = rf(ctx, listAllDnsRecordsOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnsrecordsv1.ListDnsrecordsResp) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, listAllDnsRecordsOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.ListAllDnsRecordsOptions) error); ok { + r2 = rf(ctx, listAllDnsRecordsOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListDnszonesWithContext provides a mock function with given fields: ctx, listDnszonesOptions +func (_m *mockIbmcloudClientInterface) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (*dnssvcsv1.ListDnszones, *core.DetailedResponse, error) { + ret := _m.Called(ctx, listDnszonesOptions) + + var r0 *dnssvcsv1.ListDnszones + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) *dnssvcsv1.ListDnszones); ok { + r0 = rf(ctx, listDnszonesOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ListDnszones) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, listDnszonesOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.ListDnszonesOptions) error); ok { + r2 = rf(ctx, listDnszonesOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListResourceRecordsWithContext provides a mock function with given fields: ctx, listResourceRecordsOptions +func (_m *mockIbmcloudClientInterface) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (*dnssvcsv1.ListResourceRecords, *core.DetailedResponse, error) { + ret := _m.Called(ctx, listResourceRecordsOptions) + + var r0 *dnssvcsv1.ListResourceRecords + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) *dnssvcsv1.ListResourceRecords); ok { + r0 = rf(ctx, listResourceRecordsOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ListResourceRecords) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, listResourceRecordsOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.ListResourceRecordsOptions) error); ok { + r2 = rf(ctx, listResourceRecordsOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewResourceRecordInputRdataRdataARecord provides a mock function with given fields: ip +func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataARecord(ip string) (*dnssvcsv1.ResourceRecordInputRdataRdataARecord, error) { + ret := _m.Called(ip) + + var r0 *dnssvcsv1.ResourceRecordInputRdataRdataARecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataARecord); ok { + r0 = rf(ip) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(ip) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewResourceRecordInputRdataRdataCnameRecord provides a mock function with given fields: cname +func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataCnameRecord(cname string) (*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, error) { + ret := _m.Called(cname) + + var r0 *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord); ok { + r0 = rf(cname) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(cname) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewResourceRecordInputRdataRdataTxtRecord provides a mock function with given fields: text +func (_m *mockIbmcloudClientInterface) NewResourceRecordInputRdataRdataTxtRecord(text string) (*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, error) { + ret := _m.Called(text) + + var r0 *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord); ok { + r0 = rf(text) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(text) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewResourceRecordUpdateInputRdataRdataARecord provides a mock function with given fields: ip +func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, error) { + ret := _m.Called(ip) + + var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord); ok { + r0 = rf(ip) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(ip) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewResourceRecordUpdateInputRdataRdataCnameRecord provides a mock function with given fields: cname +func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, error) { + ret := _m.Called(cname) + + var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord); ok { + r0 = rf(cname) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(cname) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewResourceRecordUpdateInputRdataRdataTxtRecord provides a mock function with given fields: text +func (_m *mockIbmcloudClientInterface) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (*dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, error) { + ret := _m.Called(text) + + var r0 *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord + if rf, ok := ret.Get(0).(func(string) *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord); ok { + r0 = rf(text) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(text) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateDnsRecordWithContext provides a mock function with given fields: ctx, updateDnsRecordOptions +func (_m *mockIbmcloudClientInterface) UpdateDnsRecordWithContext(ctx context.Context, updateDnsRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (*dnsrecordsv1.DnsrecordResp, *core.DetailedResponse, error) { + ret := _m.Called(ctx, updateDnsRecordOptions) + + var r0 *dnsrecordsv1.DnsrecordResp + if rf, ok := ret.Get(0).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) *dnsrecordsv1.DnsrecordResp); ok { + r0 = rf(ctx, updateDnsRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnsrecordsv1.DnsrecordResp) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, updateDnsRecordOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnsrecordsv1.UpdateDnsRecordOptions) error); ok { + r2 = rf(ctx, updateDnsRecordOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UpdateResourceRecordWithContext provides a mock function with given fields: ctx, updateResourceRecordOptions +func (_m *mockIbmcloudClientInterface) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (*dnssvcsv1.ResourceRecord, *core.DetailedResponse, error) { + ret := _m.Called(ctx, updateResourceRecordOptions) + + var r0 *dnssvcsv1.ResourceRecord + if rf, ok := ret.Get(0).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) *dnssvcsv1.ResourceRecord); ok { + r0 = rf(ctx, updateResourceRecordOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dnssvcsv1.ResourceRecord) + } + } + + var r1 *core.DetailedResponse + if rf, ok := ret.Get(1).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) *core.DetailedResponse); ok { + r1 = rf(ctx, updateResourceRecordOptions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*core.DetailedResponse) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *dnssvcsv1.UpdateResourceRecordOptions) error); ok { + r2 = rf(ctx, updateResourceRecordOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockSource struct { + mock.Mock +} + +// Endpoints provides a mock function with given fields: ctx +func (_m *mockSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { + ret := _m.Called(ctx) + + var r0 []*endpoint.Endpoint + if rf, ok := ret.Get(0).(func(context.Context) []*endpoint.Endpoint); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*endpoint.Endpoint) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AddEventHandler provides a mock function with given fields: _a0, _a1 +func (_m *mockSource) AddEventHandler(_a0 context.Context, _a1 func()) { + _m.Called(_a0, _a1) +} diff --git a/source/source.go b/source/source.go index 91214b5f3..dc3c26d65 100644 --- a/source/source.go +++ b/source/source.go @@ -196,6 +196,12 @@ func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.Pro Name: fmt.Sprintf("scw/%s", attr), Value: v, }) + } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") { + attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") + providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ + Name: fmt.Sprintf("ibmcloud-%s", attr), + Value: v, + }) } } return providerSpecificAnnotations, setIdentifier