diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml index 064d2de31..b8425afa7 100644 --- a/.github/workflows/lint-test-chart.yaml +++ b/.github/workflows/lint-test-chart.yaml @@ -15,10 +15,19 @@ jobs: with: fetch-depth: 0 + - name: Run Artifact Hub lint + shell: bash + run: | + set -euo pipefail + curl -Lo ah_linux_amd64.tar.gz https://github.com/artifacthub/hub/releases/download/v1.6.0/ah_1.6.0_linux_amd64.tar.gz + tar -xzvf ah_linux_amd64.tar.gz ah + ./ah lint --kind helm || exit 1 + rm -f ./ah ./ah_linux_amd64.tar.gz + - name: Set up Helm uses: azure/setup-helm@v1 with: - version: v3.6.3 + version: 3.* - name: Set up Python uses: actions/setup-python@v2 @@ -26,7 +35,7 @@ jobs: python-version: 3.7 - name: Set up chart-testing - uses: helm/chart-testing-action@v2.1.0 + uses: helm/chart-testing-action@v2.2.0 - name: Run chart-testing (list-changed) id: list-changed diff --git a/.github/workflows/release-chart.yaml b/.github/workflows/release-chart.yaml index 34bd1e779..5ec348654 100644 --- a/.github/workflows/release-chart.yaml +++ b/.github/workflows/release-chart.yaml @@ -17,6 +17,27 @@ jobs: with: fetch-depth: 0 + - name: Get chart version + id: chart_version + shell: bash + run: | + set -euo pipefail + chart_version="$(grep -Po "(?<=^version: ).+" charts/external-dns/Chart.yaml)" + echo "::set-output name=version::${chart_version}" + + - name: Get changelog entry + id: changelog_reader + uses: mindsers/changelog-reader-action@v2 + with: + path: charts/external-dns/CHANGELOG.md + version: "v${{ steps.chart_version.outputs.version }}" + + - name: Create release notes + shell: bash + run: | + set -euo pipefail + printf '${{ steps.changelog_reader.outputs.changes }}' > charts/external-dns/_release-notes.md + - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" @@ -28,7 +49,8 @@ jobs: version: v3.6.3 - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.2.1 + uses: helm/chart-releaser-action@v1.3.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_RELEASE_NAME_TEMPLATE: "external-dns-helm-chart-{{ .Version }}" + CR_RELEASE_NOTES_FILE: _release-notes.md diff --git a/README.md b/README.md index 84d174824..5b2fea0b3 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ ExternalDNS' allows you to keep selected zones (via `--domain-filter`) synchroni * [CloudFlare](https://www.cloudflare.com/dns) * [RcodeZero](https://www.rcodezero.at/) * [DigitalOcean](https://www.digitalocean.com/products/networking) -* [Hetzner](https://hetzner.com/) * [DNSimple](https://dnsimple.com/) * [Infoblox](https://www.infoblox.com/products/dns/) * [Dyn](https://dyn.com/dns/) @@ -89,7 +88,6 @@ The following table clarifies the current status of the providers according to t | CloudFlare | Beta | | | RcodeZero | Alpha | | | DigitalOcean | Alpha | | -| Hetzner | Alpha | @21h | | DNSimple | Alpha | | | Infoblox | Alpha | @saileshgiri | | Dyn | Alpha | | @@ -147,7 +145,6 @@ The following tutorials are provided: * [BlueCat](docs/tutorials/bluecat.md) * [CoreDNS](docs/tutorials/coredns.md) * [DigitalOcean](docs/tutorials/digitalocean.md) -* [Hetzner](docs/tutorials/hetzner.md) * [DNSimple](docs/tutorials/dnsimple.md) * [Dyn](docs/tutorials/dyn.md) * [Exoscale](docs/tutorials/exoscale.md) diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md new file mode 100644 index 000000000..39f0df3c9 --- /dev/null +++ b/charts/external-dns/CHANGELOG.md @@ -0,0 +1,24 @@ +# ExternalDNS Helm Chart Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + + + +## [v1.8.0] - UNRELEASED + +### Added + +- Add annotations to Deployment. [#2477](https://github.com/kubernetes-sigs/external-dns/pull/2477) from @beastob + +### Changed + +- Fix RBAC for `istio-virtualservice` source when `istio-gateway` isn't also added. [#2564](https://github.com/kubernetes-sigs/external-dns/pull/2564) from @mcwarman diff --git a/charts/external-dns/Chart.yaml b/charts/external-dns/Chart.yaml index 612755951..7d61a3c69 100644 --- a/charts/external-dns/Chart.yaml +++ b/charts/external-dns/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: external-dns description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers. type: application -version: 1.7.1 +version: 1.8.0 appVersion: 0.10.2 keywords: - kubernetes @@ -18,8 +18,6 @@ maintainers: annotations: artifacthub.io/changes: | - kind: added - description: "Allow custom ClusterRole rules to be specified for sources without defaults." + description: "Add annotations to Deployment." - kind: changed - description: "Update ExternalDNS version to v0.10.2." - - kind: changed - description: "Set ClusterRole rules based more enabled sources." + description: "Fix RBAC for istio-virtualservice source when istio-gateway isn't also added." diff --git a/docs/contributing/chart.md b/docs/contributing/chart.md index da16c6d14..5d653725f 100644 --- a/docs/contributing/chart.md +++ b/docs/contributing/chart.md @@ -3,3 +3,5 @@ ## Chart Changes When contributing chart changes please follow the same process as when contributing other content but also please **DON'T** modify _Chart.yaml_ in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart. + +Please **DO** add your changes to the _CHANGELOG.md_ file in the chart directory under the `## [UNRELEASED]` section, if there isn't an uncommented `## [UNRELEASED]` section please copy the commented out template and use that. diff --git a/docs/tutorials/hetzner.md b/docs/tutorials/hetzner.md deleted file mode 100644 index dbae14e98..000000000 --- a/docs/tutorials/hetzner.md +++ /dev/null @@ -1,191 +0,0 @@ -# Setting up ExternalDNS for Services on Hetzner DNS - -This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Hetzner DNS. - -Make sure to use **>=0.7.6** version of ExternalDNS for this tutorial. - -## Creating a Hetzner DNS zone - -If you want to learn about how to use Hetzner's DNS service read the following tutorial series: - -[An Introduction to Managing DNS](https://wiki.hetzner.de/index.php/DNS_Overview), and [Add a new DNS zone](https://wiki.hetzner.de/index.php/Getting_started). - -Create a new DNS zone where you want to create your records in. Let's use `example.com` as an example here. - -## Creating Hetzner Credentials - -Generate a new personal token by going to [the API settings](https://dns.hetzner.com/settings/api-token) or follow [Generating an API access token](https://wiki.hetzner.de/index.php/API_access_token) if you need more information. Give the token a name and choose read and write access. The token needs to be passed to ExternalDNS so make a note of it for later use. - -The environment variable `HETZNER_TOKEN` will be needed to run ExternalDNS with Hetzner. - -## 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: - replicas: 1 - selector: - matchLabels: - app: external-dns - strategy: - type: Recreate - template: - metadata: - labels: - app: external-dns - spec: - containers: - - name: external-dns - image: k8s.gcr.io/external-dns/external-dns:v0.7.6 - 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=hetzner - env: - - name: HETZNER_TOKEN - value: "YOUR_HETZNER_DNS_API_KEY" -``` - -### 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: - replicas: 1 - selector: - matchLabels: - app: external-dns - strategy: - type: Recreate - template: - metadata: - labels: - app: external-dns - spec: - serviceAccountName: external-dns - containers: - - name: external-dns - image: k8s.gcr.io/external-dns/external-dns:v0.7.6 - 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=hetzner - env: - - name: HETZNER_TOKEN - value: "YOUR_HETZNER_DNS_API_KEY" -``` - - -## 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: - replicas: 1 - 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: my-app.example.com -spec: - selector: - app: nginx - type: LoadBalancer - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Note the annotation on the service; use the same hostname as the Hetzner DNS zone created above. - -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: - -```console -$ 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 Hetzner DNS records. - -## Verifying Hetzner DNS records - -Check your [Hetzner DNS UI](https://dns.hetzner.com/) to view the records for your Hetzner DNS zone. - -Click on 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 Hetzner DNS records, we can delete the tutorial's example: - -``` -$ kubectl delete service -f nginx.yaml -$ kubectl delete service -f externaldns.yaml -``` diff --git a/go.mod b/go.mod index f7bf2c67e..aefd07cd7 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.17 require ( cloud.google.com/go/compute v1.2.0 - git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d github.com/Azure/azure-sdk-for-go v61.5.0+incompatible github.com/Azure/go-autorest/autorest v0.11.21 github.com/Azure/go-autorest/autorest/adal v0.9.16 @@ -46,7 +45,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 github.com/transip/gotransip/v6 v6.14.0 - github.com/ukfast/sdk-go v1.4.23 + github.com/ukfast/sdk-go v1.4.34 github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 github.com/vultr/govultr/v2 v2.14.1 @@ -132,7 +131,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/terra-farm/udnssdk v1.3.5 // indirect - github.com/ukfast/go-durationstring v1.0.0 // indirect + github.com/ukfast/go-durationstring v1.1.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.7.0 // indirect diff --git a/go.sum b/go.sum index 6898f0a01..ebc30518e 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d h1:d6sdozgfqtgaOhjUn++lbo5siX3HELjcOUnbtrvVQi4= -git.blindage.org/21h/hcloud-dns v0.0.0-20200807003420-f768ffe03f8d/go.mod h1:n26Twiii5jhkMC+Ocz/s8R73cBBcXRIwyTqQ+6bOZGo= git.lukeshu.com/go/libsystemd v0.5.3/go.mod h1:FfDoP0i92r4p5Vn4NCLxvjkd7rCOe6otPa4L6hZg9WM= github.com/0x4c6565/genie v1.0.0/go.mod h1:fDOjW0hFamMWOIkh4irf2D/TZpXXWMFtpP8MfgK0N3c= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -1200,10 +1198,10 @@ github.com/tsaarni/x500dn v0.0.0-20210331182804-14283c7f5a16/go.mod h1:RquKZ5rER github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ukfast/go-durationstring v1.0.0 h1:kgPuA7XjLjgLDfkG8j0MpolxcZh/eMdiVoOIFD/uc5I= -github.com/ukfast/go-durationstring v1.0.0/go.mod h1:Ci81n51kfxlKUIaLY9cINIKRO94VTqV+iCGbOMTb0V8= -github.com/ukfast/sdk-go v1.4.23 h1:dLZmHW2jgV0QQ2TGGdbL2tYVdtQPcuUub7Rzh+6Cqic= -github.com/ukfast/sdk-go v1.4.23/go.mod h1:tspweEP77MHhVEYgEEieKAKGITFgwkYl1q5fLh4HZAo= +github.com/ukfast/go-durationstring v1.1.0 h1:Ki0ubc5jqSt7XuAs+gkPNpHYolIwbcsRW4LS239tIHA= +github.com/ukfast/go-durationstring v1.1.0/go.mod h1:Ci81n51kfxlKUIaLY9cINIKRO94VTqV+iCGbOMTb0V8= +github.com/ukfast/sdk-go v1.4.34 h1:xuNbJ+WxsUqBfrm6eEdnTi6GcKL3R1cLxSN0jMk+4Rc= +github.com/ukfast/sdk-go v1.4.34/go.mod h1:vxlI1IHy2pp04AYqRMm0MHWSWOF0lwTkPJXHxTDLPok= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 h1:n7unetnX8WWTc0U85h/0+dJoLWLqoaJwowXB9RkBdxU= github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60/go.mod h1:43vmy6GEvRuVMpGEWfJ/JoEM6RIqUQI1/tb8JqZR1zI= diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index df5f4aa2a..943b16717 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -3,7 +3,7 @@ kind: Kustomization images: - name: k8s.gcr.io/external-dns/external-dns - newTag: v0.10.2 + newTag: v0.11.0 resources: - ./external-dns-deployment.yaml diff --git a/main.go b/main.go index fd3b48465..a4f5ed00b 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,6 @@ 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/hetzner" "sigs.k8s.io/external-dns/provider/infoblox" "sigs.k8s.io/external-dns/provider/inmemory" "sigs.k8s.io/external-dns/provider/linode" @@ -227,8 +226,6 @@ func main() { p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun) case "digitalocean": p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize) - case "hetzner": - p, err = hetzner.NewHetznerProvider(ctx, domainFilter, cfg.DryRun) case "ovh": p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun) case "linode": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 3b06e3a00..f559c84d5 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -397,7 +397,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, hetzner, 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", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "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, 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("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) diff --git a/provider/hetzner/hetzner.go b/provider/hetzner/hetzner.go deleted file mode 100644 index 5f367bb2c..000000000 --- a/provider/hetzner/hetzner.go +++ /dev/null @@ -1,259 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package hetzner - -import ( - "context" - "errors" - "os" - "strings" - - hclouddns "git.blindage.org/21h/hcloud-dns" - log "github.com/sirupsen/logrus" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -const ( - hetznerCreate = "CREATE" - hetznerDelete = "DELETE" - hetznerUpdate = "UPDATE" - hetznerTTL = 600 -) - -type HetznerChanges struct { - Action string - ZoneID string - ZoneName string - ResourceRecordSet hclouddns.HCloudRecord -} - -type HetznerProvider struct { - provider.BaseProvider - Client hclouddns.HCloudClientAdapter - domainFilter endpoint.DomainFilter - DryRun bool -} - -func NewHetznerProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*HetznerProvider, error) { - token, ok := os.LookupEnv("HETZNER_TOKEN") - if !ok { - return nil, errors.New("no environment variable HETZNER_TOKEN provided") - } - - client := hclouddns.New(token) - - provider := &HetznerProvider{ - Client: client, - domainFilter: domainFilter, - DryRun: dryRun, - } - return provider, nil -} - -func (p *HetznerProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - zones, err := p.Client.GetZones(hclouddns.HCloudGetZonesParams{}) - if err != nil { - return nil, err - } - endpoints := []*endpoint.Endpoint{} - for _, zone := range zones.Zones { - records, err := p.Client.GetRecords(hclouddns.HCloudGetRecordsParams{ZoneID: zone.ID}) - if err != nil { - return nil, err - } - - for _, r := range records.Records { - if provider.SupportedRecordType(string(r.RecordType)) { - name := r.Name + "." + zone.Name - - if r.Name == "@" { - name = zone.Name - } - - endpoints = append(endpoints, endpoint.NewEndpoint(name, string(r.RecordType), r.Value)) - } - } - } - - return endpoints, nil -} - -func (p *HetznerProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - combinedChanges := make([]*HetznerChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) - - combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerCreate, changes.Create)...) - combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerUpdate, changes.UpdateNew)...) - combinedChanges = append(combinedChanges, p.newHetznerChanges(hetznerDelete, changes.Delete)...) - - return p.submitChanges(ctx, combinedChanges) -} - -func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerChanges) error { - if len(changes) == 0 { - log.Infof("All records are already up to date") - return nil - } - - zones, err := p.Client.GetZones(hclouddns.HCloudGetZonesParams{}) - if err != nil { - return err - } - - zoneChanges := p.seperateChangesByZone(zones.Zones, changes) - - for _, changes := range zoneChanges { - for _, change := range changes { - // Prepare record name - recordName := strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName) - if recordName == change.ZoneName { - recordName = "@" - } - if change.ResourceRecordSet.RecordType == hclouddns.CNAME && !strings.HasSuffix(change.ResourceRecordSet.Value, ".") { - change.ResourceRecordSet.Value += "." - } - change.ResourceRecordSet.Name = recordName - - // Get ID of record if not create operation - if change.Action != hetznerCreate { - allRecords, err := p.Client.GetRecords(hclouddns.HCloudGetRecordsParams{ZoneID: change.ZoneID}) - if err != nil { - return err - } - for _, record := range allRecords.Records { - if record.Name == change.ResourceRecordSet.Name && record.RecordType == change.ResourceRecordSet.RecordType { - change.ResourceRecordSet.ID = record.ID - break - } - } - } - - logMessage := "Changing record" - if p.DryRun { - logMessage = "Would change record" - } - log.WithFields(log.Fields{ - "id": change.ResourceRecordSet.ID, - "record": change.ResourceRecordSet.Name, - "type": change.ResourceRecordSet.RecordType, - "value": change.ResourceRecordSet.Value, - "ttl": change.ResourceRecordSet.TTL, - "action": change.Action, - "zone": change.ZoneName, - "zone_id": change.ZoneID, - }).Info(logMessage) - if p.DryRun { - continue - } - - switch change.Action { - case hetznerCreate: - record := hclouddns.HCloudRecord{ - RecordType: change.ResourceRecordSet.RecordType, - ZoneID: change.ZoneID, - Name: change.ResourceRecordSet.Name, - Value: change.ResourceRecordSet.Value, - TTL: change.ResourceRecordSet.TTL, - } - answer, err := p.Client.CreateRecord(record) - if err != nil { - log.WithFields(log.Fields{ - "Code": answer.Error.Code, - "Message": answer.Error.Message, - "Record name": answer.Record.Name, - "Record type": answer.Record.RecordType, - "Record value": answer.Record.Value, - }).Warning("Create problem") - return err - } - case hetznerDelete: - answer, err := p.Client.DeleteRecord(change.ResourceRecordSet.ID) - if err != nil { - log.WithFields(log.Fields{ - "Code": answer.Error.Code, - "Message": answer.Error.Message, - }).Warning("Delete problem") - return err - } - case hetznerUpdate: - record := hclouddns.HCloudRecord{ - RecordType: change.ResourceRecordSet.RecordType, - ZoneID: change.ZoneID, - Name: change.ResourceRecordSet.Name, - Value: change.ResourceRecordSet.Value, - TTL: change.ResourceRecordSet.TTL, - ID: change.ResourceRecordSet.ID, - } - answer, err := p.Client.UpdateRecord(record) - if err != nil { - log.WithFields(log.Fields{ - "Code": answer.Error.Code, - "Message": answer.Error.Message, - "Record name": answer.Record.Name, - "Record type": answer.Record.RecordType, - "Record value": answer.Record.Value, - }).Warning("Update problem") - return err - } - } - } - } - - return nil -} - -func (p *HetznerProvider) newHetznerChanges(action string, endpoints []*endpoint.Endpoint) []*HetznerChanges { - changes := make([]*HetznerChanges, 0, len(endpoints)) - ttl := hetznerTTL - for _, e := range endpoints { - if e.RecordTTL.IsConfigured() { - ttl = int(e.RecordTTL) - } - change := &HetznerChanges{ - Action: action, - ResourceRecordSet: hclouddns.HCloudRecord{ - RecordType: hclouddns.RecordType(e.RecordType), - Name: e.DNSName, - Value: e.Targets[0], - TTL: ttl, - }, - } - changes = append(changes, change) - } - return changes -} - -func (p *HetznerProvider) seperateChangesByZone(zones []hclouddns.HCloudZone, changes []*HetznerChanges) map[string][]*HetznerChanges { - change := make(map[string][]*HetznerChanges) - zoneNameID := provider.ZoneIDName{} - - for _, z := range zones { - zoneNameID.Add(z.ID, z.Name) - change[z.ID] = []*HetznerChanges{} - } - - for _, c := range changes { - zoneID, zoneName := zoneNameID.FindZone(c.ResourceRecordSet.Name) - if zoneName == "" { - log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name) - continue - } - c.ZoneName = zoneName - c.ZoneID = zoneID - change[zoneID] = append(change[zoneID], c) - } - return change -} diff --git a/provider/hetzner/hetzner_test.go b/provider/hetzner/hetzner_test.go deleted file mode 100644 index 9ef5c92e8..000000000 --- a/provider/hetzner/hetzner_test.go +++ /dev/null @@ -1,305 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package hetzner - -import ( - "context" - "fmt" - "github.com/maxatome/go-testdeep/td" - "os" - "reflect" - "testing" - - hclouddns "git.blindage.org/21h/hcloud-dns" - "github.com/stretchr/testify/assert" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" -) - -type mockHCloudClientAdapter interface { - GetZone(ID string) (hclouddns.HCloudAnswerGetZone, error) - GetZones(params hclouddns.HCloudGetZonesParams) (hclouddns.HCloudAnswerGetZones, error) - UpdateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) - DeleteZone(ID string) (hclouddns.HCloudAnswerDeleteZone, error) - CreateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) - ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error) - ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error) - ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error) - GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error) - GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) - UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) - DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error) - CreateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) - CreateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerCreateRecords, error) - UpdateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerUpdateRecords, error) -} - -type MockAction struct { - Name string - RecordData hclouddns.HCloudRecord -} - -type mockHCloudClient struct { - Token string `yaml:"token"` - Actions []MockAction -} - -// New instance -func mockHCloudNew(t string) *mockHCloudClient { - return &mockHCloudClient{ - Token: t, - } -} - -// Mock all methods - -func (m *mockHCloudClient) GetZone(ID string) (hclouddns.HCloudAnswerGetZone, error) { - return hclouddns.HCloudAnswerGetZone{}, nil -} - -func (m *mockHCloudClient) GetZones(params hclouddns.HCloudGetZonesParams) (hclouddns.HCloudAnswerGetZones, error) { - return hclouddns.HCloudAnswerGetZones{ - Zones: []hclouddns.HCloudZone{ - { - ID: "HetznerZoneID", - Name: "blindage.org", - TTL: 666, - RecordsCount: 1, - }, - }, - }, nil -} - -// zones -func (m *mockHCloudClient) UpdateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) { - return hclouddns.HCloudAnswerGetZone{}, nil -} -func (m *mockHCloudClient) DeleteZone(ID string) (hclouddns.HCloudAnswerDeleteZone, error) { - return hclouddns.HCloudAnswerDeleteZone{}, nil -} -func (m *mockHCloudClient) CreateZone(zone hclouddns.HCloudZone) (hclouddns.HCloudAnswerGetZone, error) { - return hclouddns.HCloudAnswerGetZone{}, nil -} -func (m *mockHCloudClient) ImportZoneString(zoneID string, zonePlainText string) (hclouddns.HCloudAnswerGetZone, error) { - return hclouddns.HCloudAnswerGetZone{}, nil -} -func (m *mockHCloudClient) ExportZoneToString(zoneID string) (hclouddns.HCloudAnswerGetZonePlainText, error) { - return hclouddns.HCloudAnswerGetZonePlainText{}, nil -} -func (m *mockHCloudClient) ValidateZoneString(zonePlainText string) (hclouddns.HCloudAnswerZoneValidate, error) { - return hclouddns.HCloudAnswerZoneValidate{}, nil -} - -// records - -func (m *mockHCloudClient) GetRecord(ID string) (hclouddns.HCloudAnswerGetRecord, error) { - return hclouddns.HCloudAnswerGetRecord{}, nil -} - -func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (hclouddns.HCloudAnswerGetRecords, error) { - return hclouddns.HCloudAnswerGetRecords{ - Records: []hclouddns.HCloudRecord{ - { - RecordType: hclouddns.RecordType("A"), - ID: "ATypeRecordID", - ZoneID: "HetznerZoneID", - Name: "@", - Value: "127.0.0.1", - TTL: 666, - }, - }, - }, nil -} -func (m *mockHCloudClient) UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) { - m.Actions = append(m.Actions, MockAction{ - Name: "UpdateRecord", - RecordData: record, - }) - return hclouddns.HCloudAnswerGetRecord{}, nil -} -func (m *mockHCloudClient) DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error) { - m.Actions = append(m.Actions, MockAction{ - Name: "DeleteRecord", - RecordData: hclouddns.HCloudRecord{ - ID: ID, - }, - }) - return hclouddns.HCloudAnswerDeleteRecord{}, nil -} -func (m *mockHCloudClient) CreateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) { - m.Actions = append(m.Actions, MockAction{ - Name: "CreateRecord", - RecordData: record, - }) - return hclouddns.HCloudAnswerGetRecord{}, nil -} -func (m *mockHCloudClient) CreateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerCreateRecords, error) { - return hclouddns.HCloudAnswerCreateRecords{}, nil -} -func (m *mockHCloudClient) UpdateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerUpdateRecords, error) { - return hclouddns.HCloudAnswerUpdateRecords{}, nil -} - -func TestNewHetznerProvider(t *testing.T) { - _ = os.Setenv("HETZNER_TOKEN", "myHetznerToken") - _, err := NewHetznerProvider(context.Background(), endpoint.NewDomainFilter([]string{"blindage.org"}), true) - if err != nil { - t.Errorf("failed : %s", err) - } - - _ = os.Unsetenv("HETZNER_TOKEN") - _, err = NewHetznerProvider(context.Background(), endpoint.NewDomainFilter([]string{"blindage.org"}), true) - if err == nil { - t.Errorf("expected to fail") - } -} - -func TestHetznerProvider_TestData(t *testing.T) { - - mockedClient := mockHCloudNew("myHetznerToken") - - // Check test zone data is ok - expectedZonesAnswer := hclouddns.HCloudAnswerGetZones{ - Zones: []hclouddns.HCloudZone{ - { - ID: "HetznerZoneID", - Name: "blindage.org", - TTL: 666, - RecordsCount: 1, - }, - }, - } - - testingZonesAnswer, err := mockedClient.GetZones(hclouddns.HCloudGetZonesParams{}) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - if !reflect.DeepEqual(expectedZonesAnswer, testingZonesAnswer) { - t.Errorf("should be equal, %s", err) - } - - // Check test record data is ok - expectedRecordsAnswer := hclouddns.HCloudAnswerGetRecords{ - Records: []hclouddns.HCloudRecord{ - { - RecordType: hclouddns.RecordType("A"), - ID: "ATypeRecordID", - ZoneID: "HetznerZoneID", - Name: "@", - Value: "127.0.0.1", - TTL: 666, - }, - }, - } - - testingRecordsAnswer, err := mockedClient.GetRecords(hclouddns.HCloudGetRecordsParams{}) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - if !reflect.DeepEqual(expectedRecordsAnswer, testingRecordsAnswer) { - t.Errorf("should be equal, %s", err) - } - -} - -func TestHetznerProvider_Records(t *testing.T) { - - mockedClient := mockHCloudNew("myHetznerToken") - - mockedProvider := &HetznerProvider{ - Client: mockedClient, - } - - // Now check Records function of provider, if ZoneID equal "blindage.org" must be returned - endpoints, err := mockedProvider.Records(context.Background()) - if err != nil { - t.Errorf("should not fail, %s", err) - } - fmt.Printf("%+v\n", endpoints[0].DNSName) - assert.Equal(t, "blindage.org", endpoints[0].DNSName) -} - -func TestHetznerProvider_ApplyChanges(t *testing.T) { - changes := &plan.Changes{} - mockedClient := mockHCloudNew("myHetznerToken") - mockedProvider := &HetznerProvider{ - Client: mockedClient, - } - - changes.Create = []*endpoint.Endpoint{ - {DNSName: "blindage.org", Targets: endpoint.Targets{"target"}}, - {DNSName: "test.blindage.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666}, - } - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.blindage.org", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}} - changes.Delete = []*endpoint.Endpoint{{DNSName: "test.blindage.org", Targets: endpoint.Targets{"target"}, RecordType: "A"}} - - err := mockedProvider.ApplyChanges(context.Background(), changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - if len(mockedClient.Actions) != 4 { - t.Errorf("should be 4 changes not %d", len(mockedClient.Actions)) - } -} - -func TestHetznerProvider_ApplyChangesCreateUpdateCname(t *testing.T) { - changes := &plan.Changes{} - mockedClient := mockHCloudNew("myHetznerToken") - mockedProvider := &HetznerProvider{ - Client: mockedClient, - } - - changes.Create = []*endpoint.Endpoint{ - {DNSName: "test-cname.blindage.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "CNAME"}, - } - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-cname2.blindage.org", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 777}} - - err := mockedProvider.ApplyChanges(context.Background(), changes) - if err != nil { - t.Errorf("should not fail, %s", err) - } - - td.Cmp(t, mockedClient.Actions, []MockAction{ - { - Name: "CreateRecord", - RecordData: hclouddns.HCloudRecord{ - RecordType: "CNAME", - ID: "", - Created: "", - Modified: "", - ZoneID: "HetznerZoneID", - Name: "test-cname", - Value: "target.", - TTL: 666, - }, - }, - { - Name: "UpdateRecord", - RecordData: hclouddns.HCloudRecord{ - RecordType: "CNAME", - ID: "", - Created: "", - Modified: "", - ZoneID: "HetznerZoneID", - Name: "test-cname2", - Value: "target-new.", - TTL: 777, - }, - }, - }) -}