diff --git a/.github/workflows/ci-e2e.yaml b/.github/workflows/ci-e2e.yaml index e3304821..3dd4750d 100644 --- a/.github/workflows/ci-e2e.yaml +++ b/.github/workflows/ci-e2e.yaml @@ -45,10 +45,14 @@ jobs: - name: Create GCP provider configuration run: | make local-setup-gcp-mz-clean local-setup-gcp-mz-generate GCP_ZONE_NAME=e2e-google-hcpapps-net GCP_ZONE_DNS_NAME=e2e.google.hcpapps.net GCP_GOOGLE_CREDENTIALS='${{ secrets.E2E_GCP_GOOGLE_CREDENTIALS }}' GCP_PROJECT_ID=${{ secrets.E2E_GCP_PROJECT_ID }} + - name: Create Azure provider configuration + run: | + make local-setup-azure-mz-clean local-setup-azure-mz-generate KUADRANT_AZURE_DNS_ZONE_ID='${{ secrets.E2E_AZURE_ZONE_ID }}' KUADRANT_AZURE_ZONE_ROOT_DOMAIN=e2e.azure.hcpapps.net KUADRANT_AZURE_CREDENTIALS='${{ secrets.E2E_AZURE_CREDENTIALS }}' - name: Setup environment run: | make local-setup DEPLOY=true TEST_NAMESPACE=${{ env.TEST_NAMESPACE }} kubectl -n ${{ env.TEST_NAMESPACE }} wait --timeout=60s --for=condition=Ready managedzone/dev-mz-aws + kubectl -n ${{ env.TEST_NAMESPACE }} wait --timeout=60s --for=condition=Ready managedzone/dev-mz-azure kubectl -n ${{ env.TEST_NAMESPACE }} wait --timeout=60s --for=condition=Ready managedzone/dev-mz-gcp - name: Run suite AWS run: | @@ -64,6 +68,13 @@ jobs: export TEST_DNS_NAMESPACE=${{ env.TEST_NAMESPACE }} export TEST_DNS_PROVIDER=gcp make test-e2e + - name: Run suite Azure + run: | + export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-azure + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.azure.hcpapps.net + export TEST_DNS_NAMESPACE=${{ env.TEST_NAMESPACE }} + export TEST_DNS_PROVIDER=azure + make test-e2e - name: Dump Controller logs if: ${{ failure() }} run: | diff --git a/Makefile b/Makefile index fcd6e764..4ead905a 100644 --- a/Makefile +++ b/Makefile @@ -211,7 +211,7 @@ build: manifests generate fmt vet ## Build manager binary. .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go --zap-devel --provider inmemory,aws,google + go run ./cmd/main.go --zap-devel --provider inmemory,aws,google,azure # If you wish built the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. diff --git a/README.md b/README.md index b35e6b5b..6b24ab3e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,19 @@ make local-setup-gcp-mz-clean local-setup-gcp-mz-generate GCP_ZONE_NAME= KUADRANT_AZURE_ZONE_ROOT_DOMAIN='' +``` + +Info on generating service principal credentials [here](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/azure.md) + +Getting the zone ID can be achieved using the below command: +```bash +az network dns zone show --name --resource-group --query "{id:id,domain:name}" +``` + ### Running controller locally (default) 1. Create local environment(creates kind cluster) diff --git a/cmd/main.go b/cmd/main.go index 5c38971d..de97e9ef 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -38,6 +38,7 @@ import ( "github.com/kuadrant/dns-operator/internal/controller" "github.com/kuadrant/dns-operator/internal/provider" _ "github.com/kuadrant/dns-operator/internal/provider/aws" + _ "github.com/kuadrant/dns-operator/internal/provider/azure" _ "github.com/kuadrant/dns-operator/internal/provider/google" _ "github.com/kuadrant/dns-operator/internal/provider/inmemory" //+kubebuilder:scaffold:imports diff --git a/config/deploy/local/kustomization.yaml b/config/deploy/local/kustomization.yaml index a8ddb415..a3efa11f 100644 --- a/config/deploy/local/kustomization.yaml +++ b/config/deploy/local/kustomization.yaml @@ -10,7 +10,7 @@ patches: - patch: |- - op: add path: /spec/template/spec/containers/0/args/- - value: --provider=aws,google,inmemory + value: --provider=aws,google,inmemory,azure - op: add path: /spec/template/spec/containers/0/args/- value: --zap-log-level=debug diff --git a/config/local-setup/managedzone/azure/azure-credentials.env.template b/config/local-setup/managedzone/azure/azure-credentials.env.template new file mode 100644 index 00000000..6f8e3b95 --- /dev/null +++ b/config/local-setup/managedzone/azure/azure-credentials.env.template @@ -0,0 +1 @@ +azure.json=${KUADRANT_AZURE_CREDENTIALS} diff --git a/config/local-setup/managedzone/azure/kustomization.yaml b/config/local-setup/managedzone/azure/kustomization.yaml new file mode 100644 index 00000000..f13246df --- /dev/null +++ b/config/local-setup/managedzone/azure/kustomization.yaml @@ -0,0 +1,40 @@ +resources: + - managed_zone.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: azure-managed-zone-config + envs: + - managed-zone-config.env + +secretGenerator: + - name: azure-credentials + envs: + - azure-credentials.env + type: "kuadrant.io/azure" + +replacements: + - source: + kind: ConfigMap + name: azure-managed-zone-config + version: v1 + fieldPath: data.AZURE_DNS_ZONE_ID + targets: + - select: + kind: ManagedZone + name: dev-mz-azure + fieldPaths: + - spec.id + - source: + kind: ConfigMap + name: azure-managed-zone-config + version: v1 + fieldPath: data.AZURE_ZONE_ROOT_DOMAIN + targets: + - select: + kind: ManagedZone + name: dev-mz-azure + fieldPaths: + - spec.domainName diff --git a/config/local-setup/managedzone/azure/managed-zone-config.env.template b/config/local-setup/managedzone/azure/managed-zone-config.env.template new file mode 100644 index 00000000..8c1be956 --- /dev/null +++ b/config/local-setup/managedzone/azure/managed-zone-config.env.template @@ -0,0 +1,2 @@ +AZURE_DNS_ZONE_ID=${KUADRANT_AZURE_DNS_ZONE_ID} +AZURE_ZONE_ROOT_DOMAIN=${KUADRANT_AZURE_ZONE_ROOT_DOMAIN} diff --git a/config/local-setup/managedzone/azure/managed_zone.yaml b/config/local-setup/managedzone/azure/managed_zone.yaml new file mode 100644 index 00000000..a8ececc1 --- /dev/null +++ b/config/local-setup/managedzone/azure/managed_zone.yaml @@ -0,0 +1,10 @@ +apiVersion: kuadrant.io/v1alpha1 +kind: ManagedZone +metadata: + name: dev-mz-azure +spec: + id: DUMMY_ID + domainName: DUMMY_DOMAIN_NAME + description: "Dev Managed Zone" + dnsProviderSecretRef: + name: azure-credentials diff --git a/examples/kuadrant/loadbalanced-healthchecks-dns.yaml b/examples/kuadrant/loadbalanced-healthchecks-dns.yaml new file mode 100644 index 00000000..e6f11eed --- /dev/null +++ b/examples/kuadrant/loadbalanced-healthchecks-dns.yaml @@ -0,0 +1,51 @@ +apiVersion: kuadrant.io/v1alpha1 +kind: DNSRecord +metadata: + name: loadbalanced-healthchecks-dns +spec: + endpoints: + - dnsName: 14byhk-2k52h1.klb.${KUADRANT_SUB_DOMAIN} + recordTTL: 60 + recordType: A + targets: + - 172.32.200.1 + - dnsName: ${KUADRANT_SUB_DOMAIN} + recordTTL: 300 + recordType: CNAME + targets: + - klb.${KUADRANT_SUB_DOMAIN} + - dnsName: eu.klb.${KUADRANT_SUB_DOMAIN} + providerSpecific: + - name: weight + value: "120" + recordTTL: 60 + recordType: CNAME + setIdentifier: 14byhk-2k52h1.klb.${KUADRANT_SUB_DOMAIN} + targets: + - 14byhk-2k52h1.klb.${KUADRANT_SUB_DOMAIN} + - dnsName: klb.${KUADRANT_SUB_DOMAIN} + providerSpecific: + - name: geo-code + value: EU + recordTTL: 300 + recordType: CNAME + setIdentifier: EU + targets: + - eu.klb.${KUADRANT_SUB_DOMAIN} + - dnsName: klb.${KUADRANT_SUB_DOMAIN} + providerSpecific: + - name: geo-code + value: '*' + recordTTL: 300 + recordType: CNAME + setIdentifier: default + targets: + - eu.klb.${KUADRANT_SUB_DOMAIN} + healthCheck: + endpoint: / + failureThreshold: 3 + port: 80 + protocol: HTTPS + managedZone: + name: dev-mz-azure + rootHost: ${KUADRANT_SUB_DOMAIN} \ No newline at end of file diff --git a/examples/kuadrant/simple-dns.yaml b/examples/kuadrant/simple-dns.yaml new file mode 100644 index 00000000..d4bdb864 --- /dev/null +++ b/examples/kuadrant/simple-dns.yaml @@ -0,0 +1,14 @@ +apiVersion: kuadrant.io/v1alpha1 +kind: DNSRecord +metadata: + name: simple-dns +spec: + endpoints: + - dnsName: ${KUADRANT_SUB_DOMAIN} + recordTTL: 60 + recordType: A + targets: + - 172.32.200.17 + managedZone: + name: dev-mz-azure + rootHost: ${KUADRANT_SUB_DOMAIN} \ No newline at end of file diff --git a/examples/kuadrant/simple-healthchecks-dns.yaml b/examples/kuadrant/simple-healthchecks-dns.yaml new file mode 100644 index 00000000..a73d4839 --- /dev/null +++ b/examples/kuadrant/simple-healthchecks-dns.yaml @@ -0,0 +1,19 @@ +apiVersion: kuadrant.io/v1alpha1 +kind: DNSRecord +metadata: + name: simple-dns +spec: + healthCheck: + endpoint: "/" + port: 80 + protocol: "HTTPS" + failureThreshold: 3 + endpoints: + - dnsName: ${KUADRANT_SUB_DOMAIN} + recordTTL: 60 + recordType: A + targets: + - 172.32.200.17 + managedZone: + name: dev-mz-azure + rootHost: ${KUADRANT_SUB_DOMAIN} \ No newline at end of file diff --git a/go.mod b/go.mod index 59886c21..846323f2 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,10 @@ go 1.21 require ( cloud.google.com/go/compute/metadata v0.2.3 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 github.com/aws/aws-sdk-go v1.44.311 github.com/go-logr/logr v1.3.0 github.com/google/go-cmp v0.6.0 @@ -20,6 +24,7 @@ require ( golang.org/x/net v0.23.0 golang.org/x/oauth2 v0.13.0 google.golang.org/api v0.134.0 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.28.3 k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 @@ -30,6 +35,8 @@ require ( require ( cloud.google.com/go/compute v1.20.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -42,6 +49,7 @@ require ( github.com/go-openapi/swag v0.22.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -54,11 +62,13 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -82,7 +92,6 @@ require ( google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.28.3 // indirect k8s.io/component-base v0.28.3 // indirect diff --git a/go.sum b/go.sum index bda6cd6d..5638ab6c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,22 @@ cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZN cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 h1:rR8ZW79lE/ppfXTfiYSnMFv5EzmVuY4pfZWIkscIJ64= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go v1.44.311 h1:60i8hyVMOXqabKJQPCq4qKRBQ6hRafI/WOcDxGM+J7Q= @@ -26,6 +42,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -59,6 +77,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -128,6 +148,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -147,6 +169,8 @@ github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -262,6 +286,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index ed3ad42b..3561f95a 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -43,6 +43,7 @@ import ( "github.com/kuadrant/dns-operator/api/v1alpha1" "github.com/kuadrant/dns-operator/internal/provider" _ "github.com/kuadrant/dns-operator/internal/provider/aws" + _ "github.com/kuadrant/dns-operator/internal/provider/azure" _ "github.com/kuadrant/dns-operator/internal/provider/google" _ "github.com/kuadrant/dns-operator/internal/provider/inmemory" //+kubebuilder:scaffold:imports diff --git a/internal/external-dns/azure/azure_private_dns.go b/internal/external-dns/azure/azure_private_dns.go deleted file mode 100644 index 50df066f..00000000 --- a/internal/external-dns/azure/azure_private_dns.go +++ /dev/null @@ -1,445 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -//nolint:staticcheck // Required due to the current dependency on a deprecated version of azure-sdk-for-go -package azure - -import ( - "context" - "fmt" - "strings" - - azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - privatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" - log "github.com/sirupsen/logrus" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -// PrivateZonesClient is an interface of privatedns.PrivateZoneClient that can be stubbed for testing. -type PrivateZonesClient interface { - NewListByResourceGroupPager(resourceGroupName string, options *privatedns.PrivateZonesClientListByResourceGroupOptions) *azcoreruntime.Pager[privatedns.PrivateZonesClientListByResourceGroupResponse] -} - -// PrivateRecordSetsClient is an interface of privatedns.RecordSetsClient that can be stubbed for testing. -type PrivateRecordSetsClient interface { - NewListPager(resourceGroupName string, privateZoneName string, options *privatedns.RecordSetsClientListOptions) *azcoreruntime.Pager[privatedns.RecordSetsClientListResponse] - Delete(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, relativeRecordSetName string, options *privatedns.RecordSetsClientDeleteOptions) (privatedns.RecordSetsClientDeleteResponse, error) - CreateOrUpdate(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, relativeRecordSetName string, parameters privatedns.RecordSet, options *privatedns.RecordSetsClientCreateOrUpdateOptions) (privatedns.RecordSetsClientCreateOrUpdateResponse, error) -} - -// AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service -type AzurePrivateDNSProvider struct { - provider.BaseProvider - domainFilter endpoint.DomainFilter - zoneIDFilter provider.ZoneIDFilter - dryRun bool - resourceGroup string - userAssignedIdentityClientID string - zonesClient PrivateZonesClient - recordSetsClient PrivateRecordSetsClient -} - -// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. -// -// Returns the provider or an error if a provider could not be created. -func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) { - cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID) - if err != nil { - return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) - } - cred, clientOpts, err := getCredentials(*cfg) - if err != nil { - return nil, fmt.Errorf("failed to get credentials: %w", err) - } - - zonesClient, err := privatedns.NewPrivateZonesClient(cfg.SubscriptionID, cred, clientOpts) - if err != nil { - return nil, err - } - recordSetsClient, err := privatedns.NewRecordSetsClient(cfg.SubscriptionID, cred, clientOpts) - if err != nil { - return nil, err - } - return &AzurePrivateDNSProvider{ - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - dryRun: dryRun, - resourceGroup: cfg.ResourceGroup, - userAssignedIdentityClientID: cfg.UserAssignedIdentityID, - zonesClient: zonesClient, - recordSetsClient: recordSetsClient, - }, nil -} - -// Records gets the current records. -// -// Returns the current records or an error if the operation failed. -func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { - zones, err := p.zones(ctx) - if err != nil { - return nil, err - } - - log.Debugf("Retrieving Azure Private DNS Records for resource group '%s'", p.resourceGroup) - - for _, zone := range zones { - pager := p.recordSetsClient.NewListPager(p.resourceGroup, *zone.Name, &privatedns.RecordSetsClientListOptions{Top: nil}) - for pager.More() { - nextResult, err := pager.NextPage(ctx) - if err != nil { - return nil, err - } - - for _, recordSet := range nextResult.Value { - var recordType string - if recordSet.Type == nil { - log.Debugf("Skipping invalid record set with missing type.") - continue - } - recordType = strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/privateDnsZones/") - - var name string - if recordSet.Name == nil { - log.Debugf("Skipping invalid record set with missing name.") - continue - } - name = formatAzureDNSName(*recordSet.Name, *zone.Name) - - targets := extractAzurePrivateDNSTargets(recordSet) - if len(targets) == 0 { - log.Debugf("Failed to extract targets for '%s' with type '%s'.", name, recordType) - continue - } - - var ttl endpoint.TTL - if recordSet.Properties.TTL != nil { - ttl = endpoint.TTL(*recordSet.Properties.TTL) - } - - ep := endpoint.NewEndpointWithTTL(name, recordType, ttl, targets...) - log.Debugf( - "Found %s record for '%s' with target '%s'.", - ep.RecordType, - ep.DNSName, - ep.Targets, - ) - endpoints = append(endpoints, ep) - } - } - } - - log.Debugf("Returning %d Azure Private DNS Records for resource group '%s'", len(endpoints), p.resourceGroup) - - return endpoints, nil -} - -// ApplyChanges applies the given changes. -// -// Returns nil if the operation was successful or an error if the operation failed. -func (p *AzurePrivateDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - log.Debugf("Received %d changes to process", len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew)+len(changes.UpdateOld)) - - zones, err := p.zones(ctx) - if err != nil { - return err - } - - deleted, updated := p.mapChanges(zones, changes) - p.deleteRecords(ctx, deleted) - p.updateRecords(ctx, updated) - return nil -} - -func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.PrivateZone, error) { - log.Debugf("Retrieving Azure Private DNS zones for Resource Group '%s'", p.resourceGroup) - - var zones []privatedns.PrivateZone - - pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &privatedns.PrivateZonesClientListByResourceGroupOptions{Top: nil}) - for pager.More() { - nextResult, err := pager.NextPage(ctx) - if err != nil { - return nil, err - } - for _, zone := range nextResult.Value { - log.Debugf("Validating Zone: %v", *zone.Name) - - if zone.Name != nil && p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.ID) { - zones = append(zones, *zone) - } - } - } - - log.Debugf("Found %d Azure Private DNS zone(s).", len(zones)) - return zones, nil -} - -type azurePrivateDNSChangeMap map[string][]*endpoint.Endpoint - -func (p *AzurePrivateDNSProvider) mapChanges(zones []privatedns.PrivateZone, changes *plan.Changes) (azurePrivateDNSChangeMap, azurePrivateDNSChangeMap) { - ignored := map[string]bool{} - deleted := azurePrivateDNSChangeMap{} - updated := azurePrivateDNSChangeMap{} - zoneNameIDMapper := provider.ZoneIDName{} - for _, z := range zones { - if z.Name != nil { - zoneNameIDMapper.Add(*z.Name, *z.Name) - } - } - mapChange := func(changeMap azurePrivateDNSChangeMap, change *endpoint.Endpoint) { - zone, _ := zoneNameIDMapper.FindZone(change.DNSName) - if zone == "" { - if _, ok := ignored[change.DNSName]; !ok { - ignored[change.DNSName] = true - log.Infof("Ignoring changes to '%s' because a suitable Azure Private DNS zone was not found.", change.DNSName) - } - return - } - // Ensure the record type is suitable - changeMap[zone] = append(changeMap[zone], change) - } - - for _, change := range changes.Delete { - mapChange(deleted, change) - } - - for _, change := range changes.Create { - mapChange(updated, change) - } - - for _, change := range changes.UpdateNew { - mapChange(updated, change) - } - return deleted, updated -} - -func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azurePrivateDNSChangeMap) { - log.Debugf("Records to be deleted: %d", len(deleted)) - // Delete records first - for zone, endpoints := range deleted { - for _, ep := range endpoints { - name := p.recordSetNameForZone(zone, ep) - if p.dryRun { - log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone) - } else { - log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone) - if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, nil); err != nil { - log.Errorf( - "Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v", - ep.RecordType, - name, - zone, - err, - ) - } - } - } - } -} - -func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) { - log.Debugf("Records to be updated: %d", len(updated)) - for zone, endpoints := range updated { - for _, ep := range endpoints { - name := p.recordSetNameForZone(zone, ep) - if p.dryRun { - log.Infof( - "Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", - ep.RecordType, - name, - ep.Targets, - zone, - ) - continue - } - - log.Infof( - "Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", - ep.RecordType, - name, - ep.Targets, - zone, - ) - - recordSet, err := p.newRecordSet(ep) - if err == nil { - _, err = p.recordSetsClient.CreateOrUpdate( - ctx, - p.resourceGroup, - zone, - privatedns.RecordType(ep.RecordType), - name, - recordSet, - nil, - ) - } - if err != nil { - log.Errorf( - "Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v", - ep.RecordType, - name, - ep.Targets, - zone, - err, - ) - } - } - } -} - -func (p *AzurePrivateDNSProvider) recordSetNameForZone(zone string, endpoint *endpoint.Endpoint) string { - // Remove the zone from the record set - name := endpoint.DNSName - name = name[:len(name)-len(zone)] - name = strings.TrimSuffix(name, ".") - - // For root, use @ - if name == "" { - return "@" - } - return name -} - -func (p *AzurePrivateDNSProvider) newRecordSet(endpoint *endpoint.Endpoint) (privatedns.RecordSet, error) { - var ttl int64 = azureRecordTTL - if endpoint.RecordTTL.IsConfigured() { - ttl = int64(endpoint.RecordTTL) - } - switch privatedns.RecordType(endpoint.RecordType) { - case privatedns.RecordTypeA: - aRecords := make([]*privatedns.ARecord, len(endpoint.Targets)) - for i, target := range endpoint.Targets { - aRecords[i] = &privatedns.ARecord{ - IPv4Address: to.Ptr(target), - } - } - return privatedns.RecordSet{ - Properties: &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - ARecords: aRecords, - }, - }, nil - case privatedns.RecordTypeAAAA: - aaaaRecords := make([]*privatedns.AaaaRecord, len(endpoint.Targets)) - for i, target := range endpoint.Targets { - aaaaRecords[i] = &privatedns.AaaaRecord{ - IPv6Address: to.Ptr(target), - } - } - return privatedns.RecordSet{ - Properties: &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - AaaaRecords: aaaaRecords, - }, - }, nil - case privatedns.RecordTypeCNAME: - return privatedns.RecordSet{ - Properties: &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - CnameRecord: &privatedns.CnameRecord{ - Cname: to.Ptr(endpoint.Targets[0]), - }, - }, - }, nil - case privatedns.RecordTypeMX: - mxRecords := make([]*privatedns.MxRecord, len(endpoint.Targets)) - for i, target := range endpoint.Targets { - mxRecord, err := parseMxTarget[privatedns.MxRecord](target) - if err != nil { - return privatedns.RecordSet{}, err - } - mxRecords[i] = &mxRecord - } - return privatedns.RecordSet{ - Properties: &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - MxRecords: mxRecords, - }, - }, nil - case privatedns.RecordTypeTXT: - return privatedns.RecordSet{ - Properties: &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - TxtRecords: []*privatedns.TxtRecord{ - { - Value: []*string{ - &endpoint.Targets[0], - }, - }, - }, - }, - }, nil - } - return privatedns.RecordSet{}, fmt.Errorf("unsupported record type '%s'", endpoint.RecordType) -} - -// Helper function (shared with test code) -func extractAzurePrivateDNSTargets(recordSet *privatedns.RecordSet) []string { - properties := recordSet.Properties - if properties == nil { - return []string{} - } - - // Check for A records - aRecords := properties.ARecords - if len(aRecords) > 0 && (aRecords)[0].IPv4Address != nil { - targets := make([]string, len(aRecords)) - for i, aRecord := range aRecords { - targets[i] = *aRecord.IPv4Address - } - return targets - } - - // Check for AAAA records - aaaaRecords := properties.AaaaRecords - if len(aaaaRecords) > 0 && (aaaaRecords)[0].IPv6Address != nil { - targets := make([]string, len(aaaaRecords)) - for i, aaaaRecord := range aaaaRecords { - targets[i] = *aaaaRecord.IPv6Address - } - return targets - } - - // Check for CNAME records - cnameRecord := properties.CnameRecord - if cnameRecord != nil && cnameRecord.Cname != nil { - return []string{*cnameRecord.Cname} - } - - // Check for MX records - mxRecords := properties.MxRecords - if len(mxRecords) > 0 && (mxRecords)[0].Exchange != nil { - targets := make([]string, len(mxRecords)) - for i, mxRecord := range mxRecords { - targets[i] = fmt.Sprintf("%d %s", *mxRecord.Preference, *mxRecord.Exchange) - } - return targets - } - - // Check for TXT records - txtRecords := properties.TxtRecords - if len(txtRecords) > 0 && (txtRecords)[0].Value != nil { - values := (txtRecords)[0].Value - if len(values) > 0 { - return []string{*(values)[0]} - } - } - return []string{} -} diff --git a/internal/external-dns/azure/azure_privatedns_test.go b/internal/external-dns/azure/azure_privatedns_test.go deleted file mode 100644 index 567badea..00000000 --- a/internal/external-dns/azure/azure_privatedns_test.go +++ /dev/null @@ -1,432 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package azure - -import ( - "context" - "testing" - - azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - privatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -const ( - recordTTL = 300 -) - -// mockPrivateZonesClient implements the methods of the Azure Private DNS Zones Client which are used in the Azure Private DNS Provider -// and returns static results which are defined per test -type mockPrivateZonesClient struct { - pagingHandler azcoreruntime.PagingHandler[privatedns.PrivateZonesClientListByResourceGroupResponse] -} - -func newMockPrivateZonesClient(zones []*privatedns.PrivateZone) mockPrivateZonesClient { - pagingHandler := azcoreruntime.PagingHandler[privatedns.PrivateZonesClientListByResourceGroupResponse]{ - More: func(resp privatedns.PrivateZonesClientListByResourceGroupResponse) bool { - return false - }, - Fetcher: func(context.Context, *privatedns.PrivateZonesClientListByResourceGroupResponse) (privatedns.PrivateZonesClientListByResourceGroupResponse, error) { - return privatedns.PrivateZonesClientListByResourceGroupResponse{ - PrivateZoneListResult: privatedns.PrivateZoneListResult{ - Value: zones, - }, - }, nil - }, - } - return mockPrivateZonesClient{ - pagingHandler: pagingHandler, - } -} - -func (client *mockPrivateZonesClient) NewListByResourceGroupPager(resourceGroupName string, options *privatedns.PrivateZonesClientListByResourceGroupOptions) *azcoreruntime.Pager[privatedns.PrivateZonesClientListByResourceGroupResponse] { - return azcoreruntime.NewPager(client.pagingHandler) -} - -// mockPrivateRecordSetsClient implements the methods of the Azure Private DNS RecordSet Client which are used in the Azure Private DNS Provider -// and returns static results which are defined per test -type mockPrivateRecordSetsClient struct { - pagingHandler azcoreruntime.PagingHandler[privatedns.RecordSetsClientListResponse] - deletedEndpoints []*endpoint.Endpoint - updatedEndpoints []*endpoint.Endpoint -} - -func newMockPrivateRecordSectsClient(recordSets []*privatedns.RecordSet) mockPrivateRecordSetsClient { - pagingHandler := azcoreruntime.PagingHandler[privatedns.RecordSetsClientListResponse]{ - More: func(resp privatedns.RecordSetsClientListResponse) bool { - return false - }, - Fetcher: func(context.Context, *privatedns.RecordSetsClientListResponse) (privatedns.RecordSetsClientListResponse, error) { - return privatedns.RecordSetsClientListResponse{ - RecordSetListResult: privatedns.RecordSetListResult{ - Value: recordSets, - }, - }, nil - }, - } - return mockPrivateRecordSetsClient{ - pagingHandler: pagingHandler, - } -} - -func (client *mockPrivateRecordSetsClient) NewListPager(resourceGroupName string, privateZoneName string, options *privatedns.RecordSetsClientListOptions) *azcoreruntime.Pager[privatedns.RecordSetsClientListResponse] { - return azcoreruntime.NewPager(client.pagingHandler) -} - -func (client *mockPrivateRecordSetsClient) Delete(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, relativeRecordSetName string, options *privatedns.RecordSetsClientDeleteOptions) (privatedns.RecordSetsClientDeleteResponse, error) { - client.deletedEndpoints = append( - client.deletedEndpoints, - endpoint.NewEndpoint( - formatAzureDNSName(relativeRecordSetName, privateZoneName), - string(recordType), - "", - ), - ) - return privatedns.RecordSetsClientDeleteResponse{}, nil -} - -func (client *mockPrivateRecordSetsClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, relativeRecordSetName string, parameters privatedns.RecordSet, options *privatedns.RecordSetsClientCreateOrUpdateOptions) (privatedns.RecordSetsClientCreateOrUpdateResponse, error) { - var ttl endpoint.TTL - if parameters.Properties.TTL != nil { - ttl = endpoint.TTL(*parameters.Properties.TTL) - } - client.updatedEndpoints = append( - client.updatedEndpoints, - endpoint.NewEndpointWithTTL( - formatAzureDNSName(relativeRecordSetName, privateZoneName), - string(recordType), - ttl, - extractAzurePrivateDNSTargets(¶meters)..., - ), - ) - return privatedns.RecordSetsClientCreateOrUpdateResponse{}, nil - //return parameters, nil -} - -func createMockPrivateZone(zone string, id string) *privatedns.PrivateZone { - return &privatedns.PrivateZone{ - ID: to.Ptr(id), - Name: to.Ptr(zone), - } -} - -func privateARecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - aRecords := make([]*privatedns.ARecord, len(values)) - for i, value := range values { - aRecords[i] = &privatedns.ARecord{ - IPv4Address: to.Ptr(value), - } - } - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - ARecords: aRecords, - } -} - -func privateAAAARecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - aaaaRecords := make([]*privatedns.AaaaRecord, len(values)) - for i, value := range values { - aaaaRecords[i] = &privatedns.AaaaRecord{ - IPv6Address: to.Ptr(value), - } - } - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - AaaaRecords: aaaaRecords, - } -} - -func privateCNameRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - CnameRecord: &privatedns.CnameRecord{ - Cname: to.Ptr(values[0]), - }, - } -} - -func privateMXRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - mxRecords := make([]*privatedns.MxRecord, len(values)) - for i, target := range values { - mxRecord, _ := parseMxTarget[privatedns.MxRecord](target) - mxRecords[i] = &mxRecord - } - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - MxRecords: mxRecords, - } -} - -func privateTxtRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - TxtRecords: []*privatedns.TxtRecord{ - { - Value: []*string{&values[0]}, - }, - }, - } -} - -func privateOthersRecordSetPropertiesGetter(values []string, ttl int64) *privatedns.RecordSetProperties { - return &privatedns.RecordSetProperties{ - TTL: to.Ptr(ttl), - } -} - -func createPrivateMockRecordSet(name, recordType string, values ...string) *privatedns.RecordSet { - return createPrivateMockRecordSetMultiWithTTL(name, recordType, 0, values...) -} - -func createPrivateMockRecordSetWithTTL(name, recordType, value string, ttl int64) *privatedns.RecordSet { - return createPrivateMockRecordSetMultiWithTTL(name, recordType, ttl, value) -} - -func createPrivateMockRecordSetMultiWithTTL(name, recordType string, ttl int64, values ...string) *privatedns.RecordSet { - var getterFunc func(values []string, ttl int64) *privatedns.RecordSetProperties - - switch recordType { - case endpoint.RecordTypeA: - getterFunc = privateARecordSetPropertiesGetter - case endpoint.RecordTypeAAAA: - getterFunc = privateAAAARecordSetPropertiesGetter - case endpoint.RecordTypeCNAME: - getterFunc = privateCNameRecordSetPropertiesGetter - case endpoint.RecordTypeMX: - getterFunc = privateMXRecordSetPropertiesGetter - case endpoint.RecordTypeTXT: - getterFunc = privateTxtRecordSetPropertiesGetter - default: - getterFunc = privateOthersRecordSetPropertiesGetter - } - return &privatedns.RecordSet{ - Name: to.Ptr(name), - Type: to.Ptr("Microsoft.Network/privateDnsZones/" + recordType), - Properties: getterFunc(values, ttl), - } -} - -// newMockedAzurePrivateDNSProvider creates an AzureProvider comprising the mocked clients for zones and recordsets -func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones []*privatedns.PrivateZone, recordSets []*privatedns.RecordSet) (*AzurePrivateDNSProvider, error) { - zonesClient := newMockPrivateZonesClient(zones) - recordSetsClient := newMockPrivateRecordSectsClient(recordSets) - return newAzurePrivateDNSProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil -} - -func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider { - return &AzurePrivateDNSProvider{ - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - dryRun: dryRun, - resourceGroup: resourceGroup, - zonesClient: privateZonesClient, - recordSetsClient: privateRecordsClient, - } -} - -func TestAzurePrivateDNSRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", - []*privatedns.PrivateZone{ - createMockPrivateZone("example.com", "/privateDnsZones/example.com"), - }, - []*privatedns.RecordSet{ - createPrivateMockRecordSet("@", "NS", "ns1-03.azure-dns.com."), - createPrivateMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"), - createPrivateMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122"), - createPrivateMockRecordSet("@", endpoint.RecordTypeAAAA, "2001::123:123:123:122"), - createPrivateMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), - createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), - createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeAAAA, "2001::123:123:123:123", 3600), - createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), - createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), - createPrivateMockRecordSetWithTTL("mail", endpoint.RecordTypeMX, "10 example.com", 4000), - }) - if err != nil { - t.Fatal(err) - } - - actual, err := provider.Records(context.Background()) - if err != nil { - t.Fatal(err) - } - expected := []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::123:123:123:122"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), - endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com"), - } - - validateAzureEndpoints(t, actual, expected) -} - -func TestAzurePrivateDNSMultiRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", - []*privatedns.PrivateZone{ - createMockPrivateZone("example.com", "/privateDnsZones/example.com"), - }, - []*privatedns.RecordSet{ - createPrivateMockRecordSet("@", "NS", "ns1-03.azure-dns.com."), - createPrivateMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"), - createPrivateMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"), - createPrivateMockRecordSet("@", endpoint.RecordTypeAAAA, "2001::123:123:123:122", "2001::234:234:234:233"), - createPrivateMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), - createPrivateMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), - createPrivateMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"), - createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), - createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), - createPrivateMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), - }) - if err != nil { - t.Fatal(err) - } - - actual, err := provider.Records(context.Background()) - if err != nil { - t.Fatal(err) - } - expected := []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122", "234.234.234.233"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::123:123:123:122", "2001::234:234:234:233"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), - endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), - } - - validateAzureEndpoints(t, actual, expected) -} - -func TestAzurePrivateDNSApplyChanges(t *testing.T) { - recordsClient := mockPrivateRecordSetsClient{} - - testAzurePrivateDNSApplyChangesInternal(t, false, &recordsClient) - - validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""), - endpoint.NewEndpoint("deletedaaaa.example.com", endpoint.RecordTypeAAAA, ""), - endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""), - }) - - validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4"), - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "other.com"), - endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "5.6.7.8"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::5:6:7:8"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), - endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), - endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), - endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - }) -} - -func TestAzurePrivateDNSApplyChangesDryRun(t *testing.T) { - recordsClient := mockRecordSetsClient{} - - testAzureApplyChangesInternal(t, true, &recordsClient) - - validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{}) - - validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{}) -} - -func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client PrivateRecordSetsClient) { - zones := []*privatedns.PrivateZone{ - createMockPrivateZone("example.com", "/privateDnsZones/example.com"), - createMockPrivateZone("other.com", "/privateDnsZones/other.com"), - } - zonesClient := newMockPrivateZonesClient(zones) - - provider := newAzurePrivateDNSProvider( - endpoint.NewDomainFilter([]string{""}), - provider.NewZoneIDFilter([]string{""}), - dryRun, - "group", - &zonesClient, - client, - ) - - createRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:4"), - endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"), - endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.5", "1.2.3.4"), - endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:5", "2001::1:2:3:4"), - endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"), - endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"), - endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"), - endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"), - endpoint.NewEndpoint("other.com", endpoint.RecordTypeAAAA, "2001::5:6:7:8"), - endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"), - endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"), - endpoint.NewEndpoint("nope.com", endpoint.RecordTypeAAAA, "2001::4:4:4:4"), - endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"), - endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeMX, "10 other.com"), - endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeTXT, "tag"), - } - - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"), - endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"), - endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"), - endpoint.NewEndpoint("oldmail.example.com", endpoint.RecordTypeMX, "20 foo.other.com"), - } - updatedRecords := []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), - endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), - endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), - endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"), - endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"), - endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), - } - - deleteRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "111.222.111.222"), - endpoint.NewEndpoint("deletedaaaa.example.com", endpoint.RecordTypeAAAA, "2001::111:222:111:222"), - endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"), - endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"), - endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"), - } - - changes := &plan.Changes{ - Create: createRecords, - UpdateNew: updatedRecords, - UpdateOld: currentRecords, - Delete: deleteRecords, - } - - if err := provider.ApplyChanges(context.Background(), changes); err != nil { - t.Fatal(err) - } -} diff --git a/internal/external-dns/azure/azure.go b/internal/external-dns/provider/azure/azure.go similarity index 78% rename from internal/external-dns/azure/azure.go rename to internal/external-dns/provider/azure/azure.go index dc47d8c0..0a1433bb 100644 --- a/internal/external-dns/azure/azure.go +++ b/internal/external-dns/provider/azure/azure.go @@ -19,14 +19,14 @@ package azure import ( "context" + "errors" "fmt" "strings" - log "github.com/sirupsen/logrus" - azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "github.com/go-logr/logr" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" @@ -60,46 +60,61 @@ type AzureProvider struct { userAssignedIdentityClientID string zonesClient ZonesClient recordSetsClient RecordSetsClient + logger logr.Logger } -// NewAzureProvider creates a new Azure provider. -// -// Returns the provider or an error if a provider could not be created. -func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { - cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID) +func NewAzureProviderFromConfig(ctx context.Context, azureConfig Config) (*AzureProvider, error) { + cred, clientOpts, err := getCredentials(ctx, azureConfig) if err != nil { - return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) + return nil, err } - cred, clientOpts, err := getCredentials(*cfg) + + zonesClient, err := dns.NewZonesClient(azureConfig.SubscriptionID, cred, clientOpts) if err != nil { - return nil, fmt.Errorf("failed to get credentials: %w", err) + return nil, err } - zonesClient, err := dns.NewZonesClient(cfg.SubscriptionID, cred, clientOpts) + recordSetsClient, err := dns.NewRecordSetsClient(azureConfig.SubscriptionID, cred, clientOpts) if err != nil { return nil, err } - recordSetsClient, err := dns.NewRecordSetsClient(cfg.SubscriptionID, cred, clientOpts) + + p := &AzureProvider{ + domainFilter: azureConfig.DomainFilter, + zoneNameFilter: azureConfig.ZoneNameFilter, + zoneIDFilter: azureConfig.IDFilter, + dryRun: azureConfig.DryRun, + zonesClient: zonesClient, + recordSetsClient: recordSetsClient, + resourceGroup: azureConfig.ResourceGroup, + logger: logr.FromContextOrDiscard(ctx), + } + + return p, nil +} + +// NewAzureProvider creates a new Azure provider. +// +// Returns the provider or an error if a provider could not be created. +func NewAzureProvider(ctx context.Context, configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { + cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) } - return &AzureProvider{ - domainFilter: domainFilter, - zoneNameFilter: zoneNameFilter, - zoneIDFilter: zoneIDFilter, - dryRun: dryRun, - resourceGroup: cfg.ResourceGroup, - userAssignedIdentityClientID: cfg.UserAssignedIdentityID, - zonesClient: zonesClient, - recordSetsClient: recordSetsClient, - }, nil + + cfg.DomainFilter = domainFilter + cfg.ZoneNameFilter = zoneNameFilter + cfg.IDFilter = zoneIDFilter + cfg.DryRun = dryRun + + return NewAzureProviderFromConfig(ctx, *cfg) } // Records gets the current records. // // Returns the current records or an error if the operation failed. func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { - zones, err := p.zones(ctx) + zones, err := p.Zones(ctx) if err != nil { return nil, err } @@ -113,7 +128,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp } for _, recordSet := range nextResult.Value { if recordSet.Name == nil || recordSet.Type == nil { - log.Error("Skipping invalid record set with nil name or type.") + p.logger.Error(errors.New("record set has nil name or type"), "Skipping invalid record set with nil name or type") continue } recordType := strings.TrimPrefix(*recordSet.Type, "Microsoft.Network/dnszones/") @@ -122,12 +137,12 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp } name := formatAzureDNSName(*recordSet.Name, *zone.Name) if len(p.zoneNameFilter.Filters) > 0 && !p.domainFilter.Match(name) { - log.Debugf("Skipping return of record %s because it was filtered out by the specified --domain-filter", name) + p.logger.V(1).Info("skipping return of record because it was filtered out by the specified --domain-filter", "record name", name) continue } targets := extractAzureTargets(recordSet) if len(targets) == 0 { - log.Debugf("Failed to extract targets for '%s' with type '%s'.", name, recordType) + p.logger.V(1).Info("failed to extract targets from record set", "record name", name, "record type", recordType) continue } var ttl endpoint.TTL @@ -135,12 +150,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp ttl = endpoint.TTL(*recordSet.Properties.TTL) } ep := endpoint.NewEndpointWithTTL(name, recordType, ttl, targets...) - log.Debugf( - "Found %s record for '%s' with target '%s'.", - ep.RecordType, - ep.DNSName, - ep.Targets, - ) + p.logger.V(1).Info("found record set", "record type", ep.RecordType, "DNS Name", ep.DNSName, "targets", ep.Targets) endpoints = append(endpoints, ep) } } @@ -152,7 +162,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp // // Returns nil if the operation was successful or an error if the operation failed. func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - zones, err := p.zones(ctx) + zones, err := p.Zones(ctx) if err != nil { return err } @@ -163,8 +173,8 @@ func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return nil } -func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) { - log.Debugf("Retrieving Azure DNS zones for resource group: %s.", p.resourceGroup) +func (p *AzureProvider) Zones(ctx context.Context) ([]dns.Zone, error) { + p.logger.V(1).Info("retrieving azure DNS Zones for resource group", "resource group", p.resourceGroup) var zones []dns.Zone pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &dns.ZonesClientListByResourceGroupOptions{Top: nil}) for pager.More() { @@ -181,7 +191,7 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) { } } } - log.Debugf("Found %d Azure DNS zone(s).", len(zones)) + p.logger.V(1).Info("found azure DNS Zones", "zones count", len(zones)) return zones, nil } @@ -211,7 +221,7 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu if zone == "" { if _, ok := ignored[change.DNSName]; !ok { ignored[change.DNSName] = true - log.Infof("Ignoring changes to '%s' because a suitable Azure DNS zone was not found.", change.DNSName) + p.logger.Info("ignoring changes because a suitable Azure DNS zone was not found", "change DNS Name", change.DNSName) } return } @@ -239,21 +249,15 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa for _, ep := range endpoints { name := p.recordSetNameForZone(zone, ep) if !p.domainFilter.Match(ep.DNSName) { - log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) + p.logger.V(1).Info("skipping deletion of record as it was filtered out by the specified --domain-filter", "record name", ep.DNSName) continue } if p.dryRun { - log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone) + p.logger.Info("would delete record", "record type", ep.RecordType, "record name", name, "zone", zone) } else { - log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone) + p.logger.Info("deleting record", "record type", ep.RecordType, "record name", name, "zone", zone) if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), nil); err != nil { - log.Errorf( - "Failed to delete %s record named '%s' for Azure DNS zone '%s': %v", - ep.RecordType, - name, - zone, - err, - ) + p.logger.Error(err, "failed to delete record", "record type", ep.RecordType, "record name", name, "zone", zone) } } } @@ -265,27 +269,14 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa for _, ep := range endpoints { name := p.recordSetNameForZone(zone, ep) if !p.domainFilter.Match(ep.DNSName) { - log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) + p.logger.V(1).Info("skipping update of record because it was filtered by the specified --domain-filter", "record name", ep.DNSName) continue } if p.dryRun { - log.Infof( - "Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.", - ep.RecordType, - name, - ep.Targets, - zone, - ) + p.logger.Info("would update record", "record type", ep.RecordType, "record name", name, "targets", ep.Targets, "zone", zone) continue } - - log.Infof( - "Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.", - ep.RecordType, - name, - ep.Targets, - zone, - ) + p.logger.Info("updating record", "record type", ep.RecordType, "record name", name, "targets", ep.Targets, "zone", zone) recordSet, err := p.newRecordSet(ep) if err == nil { @@ -300,14 +291,7 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa ) } if err != nil { - log.Errorf( - "Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v", - ep.RecordType, - name, - ep.Targets, - zone, - err, - ) + p.logger.Error(err, "failed to update record", "record type", ep.RecordType, "record name", name, "targets", ep.Targets, "zone", zone) } } } diff --git a/internal/external-dns/azure/azure_test.go b/internal/external-dns/provider/azure/azure_test.go similarity index 94% rename from internal/external-dns/azure/azure_test.go rename to internal/external-dns/provider/azure/azure_test.go index 4fd9fa8f..6265c053 100644 --- a/internal/external-dns/azure/azure_test.go +++ b/internal/external-dns/provider/azure/azure_test.go @@ -26,9 +26,10 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" + + "github.com/kuadrant/dns-operator/internal/external-dns/testutils" ) // mockZonesClient implements the methods of the Azure DNS Zones Client which are used in the Azure Provider @@ -258,7 +259,7 @@ func TestAzureRecord(t *testing.T) { createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), createMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), createMockRecordSetWithTTL("nginx", endpoint.RecordTypeAAAA, "2001::123:123:123:123", 3600), - createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), + createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", azureRecordTTL), createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com"), }) @@ -277,7 +278,7 @@ func TestAzureRecord(t *testing.T) { endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), + endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, azureRecordTTL, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com"), } @@ -298,7 +299,7 @@ func TestAzureMultiRecord(t *testing.T) { createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), createMockRecordSetMultiWithTTL("nginx", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"), - createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), + createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", azureRecordTTL), createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), createMockRecordSetMultiWithTTL("mail", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), }) @@ -317,7 +318,7 @@ func TestAzureMultiRecord(t *testing.T) { endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123", "234.234.234.234"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeAAAA, 3600, "2001::123:123:123:123", "2001::234:234:234:234"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), + endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, azureRecordTTL, "heritage=external-dns,external-dns/owner=default"), endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"), endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, 4000, "10 example.com", "20 backup.example.com"), } @@ -337,23 +338,23 @@ func TestAzureApplyChanges(t *testing.T) { }) validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4"), - endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "other.com"), - endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "5.6.7.8"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::5:6:7:8"), - endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(azureRecordTTL), "1.2.3.4"), + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeAAAA, endpoint.TTL(azureRecordTTL), "2001::1:2:3:4"), + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(azureRecordTTL), "1.2.3.4", "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(azureRecordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), + endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(azureRecordTTL), "other.com"), + endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), + endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeA, endpoint.TTL(azureRecordTTL), "5.6.7.8"), + endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeAAAA, endpoint.TTL(azureRecordTTL), "2001::5:6:7:8"), + endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"), - endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(azureRecordTTL), "10 other.com"), + endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), }) } @@ -452,8 +453,8 @@ func TestAzureNameFilter(t *testing.T) { createMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), createMockRecordSetWithTTL("test.nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), createMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), - createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), - createMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL), + createMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", azureRecordTTL), + createMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", azureRecordTTL), createMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), }) if err != nil { @@ -468,8 +469,8 @@ func TestAzureNameFilter(t *testing.T) { expected := []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("test.nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), - endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), - endpoint.NewEndpointWithTTL("mail.nginx.example.com", endpoint.RecordTypeMX, recordTTL, "20 example.com"), + endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, azureRecordTTL, "heritage=external-dns,external-dns/owner=default"), + endpoint.NewEndpointWithTTL("mail.nginx.example.com", endpoint.RecordTypeMX, azureRecordTTL, "20 example.com"), } validateAzureEndpoints(t, actual, expected) @@ -487,9 +488,9 @@ func TestAzureApplyChangesZoneName(t *testing.T) { }) validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), - endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(azureRecordTTL), "1.2.3.4", "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(azureRecordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(azureRecordTTL), "tag"), endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), diff --git a/internal/external-dns/azure/common.go b/internal/external-dns/provider/azure/common.go similarity index 100% rename from internal/external-dns/azure/common.go rename to internal/external-dns/provider/azure/common.go diff --git a/internal/external-dns/azure/common_test.go b/internal/external-dns/provider/azure/common_test.go similarity index 99% rename from internal/external-dns/azure/common_test.go rename to internal/external-dns/provider/azure/common_test.go index b85fb5f4..9e09ce41 100644 --- a/internal/external-dns/azure/common_test.go +++ b/internal/external-dns/provider/azure/common_test.go @@ -23,7 +23,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" privatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" - "github.com/stretchr/testify/assert" ) diff --git a/internal/external-dns/azure/config.go b/internal/external-dns/provider/azure/config.go similarity index 86% rename from internal/external-dns/azure/config.go rename to internal/external-dns/provider/azure/config.go index c148baf8..52c97b66 100644 --- a/internal/external-dns/azure/config.go +++ b/internal/external-dns/provider/azure/config.go @@ -17,6 +17,7 @@ limitations under the License. package azure import ( + "context" "fmt" "os" "strings" @@ -25,12 +26,15 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - log "github.com/sirupsen/logrus" + "github.com/go-logr/logr" "gopkg.in/yaml.v2" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/provider" ) -// config represents common config items for Azure DNS and Azure Private DNS -type config struct { +// Config represents common config items for Azure DNS and Azure Private DNS +type Config struct { Cloud string `json:"cloud" yaml:"cloud"` TenantID string `json:"tenantId" yaml:"tenantId"` SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"` @@ -41,14 +45,18 @@ type config struct { UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"` UseWorkloadIdentityExtension bool `json:"useWorkloadIdentityExtension" yaml:"useWorkloadIdentityExtension"` UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"` + DomainFilter endpoint.DomainFilter + ZoneNameFilter endpoint.DomainFilter + IDFilter provider.ZoneIDFilter + DryRun bool } -func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) { +func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*Config, error) { contents, err := os.ReadFile(configFile) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) } - cfg := &config{} + cfg := &Config{} err = yaml.Unmarshal(contents, &cfg) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) @@ -66,7 +74,8 @@ func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) ( } // getAccessToken retrieves Azure API access token. -func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, error) { +func getCredentials(ctx context.Context, cfg Config) (azcore.TokenCredential, *arm.ClientOptions, error) { + logger := logr.FromContextOrDiscard(ctx).WithName("getCredentials") cloudCfg, err := getCloudConfiguration(cfg.Cloud) if err != nil { return nil, nil, fmt.Errorf("failed to get cloud configuration: %w", err) @@ -88,7 +97,7 @@ func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, err // In this case, we shouldn't try to use SPN to authenticate. !strings.EqualFold(cfg.ClientID, "msi") && !strings.EqualFold(cfg.ClientSecret, "msi") { - log.Info("Using client_id+client_secret to retrieve access token for Azure API.") + logger.Info("using client_id and client_secret to retrieve access token for Azure API") opts := &azidentity.ClientSecretCredentialOptions{ ClientOptions: clientOpts, } @@ -101,7 +110,7 @@ func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, err // Try to retrieve token with Workload Identity. if cfg.UseWorkloadIdentityExtension { - log.Info("Using workload identity extension to retrieve access token for Azure API.") + logger.Info("using workload identity extension to retrieve access token for Azure API") wiOpt := azidentity.WorkloadIdentityCredentialOptions{ ClientOptions: clientOpts, @@ -123,7 +132,7 @@ func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, err // Try to retrieve token with MSI. if cfg.UseManagedIdentityExtension { - log.Info("Using managed identity extension to retrieve access token for Azure API.") + logger.Info("using managed identity extension to retrieve access token for Azure API") msiOpt := azidentity.ManagedIdentityCredentialOptions{ ClientOptions: clientOpts, } diff --git a/internal/external-dns/azure/config_test.go b/internal/external-dns/provider/azure/config_test.go similarity index 100% rename from internal/external-dns/azure/config_test.go rename to internal/external-dns/provider/azure/config_test.go diff --git a/internal/provider/azure/azure.go b/internal/provider/azure/azure.go new file mode 100644 index 00000000..052e638b --- /dev/null +++ b/internal/provider/azure/azure.go @@ -0,0 +1,125 @@ +package azure + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "gopkg.in/yaml.v2" + + v1 "k8s.io/api/core/v1" + crlog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kuadrant/dns-operator/api/v1alpha1" + externaldnsproviderazure "github.com/kuadrant/dns-operator/internal/external-dns/provider/azure" + "github.com/kuadrant/dns-operator/internal/provider" +) + +type AzureProvider struct { + *externaldnsproviderazure.AzureProvider + azureConfig externaldnsproviderazure.Config + logger logr.Logger +} + +var _ provider.Provider = &AzureProvider{} + +func NewAzureProviderFromSecret(ctx context.Context, s *v1.Secret, c provider.Config) (provider.Provider, error) { + if string(s.Data["azure.json"]) == "" { + return nil, fmt.Errorf("the Azure provider credentials is empty") + } + + configString := string(s.Data["azure.json"]) + var azureConfig externaldnsproviderazure.Config + err := yaml.Unmarshal([]byte(configString), &azureConfig) + if err != nil { + return nil, err + } + + logger := crlog.FromContext(ctx). + WithName("azure-dns"). + WithValues("tenantId", azureConfig.TenantID, "resourceGroup", azureConfig.ResourceGroup) + ctx = crlog.IntoContext(ctx, logger) + + azureConfig.DomainFilter = c.DomainFilter + azureConfig.ZoneNameFilter = c.DomainFilter + azureConfig.IDFilter = c.ZoneIDFilter + azureConfig.DryRun = false + + azureProvider, err := externaldnsproviderazure.NewAzureProviderFromConfig(ctx, azureConfig) + + if err != nil { + return nil, fmt.Errorf("unable to create azure provider: %s", err) + } + + p := &AzureProvider{ + AzureProvider: azureProvider, + azureConfig: azureConfig, + logger: logger, + } + + return p, nil + +} + +// Register this Provider with the provider factory +func init() { + provider.RegisterProvider("azure", NewAzureProviderFromSecret, false) +} + +func (p *AzureProvider) HealthCheckReconciler() provider.HealthCheckReconciler { + return NewAzureHealthCheckReconciler() +} + +func (p *AzureProvider) ProviderSpecific() provider.ProviderSpecificLabels { + return provider.ProviderSpecificLabels{} +} + +func (p *AzureProvider) EnsureManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + var zoneID string + + if managedZone.Spec.ID != "" { + zoneID = managedZone.Spec.ID + } else { + zoneID = managedZone.Status.ID + } + + if zoneID != "" { + //Get existing managed zone + return p.getManagedZone(ctx, zoneID) + } + //Create new managed zone + return p.createManagedZone(ctx, managedZone) +} + +// DeleteManagedZone not implemented as managed zones are going away +func (p *AzureProvider) DeleteManagedZone(_ *v1alpha1.ManagedZone) error { + return nil // p.zonesClient.Delete(p.project, managedZone.Status.ID).Do() +} + +func (p *AzureProvider) getManagedZone(ctx context.Context, zoneID string) (provider.ManagedZoneOutput, error) { + logger := crlog.FromContext(ctx).WithName("getManagedZone") + zones, err := p.Zones(ctx) + if err != nil { + return provider.ManagedZoneOutput{}, err + } + + for _, zone := range zones { + logger.Info("comparing zone IDs", "found zone ID", zone.ID, "wanted zone ID", zoneID) + if *zone.ID == zoneID { + logger.Info("found zone ID", "found zone ID", zoneID, "wanted zone ID", zoneID) + return provider.ManagedZoneOutput{ + ID: *zone.ID, + DNSName: *zone.Name, + NameServers: zone.Properties.NameServers, + RecordCount: *zone.Properties.NumberOfRecordSets, + }, nil + } + } + + return provider.ManagedZoneOutput{}, fmt.Errorf("zone %s not found", zoneID) +} + +// createManagedZone not implemented as managed zones are going away +func (p *AzureProvider) createManagedZone(_ context.Context, _ *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + return provider.ManagedZoneOutput{}, nil +} diff --git a/internal/provider/azure/health.go b/internal/provider/azure/health.go new file mode 100644 index 00000000..4b418656 --- /dev/null +++ b/internal/provider/azure/health.go @@ -0,0 +1,31 @@ +package azure + +import ( + "context" + + externaldns "sigs.k8s.io/external-dns/endpoint" + + "github.com/kuadrant/dns-operator/api/v1alpha1" + "github.com/kuadrant/dns-operator/internal/provider" +) + +type AzureHealthCheckReconciler struct { +} + +var _ provider.HealthCheckReconciler = &AzureHealthCheckReconciler{} + +func NewAzureHealthCheckReconciler() *AzureHealthCheckReconciler { + return &AzureHealthCheckReconciler{} +} + +func (r *AzureHealthCheckReconciler) HealthCheckExists(_ context.Context, _ *v1alpha1.HealthCheckStatusProbe) (bool, error) { + return true, nil +} + +func (r *AzureHealthCheckReconciler) Reconcile(_ context.Context, _ provider.HealthCheckSpec, _ *externaldns.Endpoint, _ *v1alpha1.HealthCheckStatusProbe, _ string) provider.HealthCheckResult { + return provider.HealthCheckResult{} +} + +func (r *AzureHealthCheckReconciler) Delete(_ context.Context, _ *externaldns.Endpoint, _ *v1alpha1.HealthCheckStatusProbe) (provider.HealthCheckResult, error) { + return provider.HealthCheckResult{}, nil +} diff --git a/internal/provider/factory.go b/internal/provider/factory.go index 19ec81f0..c0270fcc 100644 --- a/internal/provider/factory.go +++ b/internal/provider/factory.go @@ -105,6 +105,8 @@ func NameForProviderSecret(secret *v1.Secret) (string, error) { switch secret.Type { case "kuadrant.io/aws": return "aws", nil + case "kuadrant.io/azure": + return "azure", nil case "kuadrant.io/gcp": return "google", nil case "kuadrant.io/inmemory": diff --git a/make/managedzones.mk b/make/managedzones.mk index 91d9d49f..04fd2387 100644 --- a/make/managedzones.mk +++ b/make/managedzones.mk @@ -11,10 +11,18 @@ endef ndef = $(if $(value $(1)),,$(error $(1) not set)) -LOCAL_SETUP_AWS_MZ_CONFIG=config/local-setup/managedzone/aws/managed-zone-config.env -LOCAL_SETUP_AWS_MZ_CREDS=config/local-setup/managedzone/aws/aws-credentials.env -LOCAL_SETUP_GCP_MZ_CONFIG=config/local-setup/managedzone/gcp/managed-zone-config.env -LOCAL_SETUP_GCP_MZ_CREDS=config/local-setup/managedzone/gcp/gcp-credentials.env + +LOCAL_SETUP_AWS_MZ_DIR=config/local-setup/managedzone/aws +LOCAL_SETUP_AWS_MZ_CONFIG=${LOCAL_SETUP_AWS_MZ_DIR}/managed-zone-config.env +LOCAL_SETUP_AWS_MZ_CREDS=${LOCAL_SETUP_AWS_MZ_DIR}/aws-credentials.env + +LOCAL_SETUP_GCP_MZ_DIR=config/local-setup/managedzone/gcp +LOCAL_SETUP_GCP_MZ_CONFIG=${LOCAL_SETUP_GCP_MZ_DIR}/managed-zone-config.env +LOCAL_SETUP_GCP_MZ_CREDS=${LOCAL_SETUP_GCP_MZ_DIR}/gcp-credentials.env + +LOCAL_SETUP_AZURE_MZ_DIR=config/local-setup/managedzone/azure +LOCAL_SETUP_AZURE_MZ_CONFIG=${LOCAL_SETUP_AZURE_MZ_DIR}/managed-zone-config.env +LOCAL_SETUP_AZURE_MZ_CREDS=${LOCAL_SETUP_AZURE_MZ_DIR}/azure-credentials.env .PHONY: local-setup-aws-mz-generate local-setup-aws-mz-generate: local-setup-aws-mz-config local-setup-aws-mz-credentials ## Generate AWS ManagedZone configuration and credentials for local-setup @@ -60,14 +68,39 @@ $(LOCAL_SETUP_GCP_MZ_CREDS): $(call ndef,GCP_PROJECT_ID) $(call patch-config,${LOCAL_SETUP_GCP_MZ_CREDS}.template,${LOCAL_SETUP_GCP_MZ_CREDS}) + +.PHONY: local-setup-azure-mz-generate +local-setup-azure-mz-generate: local-setup-azure-mz-config local-setup-azure-mz-credentials ## Generate Azure ManagedZone configuration and credentials for local-setup + +.PHONY: local-setup-azure-mz-clean +local-setup-azure-mz-clean: ## Remove Azure ManagedZone configuration and credentials + rm -f ${LOCAL_SETUP_AZURE_MZ_CONFIG} + rm -f ${LOCAL_SETUP_AZURE_MZ_CREDS} + +.PHONY: local-setup-azure-mz-config +local-setup-azure-mz-config: $(LOCAL_SETUP_AZURE_MZ_CONFIG) +$(LOCAL_SETUP_AZURE_MZ_CONFIG): + $(call ndef,KUADRANT_AZURE_DNS_ZONE_ID) + $(call patch-config,${LOCAL_SETUP_AZURE_MZ_CONFIG}.template,${LOCAL_SETUP_AZURE_MZ_CONFIG}) + +.PHONY: local-setup-azure-mz-credentials +local-setup-azure-mz-credentials: $(LOCAL_SETUP_AZURE_MZ_CREDS) +$(LOCAL_SETUP_AZURE_MZ_CREDS): + $(call ndef,KUADRANT_AZURE_CREDENTIALS) + $(call patch-config,${LOCAL_SETUP_AZURE_MZ_CREDS}.template,${LOCAL_SETUP_AZURE_MZ_CREDS}) + .PHONY: local-setup-managedzones local-setup-managedzones: TARGET_NAMESPACE=dnstest -local-setup-managedzones: kustomize ## Create AWS and GCP managedzones in the 'TARGET_NAMESPACE' namespace - @if [[ -f "config/local-setup/managedzone/gcp/managed-zone-config.env" && -f "config/local-setup/managedzone/gcp/gcp-credentials.env" ]]; then\ +local-setup-managedzones: kustomize ## Create AWS, Azure and GCP managedzones in the 'TARGET_NAMESPACE' namespace + @if [[ -f ${LOCAL_SETUP_GCP_MZ_CONFIG} && -f ${LOCAL_SETUP_GCP_MZ_CREDS} ]]; then\ echo "local-setup: creating managedzone for gcp config and credentials in ${TARGET_NAMESPACE}";\ - ${KUSTOMIZE} build config/local-setup/managedzone/gcp | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + ${KUSTOMIZE} build ${LOCAL_SETUP_GCP_MZ_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ fi - @if [[ -f "config/local-setup/managedzone/aws/managed-zone-config.env" && -f "config/local-setup/managedzone/aws/aws-credentials.env" ]]; then\ + @if [[ -f ${LOCAL_SETUP_AWS_MZ_CONFIG} && -f ${LOCAL_SETUP_AWS_MZ_CREDS} ]]; then\ echo "local-setup: creating managedzone for aws config and credentials in ${TARGET_NAMESPACE}";\ - ${KUSTOMIZE} build config/local-setup/managedzone/aws | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + ${KUSTOMIZE} build ${LOCAL_SETUP_AWS_MZ_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + fi + @if [[ -f ${LOCAL_SETUP_AZURE_MZ_CONFIG} && -f ${LOCAL_SETUP_AZURE_MZ_CREDS} ]]; then\ + echo "local-setup: creating managedzone for azure config and credentials in ${TARGET_NAMESPACE}";\ + ${KUSTOMIZE} build ${LOCAL_SETUP_AZURE_MZ_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ fi diff --git a/test/e2e/healthcheck_test.go b/test/e2e/healthcheck_test.go index 742ad8d5..98b21451 100644 --- a/test/e2e/healthcheck_test.go +++ b/test/e2e/healthcheck_test.go @@ -49,6 +49,10 @@ var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { Context("DNS Provider health checks", func() { It("creates health checks for a health check spec", func(ctx SpecContext) { + // azure only handles simple DNS Records, and AWS health checks require CNAMEs + if testDNSProvider == "azure" { + Skip("not yet supported for azure") + } healthChecksSupported := false if slices.Contains(supportedHealthCheckProviders, strings.ToLower(testDNSProvider)) { healthChecksSupported = true diff --git a/test/e2e/helpers/common.go b/test/e2e/helpers/common.go index d8630fb4..9dd2d13b 100644 --- a/test/e2e/helpers/common.go +++ b/test/e2e/helpers/common.go @@ -23,7 +23,7 @@ import ( ) var ( - SupportedProviders = []string{"aws", "gcp"} + SupportedProviders = []string{"aws", "gcp", "azure"} ) const ( @@ -78,7 +78,7 @@ func EndpointsForHost(ctx context.Context, provider provider.Provider, host stri func ProviderForManagedZone(ctx context.Context, mz *v1alpha1.ManagedZone, c client.Client) (provider.Provider, error) { //ToDo mnairn: We have a mismatch in naming GCP vs Google, we need to make this consistent one way or the other - providerFactory, err := provider.NewFactory(c, []string{"aws", "google"}) + providerFactory, err := provider.NewFactory(c, []string{"aws", "google", "azure"}) if err != nil { return nil, err } diff --git a/test/e2e/multi_instance/multi_record_test.go b/test/e2e/multi_instance/multi_record_test.go index 45e8e21d..34b0f7c7 100644 --- a/test/e2e/multi_instance/multi_record_test.go +++ b/test/e2e/multi_instance/multi_record_test.go @@ -251,6 +251,9 @@ var _ = Describe("Multi Record Test", func() { Context("loadbalanced", func() { It("creates and deletes distributed dns records", func(ctx SpecContext) { + if testDNSProvider == "azure" { + Skip("not yet supported for azure") + } testGeoRecords := map[string][]testDNSRecord{} By(fmt.Sprintf("creating %d loadbalanced dnsrecords accross %d clusters", len(testNamespaces)*len(testClusters), len(testClusters))) diff --git a/test/e2e/multi_instance/suite_test.go b/test/e2e/multi_instance/suite_test.go index 6e95370a..183d6881 100644 --- a/test/e2e/multi_instance/suite_test.go +++ b/test/e2e/multi_instance/suite_test.go @@ -26,6 +26,7 @@ import ( "github.com/kuadrant/dns-operator/api/v1alpha1" "github.com/kuadrant/dns-operator/internal/provider" _ "github.com/kuadrant/dns-operator/internal/provider/aws" + _ "github.com/kuadrant/dns-operator/internal/provider/azure" _ "github.com/kuadrant/dns-operator/internal/provider/google" . "github.com/kuadrant/dns-operator/test/e2e/helpers" ) diff --git a/test/e2e/multi_record_test.go b/test/e2e/multi_record_test.go index 618c380a..1a89c5de 100644 --- a/test/e2e/multi_record_test.go +++ b/test/e2e/multi_record_test.go @@ -256,6 +256,9 @@ var _ = Describe("Multi Record Test", func() { Context("loadbalanced", func() { It("makes available a hostname that can be resolved", func(ctx SpecContext) { + if testDNSProvider == "azure" { + Skip("not yet supported for azure") + } By("creating two dns records") klbHostName := "klb." + testHostname diff --git a/test/e2e/provider_errors_test.go b/test/e2e/provider_errors_test.go index 0553da17..9a7efb73 100644 --- a/test/e2e/provider_errors_test.go +++ b/test/e2e/provider_errors_test.go @@ -53,6 +53,8 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() //GCP expectedProviderErr = "location': 'notageocode', invalid" validGeoCode = "us-east1" + } else if testDNSProvider == "azure" { + Skip("not yet supported for azure") } else { //AWS expectedProviderErr = "Value 'notageocode' with length = '11' is not facet-valid with respect to length '2' for type 'ContinentCode'" @@ -142,6 +144,8 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() if testDNSProvider == "gcp" { //GCP expectedProviderErr = "weight': '-1.0' Reason: backendError, Message: Invalid Value" + } else if testDNSProvider == "azure" { + Skip("not yet supported for azure") } else { //AWS expectedProviderErr = "weight' failed to satisfy constraint: Member must have value greater than or equal to 0" diff --git a/test/e2e/single_record_test.go b/test/e2e/single_record_test.go index af67439b..5616a039 100644 --- a/test/e2e/single_record_test.go +++ b/test/e2e/single_record_test.go @@ -234,6 +234,9 @@ var _ = Describe("Single Record Test", func() { Context("loadbalanced", func() { It("makes available a hostname that can be resolved", func(ctx SpecContext) { + if testDNSProvider == "azure" { + Skip("not yet supported for azure") + } testTargetIP := "127.0.0.1" klbHostName := "klb." + testHostname diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index b73b4c43..08f706a4 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -20,6 +20,7 @@ import ( "github.com/kuadrant/dns-operator/api/v1alpha1" _ "github.com/kuadrant/dns-operator/internal/provider/aws" + _ "github.com/kuadrant/dns-operator/internal/provider/azure" _ "github.com/kuadrant/dns-operator/internal/provider/google" . "github.com/kuadrant/dns-operator/test/e2e/helpers" ) @@ -41,7 +42,7 @@ var ( testManagedZoneName string testNamespace string testDNSProvider string - supportedProviders = []string{"aws", "gcp"} + supportedProviders = []string{"aws", "gcp", "azure"} supportedHealthCheckProviders = []string{"aws"} testManagedZone *v1alpha1.ManagedZone )