diff --git a/.github/workflows/ci-e2e.yaml b/.github/workflows/ci-e2e.yaml new file mode 100644 index 00000000..7841ea27 --- /dev/null +++ b/.github/workflows/ci-e2e.yaml @@ -0,0 +1,70 @@ +name: CI-E2E + +on: + push: + branches: + - main + - "release-*" + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + paths-ignore: + - '**.adoc' + - '**.md' + - 'samples/**' + - 'LICENSE' + pull_request_target: + branches: + - main + - "release-*" + paths-ignore: + - '**.adoc' + - '**.md' + - 'samples/**' + - 'LICENSE' + workflow_dispatch: + +env: + TEST_NAMESPACE: e2e-test + +jobs: + e2e_test_suite: + name: E2E Test Suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - uses: actions/setup-go@v5 + with: + go-version: v1.21.x + cache: false + - name: Create AWS provider configuration + run: | + make local-setup-aws-mz-clean local-setup-aws-mz-generate AWS_ZONE_ROOT_DOMAIN=e2e.hcpapps.net AWS_DNS_PUBLIC_ZONE_ID=Z086929132US3PB46EOLR AWS_ACCESS_KEY_ID=${{ secrets.E2E_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY=${{ secrets.E2E_AWS_SECRET_ACCESS_KEY }} + - 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: 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-gcp + - name: Run suite AWS + run: | + export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.hcpapps.net + export TEST_DNS_NAMESPACE=${{ env.TEST_NAMESPACE }} + export TEST_DNS_PROVIDER=aws + make test-e2e + - name: Run suite GCP + run: | + export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-gcp + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.google.hcpapps.net + export TEST_DNS_NAMESPACE=${{ env.TEST_NAMESPACE }} + export TEST_DNS_PROVIDER=gcp + make test-e2e + - name: Dump Controller logs + if: ${{ failure() }} + run: | + kubectl get deployments -A + kubectl logs --all-containers --ignore-errors deployments/dns-operator-controller-manager -n dns-operator-system diff --git a/Makefile b/Makefile index 27706636..087fc168 100644 --- a/Makefile +++ b/Makefile @@ -143,10 +143,31 @@ test-unit: manifests generate fmt vet ## Run unit tests. test-integration: manifests generate fmt vet envtest ## Run integration tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./internal/controller... -tags=integration -coverprofile cover-integration.out +.PHONY: test-e2e +test-e2e: ginkgo + $(GINKGO) -tags=e2e -v ./test/e2e + .PHONY: local-setup -local-setup: $(KIND) ## Setup local development kind cluster and dependencies - $(MAKE) kind-delete-cluster - $(MAKE) kind-create-cluster +local-setup: DEPLOY=false +local-setup: TEST_NAMESPACE=dnstest +local-setup: $(KIND) ## Setup local development kind cluster, dependencies and optionally deploy the dns operator DEPLOY=false|true + @echo "local-setup: KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME} DEPLOY=${DEPLOY} TEST_NAMESPACE=${TEST_NAMESPACE} " + @$(MAKE) -s kind-delete-cluster + @$(MAKE) -s kind-create-cluster + @$(MAKE) -s install + @$(KUBECTL) create namespace ${TEST_NAMESPACE} --dry-run=client -o yaml | $(KUBECTL) apply -f - + @$(MAKE) -s local-setup-managedzones TARGET_NAMESPACE=${TEST_NAMESPACE} + @if [ ${DEPLOY} = "true" ]; then\ + echo "local-setup: deploying operator to ${KIND_CLUSTER_NAME}";\ + $(MAKE) -s local-deploy;\ + echo "local-setup: waiting for dns operator deployments in namespace 'dns-operator-system'";\ + $(KUBECTL) -n dns-operator-system wait --timeout=60s --for=condition=Available deployments --all;\ + fi + @echo "local-setup: Check dns operator deployments" + $(KUBECTL) -n dns-operator-system get deployments + @echo "local-setup: Check managedzones" + $(KUBECTL) -n ${TEST_NAMESPACE} get managedzones + @echo "local-setup: Complete!!" .PHONY: local-deploy local-deploy: docker-build kind-load-image ## Deploy the dns operator into local kind cluster from the current code @@ -230,6 +251,7 @@ OPENSHIFT_GOIMPORTS ?= $(LOCALBIN)/openshift-goimports KIND = $(LOCALBIN)/kind ACT = $(LOCALBIN)/act YQ = $(LOCALBIN)/yq +GINKGO ?= $(LOCALBIN)/ginkgo ## Tool Versions KUSTOMIZE_VERSION ?= v5.0.1 @@ -238,6 +260,7 @@ OPENSHIFT_GOIMPORTS_VERSION ?= c70783e636f2213cac683f6865d88c5edace3157 KIND_VERSION = v0.20.0 ACT_VERSION = latest YQ_VERSION := v4.34.2 +GINKGO_VERSION ?= v2.13.2 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. @@ -296,6 +319,11 @@ yq: $(YQ) ## Download yq locally if necessary. $(YQ): $(LOCALBIN) GOBIN=$(LOCALBIN) go install github.com/mikefarah/yq/v4@$(YQ_VERSION) +.PHONY: ginkgo +ginkgo: $(GINKGO) ## Download ginkgo locally if necessary +$(GINKGO): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION) + .PHONY: bundle bundle: manifests manifests-gen-base-csv kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. $(OPERATOR_SDK) generate kustomize manifests -q diff --git a/README.md b/README.md index 8d5f7b2c..b35e6b5b 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,85 @@ -# dns-operator -The DNS Operator is a kubernetes based controller responsible for reconciling DNS Record and Managed Zone custom resources. It interfaces with cloud DNS providers such as AWS and Google to bring the DNS zone into the state declared in these CRDs. +# DNS Operator + +The DNS Operator is a kubernetes based controller responsible for reconciling DNS Record and Managed Zone custom resources. It interfaces with cloud DNS providers such as AWS and Google to bring the DNS zone into the state declared in these CRDs. One of the key use cases the DNS operator solves, is allowing complex DNS routing strategies such as Geo and Weighted to be expressed allowing you to leverage DNS as the first layer of traffic management. In order to make these strategies valuable, it also works across multiple clusters allowing you to use a shared domain name balance traffic based on your requirements. ## Getting Started -You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. -**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). -### Running on the cluster -1. Install Instances of Custom Resources: +### Pre Setup -```sh -kubectl apply -f config/samples/ +#### Add DNS provider configuration + +**NOTE:** You can optionally skip this step but at least one ManagedZone will need to be configured and have valid credentials linked to use the DNS Operator. + +##### AWS Provider (Route53) +```bash +make local-setup-aws-mz-clean local-setup-aws-mz-generate AWS_ZONE_ROOT_DOMAIN= AWS_DNS_PUBLIC_ZONE_ID= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= ``` +More details about the AWS provider can be found [here](./docs/provider.md#aws-route-53-provider) -2. Build and push your image to the location specified by `IMG`: +##### GCP Provider -```sh -make docker-build docker-push IMG=/dns-operator:tag +```bash +make local-setup-gcp-mz-clean local-setup-gcp-mz-generate GCP_ZONE_NAME= GCP_ZONE_DNS_NAME= GCP_GOOGLE_CREDENTIALS='' GCP_PROJECT_ID= ``` +More details about the GCP provider can be found [here](./docs/provider.md#google-cloud-dns-provider) -3. Deploy the controller to the cluster with the image specified by `IMG`: +### Running controller locally (default) +1. Create local environment(creates kind cluster) ```sh -make deploy IMG=/dns-operator:tag +make local-setup ``` -### Uninstall CRDs -To delete the CRDs from the cluster: +1. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): ```sh -make uninstall +make run ``` -### Undeploy controller -UnDeploy the controller from the cluster: +### Running controller on the cluster +1. Create local environment(creates kind cluster) ```sh -make undeploy +make local-setup DEPLOY=true ``` -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project +1. Verify controller deployment +```sh +kubectl logs -f deployments/dns-operator-controller-manager -n dns-operator-system +``` -### How it works -This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). +### Running controller on existing cluster -It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), -which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). -### Test It Out -1. Install the CRDs into the cluster: +1. Apply Operator manifests +```sh +kustomize build config/default | kubectl apply -f - +``` +1. Verify controller deployment ```sh -make install +kubectl logs -f deployments/dns-operator-controller-manager -n dns-operator-system ``` -2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): +## Development + +### E2E Test Suite + +The e2e test suite can be executed against any cluster running the DNS Operator with configuration added for any supported provider. -```sh -make run +``` +make test-e2e TEST_DNS_MANAGED_ZONE_NAME= TEST_DNS_ZONE_DOMAIN_NAME= TEST_DNS_NAMESPACE= TEST_DNS_PROVIDER= ``` -**NOTE:** You can also run this in one step by running: `make install run` +| Environment Variable | Description | +|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| TEST_DNS_MANAGED_ZONE_NAME | Name of the managed zone relevant for the test domain (TEST_DNS_ZONE_DOMAIN_NAME). If using local-setup Managed zones, one of [dev-mz-aws; dev-mz-gcp] | +| TEST_DNS_ZONE_DOMAIN_NAME | Domain name being used for the test, must match the domain of the managed zone (TEST_DNS_MANAGED_ZONE_NAME) | +| TEST_DNS_NAMESPACE | The namespace to run the test in, must be the same namespace as the TEST_DNS_MANAGED_ZONE_NAME | +| TEST_DNS_PROVIDER | DNS Provider currently being tested, one of [aws; gcp] | ### Modifying the API definitions If you are editing the API definitions, generate the manifests such as CRs or CRDs using: @@ -89,4 +107,3 @@ 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. - diff --git a/config/local-setup/managedzone/aws/aws-credentials.env.template b/config/local-setup/managedzone/aws/aws-credentials.env.template new file mode 100644 index 00000000..efc9f80e --- /dev/null +++ b/config/local-setup/managedzone/aws/aws-credentials.env.template @@ -0,0 +1,3 @@ +AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} +AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} +AWS_REGION=${AWS_REGION} diff --git a/config/local-setup/managedzone/aws/kustomization.yaml b/config/local-setup/managedzone/aws/kustomization.yaml index b69e4dd4..daa87a19 100644 --- a/config/local-setup/managedzone/aws/kustomization.yaml +++ b/config/local-setup/managedzone/aws/kustomization.yaml @@ -31,7 +31,7 @@ replacements: kind: ConfigMap name: aws-managed-zone-config version: v1 - fieldPath: data.ZONE_ROOT_DOMAIN + fieldPath: data.AWS_ZONE_ROOT_DOMAIN targets: - select: kind: ManagedZone diff --git a/config/local-setup/managedzone/aws/managed-zone-config.env.template b/config/local-setup/managedzone/aws/managed-zone-config.env.template new file mode 100644 index 00000000..9e154b85 --- /dev/null +++ b/config/local-setup/managedzone/aws/managed-zone-config.env.template @@ -0,0 +1,2 @@ +AWS_DNS_PUBLIC_ZONE_ID=${AWS_DNS_PUBLIC_ZONE_ID} +AWS_ZONE_ROOT_DOMAIN=${AWS_ZONE_ROOT_DOMAIN} diff --git a/config/local-setup/managedzone/gcp/gcp-credentials.env.template b/config/local-setup/managedzone/gcp/gcp-credentials.env.template new file mode 100644 index 00000000..dd1822ec --- /dev/null +++ b/config/local-setup/managedzone/gcp/gcp-credentials.env.template @@ -0,0 +1,2 @@ +GOOGLE=${GCP_GOOGLE_CREDENTIALS} +PROJECT_ID=${GCP_PROJECT_ID} diff --git a/config/local-setup/managedzone/gcp/kustomization.yaml b/config/local-setup/managedzone/gcp/kustomization.yaml index a0e92cfa..75671bfe 100644 --- a/config/local-setup/managedzone/gcp/kustomization.yaml +++ b/config/local-setup/managedzone/gcp/kustomization.yaml @@ -20,7 +20,7 @@ replacements: kind: ConfigMap name: gcp-managed-zone-config version: v1 - fieldPath: data.ZONE_NAME + fieldPath: data.GCP_ZONE_NAME targets: - select: kind: ManagedZone @@ -31,7 +31,7 @@ replacements: kind: ConfigMap name: gcp-managed-zone-config version: v1 - fieldPath: data.ZONE_DNS_NAME + fieldPath: data.GCP_ZONE_DNS_NAME targets: - select: kind: ManagedZone diff --git a/config/local-setup/managedzone/gcp/managed-zone-config.env.template b/config/local-setup/managedzone/gcp/managed-zone-config.env.template new file mode 100644 index 00000000..63576e1c --- /dev/null +++ b/config/local-setup/managedzone/gcp/managed-zone-config.env.template @@ -0,0 +1,2 @@ +GCP_ZONE_NAME=${GCP_ZONE_NAME} +GCP_ZONE_DNS_NAME=${GCP_ZONE_DNS_NAME} diff --git a/go.mod b/go.mod index 6df9ca13..8c0759a0 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.44.311 - github.com/go-logr/logr v1.2.4 - github.com/onsi/ginkgo/v2 v2.11.0 - github.com/onsi/gomega v1.27.10 + github.com/go-logr/logr v1.3.0 + github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e + github.com/onsi/ginkgo/v2 v2.13.2 + github.com/onsi/gomega v1.30.0 github.com/prometheus/client_golang v1.17.0 google.golang.org/api v0.134.0 k8s.io/api v0.28.3 @@ -76,7 +77,7 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index 7dfc0dde..b8b48dc6 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= @@ -377,6 +378,8 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -573,16 +576,16 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +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 v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +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/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -938,8 +941,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/make/managedzones.mk b/make/managedzones.mk new file mode 100644 index 00000000..91d9d49f --- /dev/null +++ b/make/managedzones.mk @@ -0,0 +1,73 @@ + +##@ ManagedZones + +## Targets to help configure ManagedZones for local-setup + +define patch-config + envsubst \ + < $1 \ + > $2 +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 + +.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 + +.PHONY: local-setup-aws-mz-clean +local-setup-aws-mz-clean: ## Remove AWS ManagedZone configuration and credentials + rm -f ${LOCAL_SETUP_AWS_MZ_CONFIG} + rm -f ${LOCAL_SETUP_AWS_MZ_CREDS} + +.PHONY: local-setup-aws-mz-config +local-setup-aws-mz-config: $(LOCAL_SETUP_AWS_MZ_CONFIG) +$(LOCAL_SETUP_AWS_MZ_CONFIG): + $(call ndef,AWS_DNS_PUBLIC_ZONE_ID) + $(call ndef,AWS_ZONE_ROOT_DOMAIN) + $(call patch-config,${LOCAL_SETUP_AWS_MZ_CONFIG}.template,${LOCAL_SETUP_AWS_MZ_CONFIG}) + +.PHONY: local-setup-aws-mz-credentials +local-setup-aws-mz-credentials: $(LOCAL_SETUP_AWS_MZ_CREDS) +$(LOCAL_SETUP_AWS_MZ_CREDS): + $(call ndef,AWS_ACCESS_KEY_ID) + $(call ndef,AWS_SECRET_ACCESS_KEY) + $(call patch-config,${LOCAL_SETUP_AWS_MZ_CREDS}.template,${LOCAL_SETUP_AWS_MZ_CREDS}) + +.PHONY: local-setup-gcp-mz-generate +local-setup-gcp-mz-generate: local-setup-gcp-mz-config local-setup-gcp-mz-credentials ## Generate GCP ManagedZone configuration and credentials for local-setup + +.PHONY: local-setup-gcp-mz-clean +local-setup-gcp-mz-clean: ## Remove GCP ManagedZone configuration and credentials + rm -f ${LOCAL_SETUP_GCP_MZ_CONFIG} + rm -f ${LOCAL_SETUP_GCP_MZ_CREDS} + +.PHONY: local-setup-gcp-mz-config +local-setup-gcp-mz-config: $(LOCAL_SETUP_GCP_MZ_CONFIG) +$(LOCAL_SETUP_GCP_MZ_CONFIG): + $(call ndef,GCP_ZONE_NAME) + $(call ndef,GCP_ZONE_DNS_NAME) + $(call patch-config,${LOCAL_SETUP_GCP_MZ_CONFIG}.template,${LOCAL_SETUP_GCP_MZ_CONFIG}) + +.PHONY: local-setup-gcp-mz-credentials +local-setup-gcp-mz-credentials: $(LOCAL_SETUP_GCP_MZ_CREDS) +$(LOCAL_SETUP_GCP_MZ_CREDS): + $(call ndef,GCP_GOOGLE_CREDENTIALS) + $(call ndef,GCP_PROJECT_ID) + $(call patch-config,${LOCAL_SETUP_GCP_MZ_CREDS}.template,${LOCAL_SETUP_GCP_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\ + 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 -;\ + fi + @if [[ -f "config/local-setup/managedzone/aws/managed-zone-config.env" && -f "config/local-setup/managedzone/aws/aws-credentials.env" ]]; 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 -;\ + fi diff --git a/test/e2e/single_cluster_record_test.go b/test/e2e/single_cluster_record_test.go new file mode 100644 index 00000000..828c4680 --- /dev/null +++ b/test/e2e/single_cluster_record_test.go @@ -0,0 +1,213 @@ +//go:build e2e + +package e2e + +import ( + "context" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" + + "github.com/kuadrant/dns-operator/api/v1alpha1" + "github.com/kuadrant/dns-operator/internal/common/conditions" +) + +var _ = Describe("Single Cluster Record Test", func() { + // testID is a randomly generated identifier for the test + // it is used to name resources and/or namespaces so different + // tests can be run in parallel in the same cluster + var testID string + // testDomainName generated domain for this test e.g. t-e2e-12345.e2e.hcpapps.net + var testDomainName string + // testHostname generated hostname for this test e.g. t-gw-mgc-12345.t-e2e-12345.e2e.hcpapps.net + var testHostname string + + var dnsRecord *v1alpha1.DNSRecord + var geoCode string + + BeforeEach(func(ctx SpecContext) { + testID = "t-single-" + GenerateName() + testDomainName = strings.Join([]string{testSuiteID, testZoneDomainName}, ".") + testHostname = strings.Join([]string{testID, testDomainName}, ".") + + if testDNSProvider == "gcp" { + geoCode = "us-east1" + } else { + geoCode = "US" + } + }) + + AfterEach(func(ctx SpecContext) { + if dnsRecord != nil { + err := k8sClient.Delete(ctx, dnsRecord, + client.PropagationPolicy(metav1.DeletePropagationForeground)) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) + } + }) + + Context("simple", func() { + It("makes available a hostname that can be resolved", func(ctx SpecContext) { + By("creating a dns record") + testTargetIP := "127.0.0.1" + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + ManagedZoneRef: &v1alpha1.ManagedZoneReference{ + Name: testManagedZoneName, + }, + Endpoints: []*externaldnsendpoint.Endpoint{ + { + DNSName: testHostname, + Targets: []string{ + testTargetIP, + }, + RecordType: "A", + RecordTTL: 60, + }, + }, + }, + } + err := k8sClient.Create(ctx, dnsRecord) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(conditions.ConditionTypeReady)), + "Status": Equal(metav1.ConditionTrue), + })), + ) + }, 300*time.Second, 10*time.Second, ctx).Should(Succeed()) + + By("ensuring the authoritative nameserver resolves the hostname") + // speed up things by using the authoritative nameserver + authoritativeResolver := ResolverForDomainName(testZoneDomainName) + Eventually(func(g Gomega, ctx context.Context) { + ips, err := authoritativeResolver.LookupHost(ctx, testHostname) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ips).To(ContainElement(testTargetIP)) + }, 300*time.Second, 10*time.Second, ctx).Should(Succeed()) + }) + }) + + Context("loadbalanced", func() { + It("makes available a hostname that can be resolved", func(ctx SpecContext) { + By("creating a dns record") + testTargetIP := "127.0.0.1" + + klbHostName := "klb." + testHostname + geo1KlbHostName := geoCode + "." + klbHostName + cluster1KlbHostName := "cluster1." + klbHostName + + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-record", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + ManagedZoneRef: &v1alpha1.ManagedZoneReference{ + Name: testManagedZoneName, + }, + Endpoints: []*externaldnsendpoint.Endpoint{ + { + DNSName: cluster1KlbHostName, + Targets: []string{ + testTargetIP, + }, + RecordType: "A", + RecordTTL: 60, + }, + { + DNSName: testHostname, + Targets: []string{ + klbHostName, + }, + RecordType: "CNAME", + RecordTTL: 300, + }, + { + DNSName: geo1KlbHostName, + Targets: []string{ + cluster1KlbHostName, + }, + RecordType: "CNAME", + RecordTTL: 60, + SetIdentifier: cluster1KlbHostName, + ProviderSpecific: externaldnsendpoint.ProviderSpecific{ + { + Name: "weight", + Value: "200", + }, + }, + }, + { + DNSName: klbHostName, + Targets: []string{ + geo1KlbHostName, + }, + RecordType: "CNAME", + RecordTTL: 300, + SetIdentifier: geoCode, + ProviderSpecific: externaldnsendpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: geoCode, + }, + }, + }, + { + DNSName: klbHostName, + Targets: []string{ + geo1KlbHostName, + }, + RecordType: "CNAME", + RecordTTL: 300, + SetIdentifier: "default", + ProviderSpecific: externaldnsendpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "*", + }, + }, + }, + }, + }, + } + err := k8sClient.Create(ctx, dnsRecord) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(conditions.ConditionTypeReady)), + "Status": Equal(metav1.ConditionTrue), + })), + ) + }, 300*time.Second, 10*time.Second, ctx).Should(Succeed()) + + By("ensuring the authoritative nameserver resolves the hostname") + // speed up things by using the authoritative nameserver + authoritativeResolver := ResolverForDomainName(testZoneDomainName) + Eventually(func(g Gomega, ctx context.Context) { + ips, err := authoritativeResolver.LookupHost(ctx, testHostname) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ips).To(ContainElement(testTargetIP)) + }, 300*time.Second, 10*time.Second, ctx).Should(Succeed()) + }) + }) + +}) diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go new file mode 100644 index 00000000..02f39cc1 --- /dev/null +++ b/test/e2e/suite_test.go @@ -0,0 +1,115 @@ +//go:build e2e + +package e2e + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "net" + "os" + "slices" + "strings" + "testing" + "time" + + "github.com/goombaio/namegenerator" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/kuadrant/dns-operator/api/v1alpha1" +) + +const ( + // configuration environment variables + dnsZoneDomainNameEnvvar = "TEST_DNS_ZONE_DOMAIN_NAME" + dnsManagedZoneName = "TEST_DNS_MANAGED_ZONE_NAME" + dnsNamespace = "TEST_DNS_NAMESPACE" + dnsProvider = "TEST_DNS_PROVIDER" +) + +var ( + k8sClient client.Client + // testSuiteID is a randomly generated identifier for the test suite + testSuiteID string + // testZoneDomainName provided domain name for the testZoneID e.g. e2e.hcpapps.net + testZoneDomainName string + testManagedZoneName string + testNamespace string + testDNSProvider string + supportedProviders = []string{"aws", "gcp"} +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2E Tests Suite") +} + +var _ = BeforeSuite(func(ctx SpecContext) { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + err := setConfigFromEnvVars() + Expect(err).NotTo(HaveOccurred()) + + err = v1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ).ClientConfig() + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + testSuiteID = "dns-op-e2e-" + GenerateName() +}) + +func ResolverForDomainName(domainName string) *net.Resolver { + nameservers, err := net.LookupNS(domainName) + Expect(err).ToNot(HaveOccurred()) + GinkgoWriter.Printf("[debug] authoritative nameserver used for DNS record resolution: %s\n", nameservers[0].Host) + + authoritativeResolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{Timeout: 10 * time.Second} + return d.DialContext(ctx, network, strings.Join([]string{nameservers[0].Host, "53"}, ":")) + }, + } + return authoritativeResolver +} + +func setConfigFromEnvVars() error { + // Load test suite configuration from the environment + if testZoneDomainName = os.Getenv(dnsZoneDomainNameEnvvar); testZoneDomainName == "" { + return fmt.Errorf("env variable '%s' must be set", dnsZoneDomainNameEnvvar) + } + if testManagedZoneName = os.Getenv(dnsManagedZoneName); testManagedZoneName == "" { + return fmt.Errorf("env variable '%s' must be set", dnsManagedZoneName) + } + if testNamespace = os.Getenv(dnsNamespace); testNamespace == "" { + return fmt.Errorf("env variable '%s' must be set", dnsNamespace) + } + if testDNSProvider = os.Getenv(dnsProvider); testDNSProvider == "" { + return fmt.Errorf("env variable '%s' must be set", dnsProvider) + } + if !slices.Contains(supportedProviders, testDNSProvider) { + return fmt.Errorf("unsupported provider '%s' must be one of '%s'", testDNSProvider, supportedProviders) + } + return nil +} + +func GenerateName() string { + nBig, _ := rand.Int(rand.Reader, big.NewInt(1000000)) + return namegenerator.NewNameGenerator(nBig.Int64()).Generate() +}