diff --git a/.github/workflows/ci-e2e.yaml b/.github/workflows/ci-e2e.yaml index 017de05b..e7478ab3 100644 --- a/.github/workflows/ci-e2e.yaml +++ b/.github/workflows/ci-e2e.yaml @@ -5,6 +5,7 @@ on: branches: - main - "release-*" + - remove_managed_zone_api tags: - "v[0-9]+.[0-9]+.[0-9]+" paths-ignore: @@ -41,32 +42,35 @@ jobs: 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 }} + make local-setup-aws-clean local-setup-aws-generate 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 }} + make local-setup-gcp-clean local-setup-gcp-generate 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 }}' + make local-setup-azure-clean local-setup-azure-generate 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 + kubectl -n ${{ env.TEST_NAMESPACE }} get secret/dns-provider-credentials-aws + kubectl -n ${{ env.TEST_NAMESPACE }} get secret/dns-provider-credentials-gcp + kubectl -n ${{ env.TEST_NAMESPACE }} get secret/dns-provider-credentials-azure - name: Run suite AWS run: | - export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws - export TEST_DNS_NAMESPACES=${{ env.TEST_NAMESPACE }} + export TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-aws + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.hcpapps.net + export TEST_DNS_NAMESPACE=${{ env.TEST_NAMESPACE }} make test-e2e - name: Run suite GCP run: | - export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-gcp + export TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-gcp + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.google.hcpapps.net export TEST_DNS_NAMESPACES=${{ env.TEST_NAMESPACE }} make test-e2e - name: Run suite Azure run: | - export TEST_DNS_MANAGED_ZONE_NAME=dev-mz-azure + export TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-azure + export TEST_DNS_ZONE_DOMAIN_NAME=e2e.azure.hcpapps.net export TEST_DNS_NAMESPACES=${{ env.TEST_NAMESPACE }} make test-e2e - name: Dump Controller logs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b746ab3..d2db87d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ DNS Operator follows a specific code layout to maintain consistency and readabil - `cmd/`: This is the main entry point of the application. - `internal/`: Houses the core functionality of DNS Operator, organized into subpackages. also contains all the unit tests and integration tests for each subpackage in the `*_test.go` files. - `common/`: A few functions generally useful in the rest of the codebase. - - `controller/`: The controllers for the DNS Record CR and the Managed Zone CR. + - `controller/`: The controllers for the DNS Record CR. - `external-dns/`: copied and changed slightly from external-dns, ultimately expected to send these changes back to external-dns and delete this directory, more info in the [pull request](https://github.com/Kuadrant/dns-operator/pull/67). - `provider/`: Interface to various DNS Providers (e.g. AWS Route53). - `test/`: Includes test files for e2e testing. diff --git a/Makefile b/Makefile index fdc3b95b..c8efdca4 100644 --- a/Makefile +++ b/Makefile @@ -169,7 +169,7 @@ local-setup-cluster: $(KIND) ## Setup local development kind cluster, dependenci @$(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} + @$(MAKE) -s local-setup-dns-providers TARGET_NAMESPACE=${TEST_NAMESPACE} @if [ ${DEPLOY} = "true" ]; then\ if [ ${DEPLOYMENT_SCOPE} = "cluster" ]; then\ echo "local-setup: deploying operator (cluster scoped) to ${KIND_CLUSTER_NAME}" ;\ @@ -184,8 +184,8 @@ local-setup-cluster: $(KIND) ## Setup local development kind cluster, dependenci @echo "local-setup: Check dns operator deployments" $(KUBECTL) get deployments -l app.kubernetes.io/part-of=dns-operator -A - @echo "local-setup: Check managedzones" - $(KUBECTL) get managedzones -A + @echo "local-setup: Check dns providers" + $(KUBECTL) get secrets -l app.kubernetes.io/part-of=dns-operator -A @echo "local-setup: Complete!!" .PHONY: local-setup diff --git a/PROJECT b/PROJECT index 21fa3d08..008a45b8 100644 --- a/PROJECT +++ b/PROJECT @@ -12,14 +12,6 @@ plugins: projectName: dns-operator repo: github.com/kuadrant/dns-operator resources: -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: kuadrant.io - kind: ManagedZone - path: github.com/kuadrant/dns-operator/api/v1alpha1 - version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/README.md b/README.md index 545e589d..aafd242a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. +The DNS Operator is a kubernetes based controller responsible for reconciling DNS Record 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 @@ -9,25 +9,25 @@ One of the key use cases the DNS operator solves, is allowing complex DNS routin #### 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. +**NOTE:** You can optionally skip this step but at least one DNS Provider Secret will need to be configured with valid credentials 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= +make local-setup-aws-clean local-setup-aws-generate 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) ##### GCP Provider ```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= +make local-setup-gcp-clean local-setup-gcp-generate GCP_GOOGLE_CREDENTIALS='' GCP_PROJECT_ID= ``` More details about the GCP provider can be found [here](./docs/provider.md#google-cloud-dns-provider) ##### AZURE Provider ```bash -make local-setup-azure-mz-clean local-setup-azure-mz-generate KUADRANT_AZURE_CREDENTIALS='' KUADRANT_AZURE_DNS_ZONE_ID= KUADRANT_AZURE_ZONE_ROOT_DOMAIN='' +make local-setup-azure-clean local-setup-azure-generate KUADRANT_AZURE_CREDENTIALS='' ``` Info on generating service principal credentials [here](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/azure.md) @@ -84,13 +84,14 @@ kubectl logs -f deployments/dns-operator-controller-manager -n dns-operator-syst The e2e test suite can be executed against any cluster running the DNS Operator with configuration added for any supported provider. ``` -make test-e2e TEST_DNS_MANAGED_ZONE_NAME= TEST_DNS_NAMESPACES= +make test-e2e TEST_DNS_ZONE_DOMAIN_NAME= TEST_DNS_PROVIDER_SECRET_NAME= TEST_DNS_NAMESPACES= ``` -| Environment Variable | Description | -|----------------------------|------------------------------------------------------------------------------------------------------| -| TEST_DNS_MANAGED_ZONE_NAME | Name of the managed zone to use. If using local-setup Managed zones, one of [dev-mz-aws; dev-mz-gcp] | -| TEST_DNS_NAMESPACES | The namespace(s) where the managed zone with the name (TEST_DNS_MANAGED_ZONE_NAME) can be found | +| Environment Variable | Description | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| TEST_DNS_PROVIDER_SECRET_NAME | Name of the provider secret to use. If using local-setup provider secrets zones, one of [dns-provider-credentials-aws; dns-provider-credentials-gcp;dns-provider-credentials-azure] | +| TEST_DNS_ZONE_DOMAIN_NAME | The Domain name to use in the test. Must be a zone accessible with the (TEST_DNS_PROVIDER_SECRET_NAME) credentials with the same domain name | +| TEST_DNS_NAMESPACES | The namespace(s) where the provider secret(s) can be found | ### Modifying the API definitions If you are editing the API definitions, generate the manifests such as CRs or CRDs using: diff --git a/api/v1alpha1/dnsrecord_types.go b/api/v1alpha1/dnsrecord_types.go index c51c65d5..f14688db 100644 --- a/api/v1alpha1/dnsrecord_types.go +++ b/api/v1alpha1/dnsrecord_types.go @@ -86,8 +86,8 @@ type DNSRecordSpec struct { // +kubebuilder:validation:Pattern=`^(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)\.(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)$` RootHost string `json:"rootHost"` - // managedZone is a reference to a ManagedZone instance to which this record will publish its endpoints. - ManagedZoneRef *ManagedZoneReference `json:"managedZone"` + // providerRef is a reference to a provider secret. + ProviderRef ProviderRef `json:"providerRef"` // endpoints is a list of endpoints that will be published into the dns provider. // +kubebuilder:validation:MinItems=1 @@ -101,18 +101,13 @@ type DNSRecordSpec struct { // DNSRecordStatus defines the observed state of DNSRecord type DNSRecordStatus struct { - // conditions are any conditions associated with the record in the managed zone. + // conditions are any conditions associated with the record in the dns provider. // // If publishing the record fails, the "Failed" condition will be set with a // reason and message describing the cause of the failure. Conditions []metav1.Condition `json:"conditions,omitempty"` - // observedGeneration is the most recently observed generation of the - // DNSRecord. When the DNSRecord is updated, the controller updates the - // corresponding record in each managed zone. If an update for a - // particular zone fails, that failure is recorded in the status - // condition for the zone so that the controller can determine that it - // needs to retry the update for that specific zone. + // observedGeneration is the most recently observed generation of the DNSRecord. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` @@ -129,13 +124,7 @@ type DNSRecordStatus struct { // It is being reset to 0 when the generation changes or there are no changes to write. WriteCounter int64 `json:"writeCounter,omitempty"` - // endpoints are the last endpoints that were successfully published by the provider - // - // Provides a simple mechanism to store the current provider records in order to - // delete any that are no longer present in DNSRecordSpec.Endpoints - // - // Note: This will not be required if/when we switch to using external-dns since when - // running with a "sync" policy it will clean up unused records automatically. + // endpoints are the last endpoints that were successfully published to the provider zone Endpoints []*externaldns.Endpoint `json:"endpoints,omitempty"` HealthCheck *HealthCheckStatus `json:"healthCheck,omitempty"` @@ -145,6 +134,12 @@ type DNSRecordStatus struct { // DomainOwners is a list of all the owners working against the root domain of this record DomainOwners []string `json:"domainOwners,omitempty"` + + // zoneID is the provider specific id to which this dns record is publishing endpoints + ZoneID string `json:"zoneID,omitempty"` + + // zoneDomainName is the domain name of the zone that the dns record is publishing endpoints + ZoneDomainName string `json:"zoneDomainName,omitempty"` } //+kubebuilder:object:root=true @@ -214,11 +209,25 @@ func (s *DNSRecord) Validate() error { return nil } +var _ ProviderAccessor = &DNSRecord{} + // GetUIDHash returns a hash of the current records UID with a fixed length of 8. func (s *DNSRecord) GetUIDHash() string { return hash.ToBase36HashLen(string(s.GetUID()), 8) } +func (s *DNSRecord) GetProviderRef() ProviderRef { + return s.Spec.ProviderRef +} + +func (s *DNSRecord) HasDNSZoneAssigned() bool { + return s.Status.ZoneID != "" && s.Status.ZoneDomainName != "" +} + +func (s *DNSRecord) HasOwnerIDAssigned() bool { + return s.Status.OwnerID != "" +} + func init() { SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{}) } diff --git a/api/v1alpha1/managedzone_types.go b/api/v1alpha1/managedzone_types.go deleted file mode 100644 index 2f9e8e4e..00000000 --- a/api/v1alpha1/managedzone_types.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024. - -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 v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ManagedZoneReference holds a reference to a ManagedZone -type ManagedZoneReference struct { - // `name` is the name of the managed zone. - // Required - Name string `json:"name"` -} - -// ManagedZoneSpec defines the desired state of ManagedZone -type ManagedZoneSpec struct { - // id is the provider assigned id of this zone (i.e. route53.HostedZone.ID). - // +optional - ID string `json:"id,omitempty"` - - //domainName of this ManagedZone - // +kubebuilder:validation:Pattern=`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` - DomainName string `json:"domainName"` - - //description for this ManagedZone - Description string `json:"description"` - - // parentManagedZone reference to another managed zone that this managed zone belongs to. - // +optional - ParentManagedZone *ManagedZoneReference `json:"parentManagedZone,omitempty"` - - // dnsProviderSecretRef reference to a secret containing credentials to access a dns provider. - SecretRef ProviderRef `json:"dnsProviderSecretRef"` -} - -// ManagedZoneStatus defines the observed state of a Zone -type ManagedZoneStatus struct { - // List of status conditions to indicate the status of a ManagedZone. - // Known condition types are `Ready`. - // +listType=map - // +listMapKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty"` - - // observedGeneration is the most recently observed generation of the ManagedZone. - // +optional - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - - // The ID assigned by this provider for this zone (i.e. route53.HostedZone.ID) - // +optional - ID string `json:"id,omitempty"` - - // The number of records in the provider zone - // +optional - RecordCount int64 `json:"recordCount,omitempty"` - - // The NameServers assigned by the provider for this zone (i.e. route53.DelegationSet.NameServers) - // +optional - NameServers []*string `json:"nameServers,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:printcolumn:name="Domain Name",type="string",JSONPath=".spec.domainName",description="Domain of this Managed Zone" -//+kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="The ID assigned by this provider for this zone ." -//+kubebuilder:printcolumn:name="Record Count",type="string",JSONPath=".status.recordCount",description="Number of records in the provider zone." -//+kubebuilder:printcolumn:name="NameServers",type="string",JSONPath=".status.nameServers",description="The NameServers assigned by the provider for this zone." -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Managed Zone ready." - -// ManagedZone is the Schema for the managedzones API -type ManagedZone struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ManagedZoneSpec `json:"spec,omitempty"` - Status ManagedZoneStatus `json:"status,omitempty"` -} - -func (mz *ManagedZone) GetProviderRef() ProviderRef { - return mz.Spec.SecretRef -} - -//+kubebuilder:object:root=true - -// ManagedZoneList contains a list of ManagedZone -type ManagedZoneList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []ManagedZone `json:"items"` -} - -type ManagedHost struct { - Subdomain string - Host string - ManagedZone *ManagedZone - DnsRecord *DNSRecord -} - -func init() { - SchemeBuilder.Register(&ManagedZone{}, &ManagedZoneList{}) -} diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index ae55f9ab..69e5587e 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -16,7 +16,53 @@ limitations under the License. package v1alpha1 +import corev1 "k8s.io/api/core/v1" + +const ( + // SecretTypeKuadrantAWS contains data needed for aws(route53) authentication and configuration. + // + // Required fields: + // - Secret.Data["AWS_ACCESS_KEY_ID"] - aws access key id + // - Secret.Data["AWS_SECRET_ACCESS_KEY"] - aws secret access key + SecretTypeKuadrantAWS corev1.SecretType = "kuadrant.io/aws" + + // AWSAccessKeyIDKey is the key of the required AWS access key id for SecretTypeKuadrantAWS provider secrets + AWSAccessKeyIDKey = "AWS_ACCESS_KEY_ID" + // AWSSecretAccessKeyKey is the key of the required AWS secret access key for SecretTypeKuadrantAWS provider secrets + AWSSecretAccessKeyKey = "AWS_SECRET_ACCESS_KEY" + // AWSRegionKey is the key of the optional region for SecretTypeKuadrantAWS provider secrets + AWSRegionKey = "AWS_REGION" + + // SecretTypeKuadrantGCP contains data needed for gcp(google cloud dns) authentication and configuration. + // + // Required fields: + // - Secret.Data["GOOGLE"] - json formatted google credentials string + // - Secret.Data["PROJECT_ID"] - google project id + SecretTypeKuadrantGCP corev1.SecretType = "kuadrant.io/gcp" + + // GoogleJsonKey is the key of the required json formatted credentials string for SecretTypeKuadrantGCP provider secrets + GoogleJsonKey = "GOOGLE" + // GoogleProjectIDKey is the key of the required project id for SecretTypeKuadrantGCP provider secrets + GoogleProjectIDKey = "PROJECT_ID" + + // SecretTypeKuadrantAzure contains data needed for azure authentication and configuration. + // + // Required fields: + // - Secret.Data["azure.json"] - json formatted azure credentials string + SecretTypeKuadrantAzure corev1.SecretType = "kuadrant.io/azure" + + // AzureJsonKey is the key of the required data for SecretTypeDockerConfigJson provider secrets + AzureJsonKey = "azure.json" + + // SecretTypeKuadrantInmemory contains data needed for inmemory configuration. + SecretTypeKuadrantInmemory corev1.SecretType = "kuadrant.io/inmemory" + + // InmemInitZonesKey is the key of the optional comma separated list of zone names to initialise in the SecretTypeKuadrantInmemory provider secrets + InmemInitZonesKey = "INMEM_INIT_ZONES" +) + type ProviderRef struct { + // +kubebuilder:validation:MinLength=1 Name string `json:"name"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ce59068a..40615d16 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -88,11 +88,7 @@ func (in *DNSRecordList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordSpec) DeepCopyInto(out *DNSRecordSpec) { *out = *in - if in.ManagedZoneRef != nil { - in, out := &in.ManagedZoneRef, &out.ManagedZoneRef - *out = new(ManagedZoneReference) - **out = **in - } + out.ProviderRef = in.ProviderRef if in.Endpoints != nil { in, out := &in.Endpoints, &out.Endpoints *out = make([]*endpoint.Endpoint, len(*in)) @@ -247,159 +243,6 @@ func (in *HealthCheckStatusProbe) DeepCopy() *HealthCheckStatusProbe { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedHost) DeepCopyInto(out *ManagedHost) { - *out = *in - if in.ManagedZone != nil { - in, out := &in.ManagedZone, &out.ManagedZone - *out = new(ManagedZone) - (*in).DeepCopyInto(*out) - } - if in.DnsRecord != nil { - in, out := &in.DnsRecord, &out.DnsRecord - *out = new(DNSRecord) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedHost. -func (in *ManagedHost) DeepCopy() *ManagedHost { - if in == nil { - return nil - } - out := new(ManagedHost) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedZone) DeepCopyInto(out *ManagedZone) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZone. -func (in *ManagedZone) DeepCopy() *ManagedZone { - if in == nil { - return nil - } - out := new(ManagedZone) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ManagedZone) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedZoneList) DeepCopyInto(out *ManagedZoneList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ManagedZone, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneList. -func (in *ManagedZoneList) DeepCopy() *ManagedZoneList { - if in == nil { - return nil - } - out := new(ManagedZoneList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ManagedZoneList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedZoneReference) DeepCopyInto(out *ManagedZoneReference) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneReference. -func (in *ManagedZoneReference) DeepCopy() *ManagedZoneReference { - if in == nil { - return nil - } - out := new(ManagedZoneReference) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedZoneSpec) DeepCopyInto(out *ManagedZoneSpec) { - *out = *in - if in.ParentManagedZone != nil { - in, out := &in.ParentManagedZone, &out.ParentManagedZone - *out = new(ManagedZoneReference) - **out = **in - } - out.SecretRef = in.SecretRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneSpec. -func (in *ManagedZoneSpec) DeepCopy() *ManagedZoneSpec { - if in == nil { - return nil - } - out := new(ManagedZoneSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagedZoneStatus) DeepCopyInto(out *ManagedZoneStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.NameServers != nil { - in, out := &in.NameServers, &out.NameServers - *out = make([]*string, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(string) - **out = **in - } - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneStatus. -func (in *ManagedZoneStatus) DeepCopy() *ManagedZoneStatus { - if in == nil { - return nil - } - out := new(ManagedZoneStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderRef) DeepCopyInto(out *ProviderRef) { *out = *in diff --git a/bundle/manifests/dns-operator.clusterserviceversion.yaml b/bundle/manifests/dns-operator.clusterserviceversion.yaml index e35ab174..51e201ae 100644 --- a/bundle/manifests/dns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/dns-operator.clusterserviceversion.yaml @@ -29,34 +29,16 @@ metadata: ] } ], - "managedZone": { - "name": "managedzone-sample" + "providerRef": { + "name": "dns-provider-creds" } } - }, - { - "apiVersion": "kuadrant.io/v1alpha1", - "kind": "ManagedZone", - "metadata": { - "labels": { - "app.kubernetes.io/created-by": "dns-operator", - "app.kubernetes.io/instance": "managedzone-sample", - "app.kubernetes.io/managed-by": "kustomize", - "app.kubernetes.io/name": "managedzone", - "app.kubernetes.io/part-of": "dns-operator" - }, - "name": "managedzone-sample" - }, - "spec": { - "description": "My managed domain", - "domainName": "example.com" - } } ] capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/dns-operator:latest - createdAt: "2024-08-12T10:16:02Z" + createdAt: "2024-08-13T17:16:20Z" description: A Kubernetes Operator to manage the lifecycle of DNS resources operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 @@ -73,11 +55,6 @@ spec: kind: DNSRecord name: dnsrecords.kuadrant.io version: v1alpha1 - - description: ManagedZone is the Schema for the managedzones API - displayName: Managed Zone - kind: ManagedZone - name: managedzones.kuadrant.io - version: v1alpha1 description: A Kubernetes Operator to manage the lifecycle of DNS resources displayName: DNS Operator icon: @@ -121,32 +98,6 @@ spec: - get - patch - update - - apiGroups: - - kuadrant.io - resources: - - managedzones - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - kuadrant.io - resources: - - managedzones/finalizers - verbs: - - update - - apiGroups: - - kuadrant.io - resources: - - managedzones/status - verbs: - - get - - patch - - update serviceAccountName: dns-operator-controller-manager deployments: - label: diff --git a/bundle/manifests/kuadrant.io_dnsrecords.yaml b/bundle/manifests/kuadrant.io_dnsrecords.yaml index cf56529e..d925b242 100644 --- a/bundle/manifests/kuadrant.io_dnsrecords.yaml +++ b/bundle/manifests/kuadrant.io_dnsrecords.yaml @@ -126,18 +126,6 @@ spec: - message: Only HTTP or HTTPS protocols are allowed rule: self in ['HTTP','HTTPS'] type: object - managedZone: - description: managedZone is a reference to a ManagedZone instance - to which this record will publish its endpoints. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object ownerID: description: |- ownerID is a unique string used to identify the owner of this record. @@ -148,6 +136,15 @@ spec: x-kubernetes-validations: - message: OwnerID is immutable rule: self == oldSelf + providerRef: + description: providerRef is a reference to a provider secret. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object rootHost: description: |- rootHost is the single root for all endpoints in a DNSRecord. @@ -158,7 +155,7 @@ spec: pattern: ^(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)\.(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)$ type: string required: - - managedZone + - providerRef - rootHost type: object x-kubernetes-validations: @@ -171,7 +168,7 @@ spec: properties: conditions: description: |- - conditions are any conditions associated with the record in the managed zone. + conditions are any conditions associated with the record in the dns provider. If publishing the record fails, the "Failed" condition will be set with a @@ -251,16 +248,8 @@ spec: type: string type: array endpoints: - description: |- - endpoints are the last endpoints that were successfully published by the provider - - - Provides a simple mechanism to store the current provider records in order to - delete any that are no longer present in DNSRecordSpec.Endpoints - - - Note: This will not be required if/when we switch to using external-dns since when - running with a "sync" policy it will clean up unused records automatically. + description: endpoints are the last endpoints that were successfully + published to the provider zone items: description: Endpoint is a high-level way of a connection between a service and an IP @@ -469,13 +458,8 @@ spec: type: array type: object observedGeneration: - description: |- - observedGeneration is the most recently observed generation of the - DNSRecord. When the DNSRecord is updated, the controller updates the - corresponding record in each managed zone. If an update for a - particular zone fails, that failure is recorded in the status - condition for the zone so that the controller can determine that it - needs to retry the update for that specific zone. + description: observedGeneration is the most recently observed generation + of the DNSRecord. format: int64 type: integer ownerID: @@ -502,6 +486,14 @@ spec: It is being reset to 0 when the generation changes or there are no changes to write. format: int64 type: integer + zoneDomainName: + description: zoneDomainName is the domain name of the zone that the + dns record is publishing endpoints + type: string + zoneID: + description: zoneID is the provider specific id to which this dns + record is publishing endpoints + type: string type: object type: object served: true diff --git a/bundle/manifests/kuadrant.io_managedzones.yaml b/bundle/manifests/kuadrant.io_managedzones.yaml deleted file mode 100644 index f1f809b0..00000000 --- a/bundle/manifests/kuadrant.io_managedzones.yaml +++ /dev/null @@ -1,207 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - creationTimestamp: null - name: managedzones.kuadrant.io -spec: - group: kuadrant.io - names: - kind: ManagedZone - listKind: ManagedZoneList - plural: managedzones - singular: managedzone - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: Domain of this Managed Zone - jsonPath: .spec.domainName - name: Domain Name - type: string - - description: The ID assigned by this provider for this zone . - jsonPath: .status.id - name: ID - type: string - - description: Number of records in the provider zone. - jsonPath: .status.recordCount - name: Record Count - type: string - - description: The NameServers assigned by the provider for this zone. - jsonPath: .status.nameServers - name: NameServers - type: string - - description: Managed Zone ready. - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: ManagedZone is the Schema for the managedzones API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ManagedZoneSpec defines the desired state of ManagedZone - properties: - description: - description: description for this ManagedZone - type: string - dnsProviderSecretRef: - description: dnsProviderSecretRef reference to a secret containing - credentials to access a dns provider. - properties: - name: - type: string - required: - - name - type: object - domainName: - description: domainName of this ManagedZone - pattern: ^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$ - type: string - id: - description: id is the provider assigned id of this zone (i.e. route53.HostedZone.ID). - type: string - parentManagedZone: - description: parentManagedZone reference to another managed zone that - this managed zone belongs to. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object - required: - - description - - dnsProviderSecretRef - - domainName - type: object - status: - description: ManagedZoneStatus defines the observed state of a Zone - properties: - conditions: - description: |- - List of status conditions to indicate the status of a ManagedZone. - Known condition types are `Ready`. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - id: - description: The ID assigned by this provider for this zone (i.e. - route53.HostedZone.ID) - type: string - nameServers: - description: The NameServers assigned by the provider for this zone - (i.e. route53.DelegationSet.NameServers) - items: - type: string - type: array - observedGeneration: - description: observedGeneration is the most recently observed generation - of the ManagedZone. - format: int64 - type: integer - recordCount: - description: The number of records in the provider zone - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null diff --git a/charts/dns-operator/Chart.yaml b/charts/dns-operator/Chart.yaml index a9ffaa46..ad427da5 100644 --- a/charts/dns-operator/Chart.yaml +++ b/charts/dns-operator/Chart.yaml @@ -1,11 +1,10 @@ apiVersion: v2 name: dns-operator -description: Kubernetes operator responsible for reconciling DNS Record and Managed Zone custom resources. +description: Kubernetes operator responsible for reconciling DNS Record custom resources. home: https://kuadrant.io icon: https://raw.githubusercontent.com/Kuadrant/kuadrant.github.io/main/static/img/apple-touch-icon.png keywords: - dns - - managed zone - kubernetes - kuadrant sources: diff --git a/charts/dns-operator/templates/manifests.yaml b/charts/dns-operator/templates/manifests.yaml index ae11c4d6..c7618696 100644 --- a/charts/dns-operator/templates/manifests.yaml +++ b/charts/dns-operator/templates/manifests.yaml @@ -138,18 +138,6 @@ spec: - message: Only HTTP or HTTPS protocols are allowed rule: self in ['HTTP','HTTPS'] type: object - managedZone: - description: managedZone is a reference to a ManagedZone instance - to which this record will publish its endpoints. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object ownerID: description: |- ownerID is a unique string used to identify the owner of this record. @@ -160,6 +148,14 @@ spec: x-kubernetes-validations: - message: OwnerID is immutable rule: self == oldSelf + providerRef: + description: providerRef is a reference to a provider secret. + properties: + name: + type: string + required: + - name + type: object rootHost: description: |- rootHost is the single root for all endpoints in a DNSRecord. @@ -170,7 +166,7 @@ spec: pattern: ^(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)\.(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)$ type: string required: - - managedZone + - providerRef - rootHost type: object x-kubernetes-validations: @@ -183,7 +179,7 @@ spec: properties: conditions: description: |- - conditions are any conditions associated with the record in the managed zone. + conditions are any conditions associated with the record in the dns provider. If publishing the record fails, the "Failed" condition will be set with a @@ -475,13 +471,8 @@ spec: type: array type: object observedGeneration: - description: |- - observedGeneration is the most recently observed generation of the - DNSRecord. When the DNSRecord is updated, the controller updates the - corresponding record in each managed zone. If an update for a - particular zone fails, that failure is recorded in the status - condition for the zone so that the controller can determine that it - needs to retry the update for that specific zone. + description: observedGeneration is the most recently observed generation + of the DNSRecord. format: int64 type: integer ownerID: @@ -508,207 +499,14 @@ spec: It is being reset to 0 when the generation changes or there are no changes to write. format: int64 type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: managedzones.kuadrant.io -spec: - group: kuadrant.io - names: - kind: ManagedZone - listKind: ManagedZoneList - plural: managedzones - singular: managedzone - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: Domain of this Managed Zone - jsonPath: .spec.domainName - name: Domain Name - type: string - - description: The ID assigned by this provider for this zone . - jsonPath: .status.id - name: ID - type: string - - description: Number of records in the provider zone. - jsonPath: .status.recordCount - name: Record Count - type: string - - description: The NameServers assigned by the provider for this zone. - jsonPath: .status.nameServers - name: NameServers - type: string - - description: Managed Zone ready. - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: ManagedZone is the Schema for the managedzones API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ManagedZoneSpec defines the desired state of ManagedZone - properties: - description: - description: description for this ManagedZone - type: string - dnsProviderSecretRef: - description: dnsProviderSecretRef reference to a secret containing - credentials to access a dns provider. - properties: - name: - type: string - required: - - name - type: object - domainName: - description: domainName of this ManagedZone - pattern: ^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$ + zoneDomainName: + description: zoneDomainName is the domain name of the zone that the + dns record is publishing endpoints type: string - id: - description: id is the provider assigned id of this zone (i.e. route53.HostedZone.ID). + zoneID: + description: zoneID is the provider specific id to which this dns + record is publishing endpoints type: string - parentManagedZone: - description: parentManagedZone reference to another managed zone that - this managed zone belongs to. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object - required: - - description - - dnsProviderSecretRef - - domainName - type: object - status: - description: ManagedZoneStatus defines the observed state of a Zone - properties: - conditions: - description: |- - List of status conditions to indicate the status of a ManagedZone. - Known condition types are `Ready`. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - id: - description: The ID assigned by this provider for this zone (i.e. - route53.HostedZone.ID) - type: string - nameServers: - description: The NameServers assigned by the provider for this zone - (i.e. route53.DelegationSet.NameServers) - items: - type: string - type: array - observedGeneration: - description: observedGeneration is the most recently observed generation - of the ManagedZone. - format: int64 - type: integer - recordCount: - description: The number of records in the provider zone - format: int64 - type: integer type: object type: object served: true @@ -813,32 +611,6 @@ rules: - get - patch - update -- apiGroups: - - kuadrant.io - resources: - - managedzones - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kuadrant.io - resources: - - managedzones/finalizers - verbs: - - update -- apiGroups: - - kuadrant.io - resources: - - managedzones/status - verbs: - - get - - patch - - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/cmd/main.go b/cmd/main.go index de97e9ef..1e4c7a92 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -136,14 +136,6 @@ func main() { os.Exit(1) } - if err = (&controller.ManagedZoneReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ProviderFactory: providerFactory, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "ManagedZone") - os.Exit(1) - } if err = (&controller.DNSRecordReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/config/crd/bases/kuadrant.io_dnsrecords.yaml b/config/crd/bases/kuadrant.io_dnsrecords.yaml index 75ee6e9d..7485fa79 100644 --- a/config/crd/bases/kuadrant.io_dnsrecords.yaml +++ b/config/crd/bases/kuadrant.io_dnsrecords.yaml @@ -126,18 +126,6 @@ spec: - message: Only HTTP or HTTPS protocols are allowed rule: self in ['HTTP','HTTPS'] type: object - managedZone: - description: managedZone is a reference to a ManagedZone instance - to which this record will publish its endpoints. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object ownerID: description: |- ownerID is a unique string used to identify the owner of this record. @@ -148,6 +136,15 @@ spec: x-kubernetes-validations: - message: OwnerID is immutable rule: self == oldSelf + providerRef: + description: providerRef is a reference to a provider secret. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object rootHost: description: |- rootHost is the single root for all endpoints in a DNSRecord. @@ -158,7 +155,7 @@ spec: pattern: ^(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)\.(?:[\w\-.~:\/?#[\]@!$&'()*+,;=]+)$ type: string required: - - managedZone + - providerRef - rootHost type: object x-kubernetes-validations: @@ -171,7 +168,7 @@ spec: properties: conditions: description: |- - conditions are any conditions associated with the record in the managed zone. + conditions are any conditions associated with the record in the dns provider. If publishing the record fails, the "Failed" condition will be set with a @@ -251,16 +248,8 @@ spec: type: string type: array endpoints: - description: |- - endpoints are the last endpoints that were successfully published by the provider - - - Provides a simple mechanism to store the current provider records in order to - delete any that are no longer present in DNSRecordSpec.Endpoints - - - Note: This will not be required if/when we switch to using external-dns since when - running with a "sync" policy it will clean up unused records automatically. + description: endpoints are the last endpoints that were successfully + published to the provider zone items: description: Endpoint is a high-level way of a connection between a service and an IP @@ -469,13 +458,8 @@ spec: type: array type: object observedGeneration: - description: |- - observedGeneration is the most recently observed generation of the - DNSRecord. When the DNSRecord is updated, the controller updates the - corresponding record in each managed zone. If an update for a - particular zone fails, that failure is recorded in the status - condition for the zone so that the controller can determine that it - needs to retry the update for that specific zone. + description: observedGeneration is the most recently observed generation + of the DNSRecord. format: int64 type: integer ownerID: @@ -502,6 +486,14 @@ spec: It is being reset to 0 when the generation changes or there are no changes to write. format: int64 type: integer + zoneDomainName: + description: zoneDomainName is the domain name of the zone that the + dns record is publishing endpoints + type: string + zoneID: + description: zoneID is the provider specific id to which this dns + record is publishing endpoints + type: string type: object type: object served: true diff --git a/config/crd/bases/kuadrant.io_managedzones.yaml b/config/crd/bases/kuadrant.io_managedzones.yaml deleted file mode 100644 index 760400dc..00000000 --- a/config/crd/bases/kuadrant.io_managedzones.yaml +++ /dev/null @@ -1,201 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: managedzones.kuadrant.io -spec: - group: kuadrant.io - names: - kind: ManagedZone - listKind: ManagedZoneList - plural: managedzones - singular: managedzone - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: Domain of this Managed Zone - jsonPath: .spec.domainName - name: Domain Name - type: string - - description: The ID assigned by this provider for this zone . - jsonPath: .status.id - name: ID - type: string - - description: Number of records in the provider zone. - jsonPath: .status.recordCount - name: Record Count - type: string - - description: The NameServers assigned by the provider for this zone. - jsonPath: .status.nameServers - name: NameServers - type: string - - description: Managed Zone ready. - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: ManagedZone is the Schema for the managedzones API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ManagedZoneSpec defines the desired state of ManagedZone - properties: - description: - description: description for this ManagedZone - type: string - dnsProviderSecretRef: - description: dnsProviderSecretRef reference to a secret containing - credentials to access a dns provider. - properties: - name: - type: string - required: - - name - type: object - domainName: - description: domainName of this ManagedZone - pattern: ^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$ - type: string - id: - description: id is the provider assigned id of this zone (i.e. route53.HostedZone.ID). - type: string - parentManagedZone: - description: parentManagedZone reference to another managed zone that - this managed zone belongs to. - properties: - name: - description: |- - `name` is the name of the managed zone. - Required - type: string - required: - - name - type: object - required: - - description - - dnsProviderSecretRef - - domainName - type: object - status: - description: ManagedZoneStatus defines the observed state of a Zone - properties: - conditions: - description: |- - List of status conditions to indicate the status of a ManagedZone. - Known condition types are `Ready`. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - id: - description: The ID assigned by this provider for this zone (i.e. - route53.HostedZone.ID) - type: string - nameServers: - description: The NameServers assigned by the provider for this zone - (i.e. route53.DelegationSet.NameServers) - items: - type: string - type: array - observedGeneration: - description: observedGeneration is the most recently observed generation - of the ManagedZone. - format: int64 - type: integer - recordCount: - description: The number of records in the provider zone - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a9ea4a8b..65405b68 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -2,20 +2,17 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: -- bases/kuadrant.io_managedzones.yaml - bases/kuadrant.io_dnsrecords.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -#- path: patches/webhook_in_managedzones.yaml #- path: patches/webhook_in_dnsrecords.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -#- path: patches/cainjection_in_managedzones.yaml #- path: patches/cainjection_in_dnsrecords.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch diff --git a/config/crd/patches/cainjection_in_managedzones.yaml b/config/crd/patches/cainjection_in_managedzones.yaml deleted file mode 100644 index f55dd30c..00000000 --- a/config/crd/patches/cainjection_in_managedzones.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME - name: managedzones.kuadrant.io diff --git a/config/crd/patches/webhook_in_managedzones.yaml b/config/crd/patches/webhook_in_managedzones.yaml deleted file mode 100644 index 63f66e4f..00000000 --- a/config/crd/patches/webhook_in_managedzones.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: managedzones.kuadrant.io -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/local-setup/dns-operator/kustomization.yaml b/config/local-setup/dns-operator/kustomization.yaml index 4bbeeceb..f2109537 100644 --- a/config/local-setup/dns-operator/kustomization.yaml +++ b/config/local-setup/dns-operator/kustomization.yaml @@ -8,9 +8,3 @@ patches: kind: CustomResourceDefinition metadata: name: dnsrecords.kuadrant.io - - patch: |- - $patch: delete - apiVersion: apiextensions.k8s.io/v1 - kind: CustomResourceDefinition - metadata: - name: managedzones.kuadrant.io diff --git a/config/local-setup/managedzone/aws/aws-credentials.env.template b/config/local-setup/dns-provider/aws/aws-credentials.env.template similarity index 100% rename from config/local-setup/managedzone/aws/aws-credentials.env.template rename to config/local-setup/dns-provider/aws/aws-credentials.env.template diff --git a/config/local-setup/dns-provider/aws/kustomization.yaml b/config/local-setup/dns-provider/aws/kustomization.yaml new file mode 100644 index 00000000..a0e79e22 --- /dev/null +++ b/config/local-setup/dns-provider/aws/kustomization.yaml @@ -0,0 +1,13 @@ +nameSuffix: -aws + +generatorOptions: + disableNameSuffixHash: true + labels: + app.kubernetes.io/part-of: dns-operator + app.kubernetes.io/managed-by: kustomize + +secretGenerator: + - name: dns-provider-credentials + envs: + - aws-credentials.env + type: "kuadrant.io/aws" diff --git a/config/local-setup/managedzone/azure/azure-credentials.env.template b/config/local-setup/dns-provider/azure/azure-credentials.env.template similarity index 100% rename from config/local-setup/managedzone/azure/azure-credentials.env.template rename to config/local-setup/dns-provider/azure/azure-credentials.env.template diff --git a/config/local-setup/dns-provider/azure/kustomization.yaml b/config/local-setup/dns-provider/azure/kustomization.yaml new file mode 100644 index 00000000..2c5fb18e --- /dev/null +++ b/config/local-setup/dns-provider/azure/kustomization.yaml @@ -0,0 +1,13 @@ +nameSuffix: -azure + +generatorOptions: + disableNameSuffixHash: true + labels: + app.kubernetes.io/part-of: dns-operator + app.kubernetes.io/managed-by: kustomize + +secretGenerator: + - name: dns-provider-credentials + envs: + - azure-credentials.env + type: "kuadrant.io/azure" diff --git a/config/local-setup/managedzone/gcp/gcp-credentials.env.template b/config/local-setup/dns-provider/gcp/gcp-credentials.env.template similarity index 100% rename from config/local-setup/managedzone/gcp/gcp-credentials.env.template rename to config/local-setup/dns-provider/gcp/gcp-credentials.env.template diff --git a/config/local-setup/dns-provider/gcp/kustomization.yaml b/config/local-setup/dns-provider/gcp/kustomization.yaml new file mode 100644 index 00000000..0f7504a9 --- /dev/null +++ b/config/local-setup/dns-provider/gcp/kustomization.yaml @@ -0,0 +1,13 @@ +nameSuffix: -gcp + +generatorOptions: + disableNameSuffixHash: true + labels: + app.kubernetes.io/part-of: dns-operator + app.kubernetes.io/managed-by: kustomize + +secretGenerator: + - name: dns-provider-credentials + envs: + - gcp-credentials.env + type: "kuadrant.io/gcp" diff --git a/config/local-setup/dns-provider/inmemory/kustomization.yaml b/config/local-setup/dns-provider/inmemory/kustomization.yaml new file mode 100644 index 00000000..ab0ff244 --- /dev/null +++ b/config/local-setup/dns-provider/inmemory/kustomization.yaml @@ -0,0 +1,13 @@ +nameSuffix: -inmemory + +generatorOptions: + disableNameSuffixHash: true + labels: + app.kubernetes.io/part-of: dns-operator + app.kubernetes.io/managed-by: kustomize + +secretGenerator: + - name: dns-provider-credentials + type: "kuadrant.io/inmemory" + literals: + - INMEM_INIT_ZONES=kuadrant.local diff --git a/config/local-setup/managedzone/aws/kustomization.yaml b/config/local-setup/managedzone/aws/kustomization.yaml deleted file mode 100644 index 535ebe51..00000000 --- a/config/local-setup/managedzone/aws/kustomization.yaml +++ /dev/null @@ -1,45 +0,0 @@ -resources: - - ../base - -nameSuffix: -aws - -generatorOptions: - disableNameSuffixHash: true - -configMapGenerator: - - name: managed-zone-config - envs: - - managed-zone-config.env - options: - annotations: - config.kubernetes.io/local-config: "true" - -secretGenerator: - - name: dns-provider-credentials - envs: - - aws-credentials.env - type: "kuadrant.io/aws" - -replacements: - - source: - kind: ConfigMap - name: managed-zone-config - version: v1 - fieldPath: data.AWS_DNS_PUBLIC_ZONE_ID - targets: - - select: - kind: ManagedZone - name: dev-mz-aws - fieldPaths: - - spec.id - - source: - kind: ConfigMap - name: managed-zone-config - version: v1 - fieldPath: data.AWS_ZONE_ROOT_DOMAIN - targets: - - select: - kind: ManagedZone - name: dev-mz-aws - fieldPaths: - - spec.domainName diff --git a/config/local-setup/managedzone/aws/managed-zone-config.env.template b/config/local-setup/managedzone/aws/managed-zone-config.env.template deleted file mode 100644 index 9e154b85..00000000 --- a/config/local-setup/managedzone/aws/managed-zone-config.env.template +++ /dev/null @@ -1,2 +0,0 @@ -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/azure/kustomization.yaml b/config/local-setup/managedzone/azure/kustomization.yaml deleted file mode 100644 index f13246df..00000000 --- a/config/local-setup/managedzone/azure/kustomization.yaml +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 8c1be956..00000000 --- a/config/local-setup/managedzone/azure/managed-zone-config.env.template +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index a8ececc1..00000000 --- a/config/local-setup/managedzone/azure/managed_zone.yaml +++ /dev/null @@ -1,10 +0,0 @@ -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/config/local-setup/managedzone/base/kustomization.yaml b/config/local-setup/managedzone/base/kustomization.yaml deleted file mode 100644 index d7266648..00000000 --- a/config/local-setup/managedzone/base/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -resources: - - managed_zone.yaml - -configurations: - - name-reference.yaml diff --git a/config/local-setup/managedzone/base/managed_zone.yaml b/config/local-setup/managedzone/base/managed_zone.yaml deleted file mode 100644 index b9b90528..00000000 --- a/config/local-setup/managedzone/base/managed_zone.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: kuadrant.io/v1alpha1 -kind: ManagedZone -metadata: - name: dev-mz -spec: - id: DUMMY_ID - domainName: DUMMY_DOMAIN_NAME - description: "Dev Managed Zone" - dnsProviderSecretRef: - name: dns-provider-credentials diff --git a/config/local-setup/managedzone/base/name-reference.yaml b/config/local-setup/managedzone/base/name-reference.yaml deleted file mode 100644 index 96625a1b..00000000 --- a/config/local-setup/managedzone/base/name-reference.yaml +++ /dev/null @@ -1,5 +0,0 @@ -nameReference: - - kind: Secret - fieldSpecs: - - kind: ManagedZone - path: spec/dnsProviderSecretRef/name diff --git a/config/local-setup/managedzone/gcp/kustomization.yaml b/config/local-setup/managedzone/gcp/kustomization.yaml deleted file mode 100644 index a05caaa6..00000000 --- a/config/local-setup/managedzone/gcp/kustomization.yaml +++ /dev/null @@ -1,45 +0,0 @@ -resources: - - ../base - -nameSuffix: -gcp - -generatorOptions: - disableNameSuffixHash: true - -configMapGenerator: - - name: managed-zone-config - envs: - - managed-zone-config.env - options: - annotations: - config.kubernetes.io/local-config: "true" - -secretGenerator: - - name: dns-provider-credentials - envs: - - gcp-credentials.env - type: "kuadrant.io/gcp" - -replacements: - - source: - kind: ConfigMap - name: managed-zone-config - version: v1 - fieldPath: data.GCP_ZONE_NAME - targets: - - select: - kind: ManagedZone - name: dev-mz-gcp - fieldPaths: - - spec.id - - source: - kind: ConfigMap - name: managed-zone-config - version: v1 - fieldPath: data.GCP_ZONE_DNS_NAME - targets: - - select: - kind: ManagedZone - name: dev-mz-gcp - fieldPaths: - - spec.domainName diff --git a/config/local-setup/managedzone/gcp/managed-zone-config.env.template b/config/local-setup/managedzone/gcp/managed-zone-config.env.template deleted file mode 100644 index 63576e1c..00000000 --- a/config/local-setup/managedzone/gcp/managed-zone-config.env.template +++ /dev/null @@ -1,2 +0,0 @@ -GCP_ZONE_NAME=${GCP_ZONE_NAME} -GCP_ZONE_DNS_NAME=${GCP_ZONE_DNS_NAME} diff --git a/config/local-setup/managedzone/inmemory/kustomization.yaml b/config/local-setup/managedzone/inmemory/kustomization.yaml deleted file mode 100644 index af4f8c49..00000000 --- a/config/local-setup/managedzone/inmemory/kustomization.yaml +++ /dev/null @@ -1,24 +0,0 @@ -resources: - - ../base - -nameSuffix: -inmemory - -generatorOptions: - disableNameSuffixHash: true - -secretGenerator: - - name: dns-provider-credentials - type: "kuadrant.io/inmemory" - -patches: - - patch: |- - - op: remove - path: /spec/id - target: - kind: ManagedZone - - patch: |- - - op: replace - path: /spec/domainName - value: dev.kuadrant.local - target: - kind: ManagedZone diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index a2d3dac0..124ab889 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -7,4 +7,6 @@ kind: Kustomization images: - name: controller newName: quay.io/kuadrant/dns-operator - newTag: latest + newTag: remove_managed_zone_api + +#ToDo mnairn: Revert tag before merge diff --git a/config/manifests/bases/dns-operator.clusterserviceversion.yaml b/config/manifests/bases/dns-operator.clusterserviceversion.yaml index 2c8461a7..5a826fe9 100644 --- a/config/manifests/bases/dns-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/dns-operator.clusterserviceversion.yaml @@ -20,11 +20,6 @@ spec: kind: DNSRecord name: dnsrecords.kuadrant.io version: v1alpha1 - - description: ManagedZone is the Schema for the managedzones API - displayName: Managed Zone - kind: ManagedZone - name: managedzones.kuadrant.io - version: v1alpha1 description: A Kubernetes Operator to manage the lifecycle of DNS resources displayName: DNS Operator icon: diff --git a/config/rbac/managedzone_editor_role.yaml b/config/rbac/managedzone_editor_role.yaml deleted file mode 100644 index 1c646f1c..00000000 --- a/config/rbac/managedzone_editor_role.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# permissions for end users to edit managedzones. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: managedzone-editor-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: dns-operator - app.kubernetes.io/part-of: dns-operator - app.kubernetes.io/managed-by: kustomize - name: managedzone-editor-role -rules: -- apiGroups: - - kuadrant.io - resources: - - managedzones - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kuadrant.io - resources: - - managedzones/status - verbs: - - get diff --git a/config/rbac/managedzone_viewer_role.yaml b/config/rbac/managedzone_viewer_role.yaml deleted file mode 100644 index 7cce228c..00000000 --- a/config/rbac/managedzone_viewer_role.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# permissions for end users to view managedzones. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: managedzone-viewer-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: dns-operator - app.kubernetes.io/part-of: dns-operator - app.kubernetes.io/managed-by: kustomize - name: managedzone-viewer-role -rules: -- apiGroups: - - kuadrant.io - resources: - - managedzones - verbs: - - get - - list - - watch -- apiGroups: - - kuadrant.io - resources: - - managedzones/status - verbs: - - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1653add9..4c671df5 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -38,29 +38,3 @@ rules: - get - patch - update -- apiGroups: - - kuadrant.io - resources: - - managedzones - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kuadrant.io - resources: - - managedzones/finalizers - verbs: - - update -- apiGroups: - - kuadrant.io - resources: - - managedzones/status - verbs: - - get - - patch - - update diff --git a/config/samples/kuadrant.io_v1alpha1_dnsrecord.yaml b/config/samples/kuadrant.io_v1alpha1_dnsrecord.yaml index 049264b7..588aecff 100644 --- a/config/samples/kuadrant.io_v1alpha1_dnsrecord.yaml +++ b/config/samples/kuadrant.io_v1alpha1_dnsrecord.yaml @@ -9,8 +9,8 @@ metadata: app.kubernetes.io/created-by: dns-operator name: dnsrecord-sample spec: - managedZone: - name: managedzone-sample + providerRef: + name: dns-provider-creds endpoints: - dnsName: dnsrecord-simple.example.com recordTTL: 60 diff --git a/config/samples/kuadrant.io_v1alpha1_managedzone.yaml b/config/samples/kuadrant.io_v1alpha1_managedzone.yaml deleted file mode 100644 index 59dccb7c..00000000 --- a/config/samples/kuadrant.io_v1alpha1_managedzone.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kuadrant.io/v1alpha1 -kind: ManagedZone -metadata: - labels: - app.kubernetes.io/name: managedzone - app.kubernetes.io/instance: managedzone-sample - app.kubernetes.io/part-of: dns-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: dns-operator - name: managedzone-sample -spec: - domainName: example.com - description: "My managed domain" diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 7bf6c2a0..c65ac237 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,5 +1,4 @@ ## Append samples of your project ## resources: -- kuadrant.io_v1alpha1_managedzone.yaml - kuadrant.io_v1alpha1_dnsrecord.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/docs/managedzone.md b/docs/managedzone.md deleted file mode 100644 index 9d965463..00000000 --- a/docs/managedzone.md +++ /dev/null @@ -1,86 +0,0 @@ -# Creating and using a ManagedZone resource. - -## What is a ManagedZone -A ManagedZone is a reference to a [DNS zone](https://en.wikipedia.org/wiki/DNS_zone). -By creating a ManagedZone we are instructing the MGC about a domain or subdomain that can be used as a host by any gateways in the same namespace. -These gateways can use a subdomain of the ManagedZone. - -If a gateway attempts to a use a domain as a host, and there is no matching ManagedZone for that host, then that host on that gateway will fail to function. - -A gateway's host will be matched to any ManagedZone that the host is a subdomain of, i.e. `test.api.hcpapps.net` will be matched by any ManagedZone (in the same namespace) of: `test.api.hcpapps.net`, `api.hcpapps.net` or `hcpapps.net`. - -When MGC wants to create the DNS Records for a host, it will create them in the most exactly matching ManagedZone. -e.g. given the zones `hcpapps.net` and `api.hcpapps.net` the DNS Records for the host `test.api.hcpapps.net` will be created in the `api.hcpapps.net` zone. - -### Private and Public Zones - -Some DNS providers offer private zones. While this is something we will want to support in the future, we currently only support public zones. - -### Delegation -Delegation allows you to give control of a subdomain of a root domain to MGC while the root domain has it's DNS zone elsewhere. - -In the scenario where a root domain has a zone outside Route53, e.g. `external.com`, and a ManagedZone for `delegated.external.com` is required, the following steps can be taken: -- Create the ManagedZone for `delegated.external.com` and wait until the status is updated with an array of nameservers (e.g. `ns1.hcpapps.net`, `ns2.hcpapps.net`). -- Copy these nameservers to your root zone for `external.com`, you can create a NS record for each nameserver against the `delegated.external.com` record. - -For example: -``` -delegated.external.com. 3600 IN NS ns1.hcpapps.net. -delegated.external.com. 3600 IN NS ns2.hcpapps.net. -``` - -Now, when MGC creates a DNS record in it's Route53 zone for `delegated.external.com`, it will be resolved correctly. -### Creating a ManagedZone - -To create a `ManagedZone`, you will first need to create a DNS provider Secret. To create one, see our [DNS Provider](provider.md) setup guide, and make note of your provider's secret name. - - -#### Example ManagedZone -To create a new `ManagedZone` with AWS Route, with a DNS Provider secret named `my-aws-credentials`: - -```bash -kubectl apply -f - <= lowerLimmit && float64(randomizedDuration.Milliseconds()) < upperLimit } - -func TestOwns(t *testing.T) { - RegisterTestingT(t) - testCases := []struct { - Name string - Object metav1.Object - Owner metav1.Object - Verify func(t *testing.T, result bool) - }{ - { - Name: "object is owned", - Object: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - UID: "unique-uid", - }, - }, - Verify: func(t *testing.T, result bool) { - Expect(result).To(BeTrue()) - }, - }, { - Name: "object is owned by multiple", - Object: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone-other", - UID: "unique-uid-other", - BlockOwnerDeletion: ptr.To(true), - }, - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - }, - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone-other2", - UID: "unique-uid-other2", - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - UID: "unique-uid", - }, - }, - Verify: func(t *testing.T, result bool) { - Expect(result).To(BeTrue()) - }, - }, { - Name: "object is not owned", - Object: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone-other", - UID: "unique-uid-other", - BlockOwnerDeletion: ptr.To(false), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - UID: "unique-uid", - }, - }, - Verify: func(t *testing.T, result bool) { - Expect(result).To(BeFalse()) - }, - }, { - Name: "object is not owned multiple", - Object: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone-other", - UID: "unique-uid-other", - BlockOwnerDeletion: ptr.To(true), - }, { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone-other2", - UID: "unique-uid-other2", - BlockOwnerDeletion: ptr.To(false), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - UID: "unique-uid", - }, - }, - Verify: func(t *testing.T, result bool) { - Expect(result).To(BeFalse()) - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - testCase.Verify(t, Owns(testCase.Owner, testCase.Object)) - }) - } -} - -func TestEnsureOwnerRef(t *testing.T) { - RegisterTestingT(t) - testCases := []struct { - Name string - Owned metav1.Object - Owner metav1.Object - BlockDelete bool - Verify func(t *testing.T, err error, obj metav1.Object) - }{ - { - Name: "Owner is added", - Owned: &v1alpha1.DNSRecord{}, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-zone", - UID: "unique-uid", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "ManagedZone", - APIVersion: "v1beta1", - }, - }, - BlockDelete: true, - Verify: func(t *testing.T, err error, obj metav1.Object) { - Expect(err).NotTo(HaveOccurred()) - Expect(len(obj.GetOwnerReferences())).To(Equal(1)) - - expectedOwnerRef := metav1.OwnerReference{ - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - } - Expect(obj.GetOwnerReferences()[0]).To(Equal(expectedOwnerRef)) - }, - }, - { - Name: "Does not duplicate owner ref", - Owned: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-zone", - UID: "unique-uid", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "ManagedZone", - APIVersion: "v1beta1", - }, - }, - BlockDelete: true, - Verify: func(t *testing.T, err error, obj metav1.Object) { - Expect(err).NotTo(HaveOccurred()) - Expect(len(obj.GetOwnerReferences())).To(Equal(1)) - - expectedOwnerRef := metav1.OwnerReference{ - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - } - Expect(obj.GetOwnerReferences()[0]).To(Equal(expectedOwnerRef)) - }, - }, - { - Name: "Does update owner ref", - Owned: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(false), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-zone", - UID: "unique-uid", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "ManagedZone", - APIVersion: "v1beta1", - }, - }, - BlockDelete: true, - Verify: func(t *testing.T, err error, obj metav1.Object) { - Expect(err).NotTo(HaveOccurred()) - Expect(len(obj.GetOwnerReferences())).To(Equal(1)) - - expectedOwnerRef := metav1.OwnerReference{ - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - } - Expect(obj.GetOwnerReferences()[0]).To(Equal(expectedOwnerRef)) - }, - }, - { - Name: "Does append owner ref", - Owned: &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "v1", - Kind: "OtherThing", - Name: "otherName", - UID: "other-unique-uid", - BlockOwnerDeletion: ptr.To(false), - }, - }, - }, - }, - Owner: &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-zone", - UID: "unique-uid", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "ManagedZone", - APIVersion: "v1beta1", - }, - }, - BlockDelete: true, - Verify: func(t *testing.T, err error, obj metav1.Object) { - Expect(err).NotTo(HaveOccurred()) - Expect(len(obj.GetOwnerReferences())).To(Equal(2)) - - expectedOwnerRef := metav1.OwnerReference{ - APIVersion: "v1beta1", - Kind: "ManagedZone", - Name: "test-zone", - UID: "unique-uid", - BlockOwnerDeletion: ptr.To(true), - } - Expect(obj.GetOwnerReferences()[1]).To(Equal(expectedOwnerRef)) - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - err := EnsureOwnerRef(testCase.Owner, testCase.Owned, testCase.BlockDelete) - testCase.Verify(t, err, testCase.Owned) - }) - } -} diff --git a/internal/controller/dnsrecord_controller.go b/internal/controller/dnsrecord_controller.go index 9f5b62de..0169b74b 100644 --- a/internal/controller/dnsrecord_controller.go +++ b/internal/controller/dnsrecord_controller.go @@ -18,10 +18,14 @@ package controller import ( "context" + "errors" "fmt" "strings" "time" + "github.com/go-logr/logr" + + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -76,8 +80,10 @@ type DNSRecordReconciler struct { //+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords/finalizers,verbs=update func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithName("dnsrecord_controller") - ctx = log.IntoContext(ctx, logger) + // Keep a reference to the initial logger(baseLogger) so we can update it throughout the reconcile + baseLogger := log.FromContext(ctx).WithName("dnsrecord_controller") + ctx = log.IntoContext(ctx, baseLogger) + logger := baseLogger logger.Info("Reconciling DNSRecord") @@ -97,49 +103,38 @@ func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } dnsRecord := previous.DeepCopy() - //Ensure OwnerID is set in the status - if dnsRecord.Status.OwnerID == "" { - if dnsRecord.Spec.OwnerID != "" { - dnsRecord.Status.OwnerID = dnsRecord.Spec.OwnerID - } else { - dnsRecord.Status.OwnerID = dnsRecord.GetUIDHash() - } - } - - managedZone := &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: dnsRecord.Spec.ManagedZoneRef.Name, - Namespace: dnsRecord.Namespace, - }, - } - err = r.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone, &client.GetOptions{}) - if err != nil { - reason := "ManagedZoneError" - message := fmt.Sprintf("The managedZone could not be loaded: %v", err) - setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, reason, message) - return r.updateStatus(ctx, previous, dnsRecord, false, err) - } - - logger = log.FromContext(ctx). - WithValues("ownerID", dnsRecord.Status.OwnerID). - WithValues("zoneID", managedZone.Status.ID) - ctx = log.IntoContext(ctx, logger) + // Update the logger with appropriate record/zone metadata from the dnsRecord + ctx, logger = r.setLogger(ctx, baseLogger, dnsRecord) if dnsRecord.DeletionTimestamp != nil && !dnsRecord.DeletionTimestamp.IsZero() { logger.Info("Deleting DNSRecord") - if err = r.ReconcileHealthChecks(ctx, dnsRecord, managedZone); client.IgnoreNotFound(err) != nil { - return ctrl.Result{}, err - } - hadChanges, err := r.deleteRecord(ctx, dnsRecord, managedZone) - if err != nil { - logger.Error(err, "Failed to delete DNSRecord") - return ctrl.Result{}, err - } - // if hadChanges - the deleteRecord has successfully applied changes - // in this case we need to queue for validation to ensure DNS Provider retained changes - // before removing finalizer and deleting the DNS Record CR - if hadChanges { - return ctrl.Result{RequeueAfter: randomizedValidationRequeue}, nil + if dnsRecord.HasDNSZoneAssigned() { + // Create a dns provider with config calculated for the current dns record status (Last successful) + dnsProvider, err := r.getDNSProvider(ctx, dnsRecord) + if err != nil { + logger.Error(err, "Failed to load DNS Provider") + reason := "DNSProviderError" + message := fmt.Sprintf("The dns provider could not be loaded: %v", err) + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, reason, message) + return r.updateStatus(ctx, previous, dnsRecord, false, err) + } + + if err = r.ReconcileHealthChecks(ctx, dnsRecord); client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, err + } + hadChanges, err := r.deleteRecord(ctx, dnsRecord, dnsProvider) + if err != nil { + logger.Error(err, "Failed to delete DNSRecord") + return ctrl.Result{}, err + } + // if hadChanges - the deleteRecord has successfully applied changes + // in this case we need to queue for validation to ensure DNS Provider retained changes + // before removing finalizer and deleting the DNS Record CR + if hadChanges { + return ctrl.Result{RequeueAfter: randomizedValidationRequeue}, nil + } + } else { + logger.Info("dns zone was never assigned, skipping zone cleanup") } logger.Info("Removing Finalizer", "finalizer_name", DNSRecordFinalizer) @@ -164,49 +159,91 @@ func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{RequeueAfter: randomizedValidationRequeue}, nil } - if !common.Owns(managedZone, dnsRecord) { - logger.V(1).Info("Record is not owned by ManagedZone", "ManagedZoneName", managedZone.Name) - err = common.EnsureOwnerRef(managedZone, dnsRecord, true) + err = dnsRecord.Validate() + if err != nil { + logger.Error(err, "Failed to validate record") + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, + "ValidationError", fmt.Sprintf("validation of DNSRecord failed: %v", err)) + return r.updateStatus(ctx, previous, dnsRecord, false, err) + } + + //Ensure an Owner ID has been assigned to the record (OwnerID set in the status) + if !dnsRecord.HasOwnerIDAssigned() { + if dnsRecord.Spec.OwnerID != "" { + dnsRecord.Status.OwnerID = dnsRecord.Spec.OwnerID + } else { + dnsRecord.Status.OwnerID = dnsRecord.GetUIDHash() + } + //Update logger and context so it includes updated owner metadata + ctx, logger = r.setLogger(ctx, baseLogger, dnsRecord) + } + + // Ensure a DNS Zone has been assigned to the record (ZoneID and ZoneDomainName are set in the status) + if !dnsRecord.HasDNSZoneAssigned() { + logger.Info(fmt.Sprintf("provider zone not assigned for root host %s, finding suitable zone", dnsRecord.Spec.RootHost)) + + // Create a dns provider with no config to list all potential zones available from the configured provider + p, err := r.ProviderFactory.ProviderFor(ctx, dnsRecord, provider.Config{}) if err != nil { - return ctrl.Result{}, err + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, + "DNSProviderError", fmt.Sprintf("The dns provider could not be loaded: %v", err)) + return r.updateStatus(ctx, previous, dnsRecord, false, err) } - err = r.Client.Update(ctx, dnsRecord) - return ctrl.Result{}, err + + z, err := p.DNSZoneForHost(ctx, dnsRecord.Spec.RootHost) + if err != nil { + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, + "DNSProviderError", fmt.Sprintf("Unable to find suitable zone in provider: %v", err)) + return r.updateStatus(ctx, previous, dnsRecord, false, err) + } + + //Add zone id/domainName to status + dnsRecord.Status.ZoneID = z.ID + dnsRecord.Status.ZoneDomainName = z.DNSName + + //Update logger and context so it includes updated zone metadata + ctx, logger = r.setLogger(ctx, baseLogger, dnsRecord) } - var reason, message string - err = dnsRecord.Validate() + + // Create a dns provider for the current record, must have an owner and zone assigned or will throw an error + dnsProvider, err := r.getDNSProvider(ctx, dnsRecord) if err != nil { - reason = "ValidationError" - message = fmt.Sprintf("validation of DNSRecord failed: %v", err) - setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, reason, message) + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, + "DNSProviderError", fmt.Sprintf("The dns provider could not be loaded: %v", err)) return r.updateStatus(ctx, previous, dnsRecord, false, err) } // Publish the record - hadChanges, err := r.publishRecord(ctx, dnsRecord, managedZone) + hadChanges, err := r.publishRecord(ctx, dnsRecord, dnsProvider) if err != nil { - reason = "ProviderError" - message = fmt.Sprintf("The DNS provider failed to ensure the record: %v", provider.SanitizeError(err)) - setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, reason, message) + logger.Error(err, "Failed to publish record") + setDNSRecordCondition(dnsRecord, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, + "ProviderError", fmt.Sprintf("The DNS provider failed to ensure the record: %v", provider.SanitizeError(err))) return r.updateStatus(ctx, previous, dnsRecord, hadChanges, err) } - if err = r.ReconcileHealthChecks(ctx, dnsRecord, managedZone); err != nil { + if err = r.ReconcileHealthChecks(ctx, dnsRecord); err != nil { return ctrl.Result{}, err } return r.updateStatus(ctx, previous, dnsRecord, hadChanges, nil) } +// setLogger Updates the given Logger with record/zone metadata from the given DNSRecord. +// returns the context with the updated logger set on it, and the updated logger itself. +func (r *DNSRecordReconciler) setLogger(ctx context.Context, logger logr.Logger, dnsRecord *v1alpha1.DNSRecord) (context.Context, logr.Logger) { + logger = logger. + WithValues("rootHost", dnsRecord.Spec.RootHost). + WithValues("ownerID", dnsRecord.Status.OwnerID). + WithValues("zoneID", dnsRecord.Status.ZoneID). + WithValues("zoneDomainName", dnsRecord.Status.ZoneDomainName) + return log.IntoContext(ctx, logger), logger +} + func (r *DNSRecordReconciler) updateStatus(ctx context.Context, previous, current *v1alpha1.DNSRecord, hadChanges bool, specErr error) (reconcile.Result, error) { var requeueTime time.Duration logger := log.FromContext(ctx) - // short loop. We don't publish anything so not changing status - if prematurely, requeueIn := recordReceivedPrematurely(current); prematurely { - return reconcile.Result{RequeueAfter: requeueIn}, nil - } - // failure if specErr != nil { logger.Error(specErr, "Error reconciling DNS Record") @@ -219,6 +256,11 @@ func (r *DNSRecordReconciler) updateStatus(ctx context.Context, previous, curren return ctrl.Result{Requeue: true}, updateError } + // short loop. We don't publish anything so not changing status + if prematurely, requeueIn := recordReceivedPrematurely(current); prematurely { + return reconcile.Result{RequeueAfter: requeueIn}, nil + } + // success if hadChanges { // generation has not changed but there are changes. @@ -278,18 +320,26 @@ func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager, maxRequeue, val return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.DNSRecord{}). - Watches(&v1alpha1.ManagedZone{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + Watches(&v1.Secret{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { logger := log.FromContext(ctx) + s, ok := o.(*v1.Secret) + if !ok { + logger.V(1).Info("unexpected object type", "error", fmt.Sprintf("%T is not a *v1.Secret", o)) + return nil + } + if !strings.HasPrefix(string(s.Type), "kuadrant.io") { + return nil + } var toReconcile []reconcile.Request - // list dns records in the managedzone namespace as they will be in the same namespace as the zone + // list dns records in the secret namespace as they will be in the same namespace as the secret records := &v1alpha1.DNSRecordList{} if err := mgr.GetClient().List(ctx, records, &client.ListOptions{Namespace: o.GetNamespace()}); err != nil { logger.Error(err, "failed to list dnsrecords ", "namespace", o.GetNamespace()) return toReconcile } for _, record := range records.Items { - if common.Owns(o, &record) { - logger.Info("managed zone updated", "managedzone", o.GetNamespace()+"/"+o.GetName(), "enqueuing dnsrecord ", record.GetName()) + if record.Spec.ProviderRef.Name == o.GetName() { + logger.Info("secret updated", "secret", o.GetNamespace()+"/"+o.GetName(), "enqueuing dnsrecord ", record.GetName()) toReconcile = append(toReconcile, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&record)}) } } @@ -298,54 +348,40 @@ func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager, maxRequeue, val Complete(r) } -// deleteRecord deletes record(s) in the DNSPRovider(i.e. route53) configured by the ManagedZone assigned to this -// DNSRecord (dnsRecord.Status.ParentManagedZone). -func (r *DNSRecordReconciler) deleteRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) (bool, error) { +// deleteRecord deletes record(s) in the DNSPRovider(i.e. route53) zone (dnsRecord.Status.ZoneID). +func (r *DNSRecordReconciler) deleteRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, dnsProvider provider.Provider) (bool, error) { logger := log.FromContext(ctx) - managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") - - if !managedZoneReady { - return false, fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) - } - - hadChanges, err := r.applyChanges(ctx, dnsRecord, managedZone, true) + hadChanges, err := r.applyChanges(ctx, dnsRecord, dnsProvider, true) if err != nil { if strings.Contains(err.Error(), "was not found") || strings.Contains(err.Error(), "notFound") { - logger.Info("Record not found in managed zone, continuing", "managedZone", managedZone.Name) + logger.Info("Record not found in zone, continuing") return false, nil } else if strings.Contains(err.Error(), "no endpoints") { - logger.Info("DNS record had no endpoint, continuing", "managedZone", managedZone.Name) + logger.Info("DNS record had no endpoint, continuing") return false, nil } return false, err } - logger.Info("Deleted DNSRecord in manage zone", "managedZone", managedZone.Name) + logger.Info("Deleted DNSRecord in zone") return hadChanges, nil } -// publishRecord publishes record(s) to the DNSPRovider(i.e. route53) configured by the ManagedZone assigned to this -// DNSRecord (dnsRecord.Status.ParentManagedZone). -func (r *DNSRecordReconciler) publishRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) (bool, error) { +// publishRecord publishes record(s) to the DNSPRovider(i.e. route53) zone (dnsRecord.Status.ZoneID). +func (r *DNSRecordReconciler) publishRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, dnsProvider provider.Provider) (bool, error) { logger := log.FromContext(ctx) - managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") - - if !managedZoneReady { - return false, fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) - } - if prematurely, _ := recordReceivedPrematurely(dnsRecord); prematurely { - logger.V(1).Info("Skipping DNSRecord - is still valid", "managedZone", managedZone.Name) + logger.V(1).Info("Skipping DNSRecord - is still valid") return false, nil } - hadChanges, err := r.applyChanges(ctx, dnsRecord, managedZone, false) + hadChanges, err := r.applyChanges(ctx, dnsRecord, dnsProvider, false) if err != nil { return hadChanges, err } - logger.Info("Published DNSRecord to manage zone", "managedZone", managedZone.Name) + logger.Info("Published DNSRecord to zone") return hadChanges, nil } @@ -395,29 +431,37 @@ func setDNSRecordCondition(dnsRecord *v1alpha1.DNSRecord, conditionType string, meta.SetStatusCondition(&dnsRecord.Status.Conditions, cond) } -func (r *DNSRecordReconciler) getDNSProvider(ctx context.Context, managedZone *v1alpha1.ManagedZone) (provider.Provider, error) { +// getDNSProvider returns a Provider configured for the given DNSRecord +// If no zone/id/domain has been assigned to the given record, an error is thrown. +// If no owner has been assigned to the given record, an error is thrown. +// If the provider can't be initialised, an error is thrown. +func (r *DNSRecordReconciler) getDNSProvider(ctx context.Context, dnsRecord *v1alpha1.DNSRecord) (provider.Provider, error) { + var err error + if !dnsRecord.HasOwnerIDAssigned() { + err = errors.Join(fmt.Errorf("has no ownerID assigned")) + } + if !dnsRecord.HasDNSZoneAssigned() { + err = errors.Join(fmt.Errorf("has no DNSZone assigned")) + } + if err != nil { + return nil, err + } providerConfig := provider.Config{ - DomainFilter: externaldnsendpoint.NewDomainFilter([]string{managedZone.Spec.DomainName}), + DomainFilter: externaldnsendpoint.NewDomainFilter([]string{dnsRecord.Status.ZoneDomainName}), ZoneTypeFilter: externaldnsprovider.NewZoneTypeFilter(""), - ZoneIDFilter: externaldnsprovider.NewZoneIDFilter([]string{managedZone.Status.ID}), + ZoneIDFilter: externaldnsprovider.NewZoneIDFilter([]string{dnsRecord.Status.ZoneID}), } - - return r.ProviderFactory.ProviderFor(ctx, managedZone, providerConfig) + return r.ProviderFactory.ProviderFor(ctx, dnsRecord, providerConfig) } // applyChanges creates the Plan and applies it to the registry. Returns true only if the Plan had no errors and there were changes to apply. // The error is nil only if the changes were successfully applied or there were no changes to be made. -func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone, isDelete bool) (bool, error) { +func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, dnsProvider provider.Provider, isDelete bool) (bool, error) { logger := log.FromContext(ctx) - zoneDomainName, _ := strings.CutPrefix(managedZone.Spec.DomainName, v1alpha1.WildcardPrefix) rootDomainName := dnsRecord.Spec.RootHost - zoneDomainFilter := externaldnsendpoint.NewDomainFilter([]string{zoneDomainName}) + zoneDomainFilter := externaldnsendpoint.NewDomainFilter([]string{dnsRecord.Status.ZoneDomainName}) managedDNSRecordTypes := []string{externaldnsendpoint.RecordTypeA, externaldnsendpoint.RecordTypeAAAA, externaldnsendpoint.RecordTypeCNAME} var excludeDNSRecordTypes []string - dnsProvider, err := r.getDNSProvider(ctx, managedZone) - if err != nil { - return false, err - } registry, err := externaldnsregistry.NewTXTRegistry(ctx, dnsProvider, txtRegistryPrefix, txtRegistrySuffix, dnsRecord.Status.OwnerID, txtRegistryCacheInterval, txtRegistryWildcardReplacement, managedDNSRecordTypes, diff --git a/internal/controller/dnsrecord_controller_test.go b/internal/controller/dnsrecord_controller_test.go index 2c35b5e0..dab81629 100644 --- a/internal/controller/dnsrecord_controller_test.go +++ b/internal/controller/dnsrecord_controller_test.go @@ -33,52 +33,32 @@ import ( externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" "github.com/kuadrant/dns-operator/api/v1alpha1" - "github.com/kuadrant/dns-operator/internal/common" + "github.com/kuadrant/dns-operator/pkg/builder" ) var _ = Describe("DNSRecordReconciler", func() { var ( - dnsRecord *v1alpha1.DNSRecord - dnsRecord2 *v1alpha1.DNSRecord - dnsProviderSecret *v1.Secret - managedZone *v1alpha1.ManagedZone - brokenZone *v1alpha1.ManagedZone - testNamespace string + dnsRecord *v1alpha1.DNSRecord + dnsRecord2 *v1alpha1.DNSRecord + dnsProviderSecret *v1.Secret + testNamespace string + testZoneDomainName string + testZoneID string ) BeforeEach(func() { CreateNamespace(&testNamespace) - dnsProviderSecret = testBuildInMemoryCredentialsSecret("inmemory-credentials", testNamespace) - managedZone = testBuildManagedZone("mz-example-com", testNamespace, "example.com", dnsProviderSecret.Name) - brokenZone = testBuildManagedZone("mz-fix-com", testNamespace, "fix.com", "not-there") + testZoneDomainName = "example.com" + // In memory provider currently uses the same value for domain and id + // Issue here to change this https://github.com/Kuadrant/dns-operator/issues/208 + testZoneID = testZoneDomainName + dnsProviderSecret = builder.NewProviderBuilder("inmemory-credentials", testNamespace). + For(v1alpha1.SecretTypeKuadrantInmemory). + WithZonesInitialisedFor(testZoneDomainName). + Build() Expect(k8sClient.Create(ctx, dnsProviderSecret)).To(Succeed()) - Expect(k8sClient.Create(ctx, managedZone)).To(Succeed()) - Expect(k8sClient.Create(ctx, brokenZone)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(managedZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "ObservedGeneration": Equal(managedZone.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(brokenZone), brokenZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(brokenZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionFalse), - "ObservedGeneration": Equal(brokenZone.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) dnsRecord = &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ @@ -87,8 +67,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getDefaultTestEndpoints(), }, @@ -104,43 +84,80 @@ var _ = Describe("DNSRecordReconciler", func() { err := k8sClient.Delete(ctx, dnsRecord2) Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) } - if managedZone != nil { - err := k8sClient.Delete(ctx, managedZone, client.PropagationPolicy(metav1.DeletePropagationForeground)) - Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) - } - if brokenZone != nil { - err := k8sClient.Delete(ctx, brokenZone, client.PropagationPolicy(metav1.DeletePropagationForeground)) + if dnsProviderSecret != nil { + err := k8sClient.Delete(ctx, dnsProviderSecret) Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) } }) - It("prevents creation of invalid records", func(ctx SpecContext) { - dnsRecord = &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - Name: "bar.example.com", - Namespace: testNamespace, - }, - Spec: v1alpha1.DNSRecordSpec{ - RootHost: "bar.example .com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + // Test cases covering validation of the DNSRecord resource fields + Context("validation", func() { + It("should error with no providerRef", func(ctx SpecContext) { + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar.example.com", + Namespace: testNamespace, }, - Endpoints: getTestEndpoints("bar.example.com", "127.0.0.1"), - HealthCheck: &v1alpha1.HealthCheckSpec{ - Endpoint: "health", - Port: ptr.To(5), - Protocol: ptr.To(v1alpha1.HealthProtocol("cat")), - FailureThreshold: ptr.To(-1), + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "bar.example.com", + Endpoints: getTestEndpoints("bar.example.com", "127.0.0.1"), + HealthCheck: nil, }, - }, - } - err := k8sClient.Create(ctx, dnsRecord) - Expect(err).To(MatchError(ContainSubstring("spec.rootHost: Invalid value"))) - Expect(err).To(MatchError(ContainSubstring("spec.healthCheck.endpoint: Invalid value"))) - Expect(err).To(MatchError(ContainSubstring("Only ports 80, 443, 1024-49151 are allowed"))) - Expect(err).To(MatchError(ContainSubstring("Only HTTP or HTTPS protocols are allowed"))) - Expect(err).To(MatchError(ContainSubstring("Failure threshold must be greater than 0"))) - + } + err := k8sClient.Create(ctx, dnsRecord) + // It doesn't seem to be possible to have a field marked as required and include the omitempty json struct tag + // so even though we don't include the providerRef in the test an empty one is being added. + // The error in this case when created via the json openapi would actually be `spec.providerRef: Required value` + Expect(err).To(MatchError(ContainSubstring("spec.providerRef.name in body should be at least 1 chars long"))) + }) + + It("should error with no rootHost", func(ctx SpecContext) { + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar.example.com", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, + }, + Endpoints: getTestEndpoints("bar.example.com", "127.0.0.1"), + HealthCheck: nil, + }, + } + err := k8sClient.Create(ctx, dnsRecord) + // as above + // The error in this case when created via the json openapi would actually be `spec.providerRef: Required value` + Expect(err).To(MatchError(ContainSubstring("spec.rootHost in body should be at least 1 chars long"))) + }) + + It("prevents creation of invalid records", func(ctx SpecContext) { + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar.example.com", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "bar.example .com", + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, + }, + Endpoints: getTestEndpoints("bar.example.com", "127.0.0.1"), + HealthCheck: &v1alpha1.HealthCheckSpec{ + Endpoint: "health", + Port: ptr.To(5), + Protocol: ptr.To(v1alpha1.HealthProtocol("cat")), + FailureThreshold: ptr.To(-1), + }, + }, + } + err := k8sClient.Create(ctx, dnsRecord) + Expect(err).To(MatchError(ContainSubstring("spec.rootHost: Invalid value"))) + Expect(err).To(MatchError(ContainSubstring("spec.healthCheck.endpoint: Invalid value"))) + Expect(err).To(MatchError(ContainSubstring("Only ports 80, 443, 1024-49151 are allowed"))) + Expect(err).To(MatchError(ContainSubstring("Only HTTP or HTTPS protocols are allowed"))) + Expect(err).To(MatchError(ContainSubstring("Failure threshold must be greater than 0"))) + }) }) It("handles records with similar root hosts", func(ctx SpecContext) { @@ -151,8 +168,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "bar.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getTestEndpoints("bar.example.com", "127.0.0.1"), HealthCheck: nil, @@ -181,8 +198,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.bar.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getTestEndpoints("foo.bar.example.com", "127.0.0.2"), HealthCheck: nil, @@ -214,58 +231,7 @@ var _ = Describe("DNSRecordReconciler", func() { }, TestTimeoutMedium, time.Second).Should(Succeed()) }) - It("dns records are reconciled once zone is fixed", func(ctx SpecContext) { - dnsRecord = &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo.fix.com", - Namespace: testNamespace, - }, - Spec: v1alpha1.DNSRecordSpec{ - RootHost: "foo.fix.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: brokenZone.Name, - }, - Endpoints: getTestEndpoints("foo.fix.com", "127.0.0.1"), - HealthCheck: nil, - }, - } - Expect(k8sClient.Create(ctx, dnsRecord)).To(Succeed()) - Eventually(func(g Gomega) { - 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(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionFalse), - "Reason": Equal("ProviderError"), - "Message": ContainSubstring("The DNS provider failed to ensure the record"), - "ObservedGeneration": Equal(dnsRecord.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - fixedZone := brokenZone.DeepCopy() - fixedZone.Spec.SecretRef.Name = dnsProviderSecret.Name - Expect(k8sClient.Update(ctx, fixedZone)).NotTo(HaveOccurred()) - - Eventually(func(g Gomega) { - 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(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "Reason": Equal("ProviderSuccess"), - "Message": Equal("Provider ensured the dns record"), - "ObservedGeneration": Equal(dnsRecord.Generation), - })), - ) - g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) - g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - }) - - It("can delete a record with an valid managed zone", func(ctx SpecContext) { + It("can delete a record with a valid dns provider secret", func(ctx SpecContext) { dnsRecord = &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.example.com", @@ -273,8 +239,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getDefaultTestEndpoints(), HealthCheck: nil, @@ -293,7 +259,8 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) - g.Expect(common.Owns(managedZone, dnsRecord)).To(BeTrue()) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal("example.com")) + g.Expect(dnsRecord.Status.ZoneID).To(Equal("example.com")) }, TestTimeoutMedium, time.Second).Should(Succeed()) err := k8sClient.Delete(ctx, dnsRecord) @@ -322,6 +289,8 @@ var _ = Describe("DNSRecordReconciler", func() { ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) g.Expect(dnsRecord.Status.WriteCounter).To(BeZero()) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -344,6 +313,8 @@ var _ = Describe("DNSRecordReconciler", func() { ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) g.Expect(dnsRecord.Status.WriteCounter).To(BeZero()) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) @@ -374,8 +345,8 @@ var _ = Describe("DNSRecordReconciler", func() { Spec: v1alpha1.DNSRecordSpec{ OwnerID: "owner1", RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getDefaultTestEndpoints(), }, @@ -396,6 +367,8 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf("owner1")) }, TestTimeoutMedium, time.Second).Should(Succeed()) @@ -428,8 +401,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getDefaultTestEndpoints(), }, @@ -441,8 +414,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: getTestEndpoints("foo.example.com", "127.0.0.2"), }, @@ -464,6 +437,8 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) @@ -552,6 +527,8 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) }, TestTimeoutMedium, time.Second).Should(Succeed()) dnsRecord2 = &v1alpha1.DNSRecord{ @@ -561,8 +538,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -612,6 +589,8 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.ZoneID).To(Equal(testZoneID)) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal(testZoneDomainName)) }, TestTimeoutMedium, time.Second).Should(Succeed()) dnsRecord2 = &v1alpha1.DNSRecord{ @@ -621,8 +600,8 @@ var _ = Describe("DNSRecordReconciler", func() { }, Spec: v1alpha1.DNSRecordSpec{ RootHost: "bar.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecret.Name, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -655,4 +634,151 @@ var _ = Describe("DNSRecordReconciler", func() { }, TestTimeoutMedium, time.Second).Should(Succeed()) }) + // DNS Provider configuration specific test cases + Context("dns provider", func() { + + var pBuilder *builder.ProviderBuilder + var pSecret *v1.Secret + + BeforeEach(func() { + pBuilder = builder.NewProviderBuilder("inmemory-credentials-2", testNamespace). + For(v1alpha1.SecretTypeKuadrantInmemory) + }) + + AfterEach(func() { + if pSecret != nil { + err := k8sClient.Delete(ctx, pSecret) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) + } + }) + + It("should assign the most suitable zone for the provider", func(ctx SpecContext) { + pSecret = pBuilder. + WithZonesInitialisedFor("example.com", "foo.example.com", "bar.foo.example.com"). + Build() + Expect(k8sClient.Create(ctx, pSecret)).To(Succeed()) + + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar.foo.example.com", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "bar.foo.example.com", + ProviderRef: v1alpha1.ProviderRef{ + Name: pSecret.Name, + }, + Endpoints: getTestEndpoints("bar.foo.example.com", "127.0.0.1"), + }, + } + Expect(k8sClient.Create(ctx, dnsRecord)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.ZoneID).To(Equal("foo.example.com")) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal("foo.example.com")) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + }) + + It("should report an error when no suitable zone can be found for the provider", func(ctx SpecContext) { + pSecret = pBuilder. + WithZonesInitialisedFor("example.com"). + Build() + Expect(k8sClient.Create(ctx, pSecret)).To(Succeed()) + + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo.noexist.com", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "foo.noexist.com", + ProviderRef: v1alpha1.ProviderRef{ + Name: pSecret.Name, + }, + Endpoints: getTestEndpoints("foo.noexist.com", "127.0.0.1"), + }, + } + Expect(k8sClient.Create(ctx, dnsRecord)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.ZoneID).To(BeEmpty()) + g.Expect(dnsRecord.Status.ZoneDomainName).To(BeEmpty()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + "Status": Equal(metav1.ConditionFalse), + "Reason": Equal("DNSProviderError"), + "Message": Equal("Unable to find suitable zone in provider: no valid zone found for host: foo.noexist.com"), + "ObservedGeneration": Equal(dnsRecord.Generation), + })), + ) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + }) + + It("should update broken record when provider is updated", func(ctx SpecContext) { + pSecret = pBuilder. + WithZonesInitialisedFor("example.com"). + Build() + Expect(k8sClient.Create(ctx, pSecret)).To(Succeed()) + + dnsRecord = &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo.otherdomain.com", + Namespace: testNamespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "foo.otherdomain.com", + ProviderRef: v1alpha1.ProviderRef{ + Name: pSecret.Name, + }, + Endpoints: getTestEndpoints("foo.otherdomain.com", "127.0.0.1"), + }, + } + Expect(k8sClient.Create(ctx, dnsRecord)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.ZoneID).To(BeEmpty()) + g.Expect(dnsRecord.Status.ZoneDomainName).To(BeEmpty()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + "Status": Equal(metav1.ConditionFalse), + "Reason": Equal("DNSProviderError"), + "Message": Equal("Unable to find suitable zone in provider: no valid zone found for host: foo.otherdomain.com"), + "ObservedGeneration": Equal(dnsRecord.Generation), + })), + ) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + + pSecretUpdate := builder.NewProviderBuilder("inmemory-credentials-2", testNamespace). + For(v1alpha1.SecretTypeKuadrantInmemory). + WithZonesInitialisedFor("example.com", "otherdomain.com"). + Build() + + // Update the provider secrets init zones to include the other domain, now matches for record with root host `foo.example.com` + pSecret.StringData = pSecretUpdate.StringData + Expect(k8sClient.Update(ctx, pSecret)).NotTo(HaveOccurred()) + + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.ZoneID).To(Equal("otherdomain.com")) + g.Expect(dnsRecord.Status.ZoneDomainName).To(Equal("otherdomain.com")) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + "Status": Equal(metav1.ConditionTrue), + "Reason": Equal("ProviderSuccess"), + "Message": Equal("Provider ensured the dns record"), + "ObservedGeneration": Equal(dnsRecord.Generation), + })), + ) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + }) + + }) + }) diff --git a/internal/controller/dnsrecord_healthchecks.go b/internal/controller/dnsrecord_healthchecks.go index a4df4803..00cd01a1 100644 --- a/internal/controller/dnsrecord_healthchecks.go +++ b/internal/controller/dnsrecord_healthchecks.go @@ -23,11 +23,11 @@ type healthChecksConfig struct { Protocol *provider.HealthCheckProtocol } -func (r *DNSRecordReconciler) ReconcileHealthChecks(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { +func (r *DNSRecordReconciler) ReconcileHealthChecks(ctx context.Context, dnsRecord *v1alpha1.DNSRecord) error { var results []provider.HealthCheckResult var err error - dnsProvider, err := r.getDNSProvider(ctx, managedZone) + dnsProvider, err := r.getDNSProvider(ctx, dnsRecord) if err != nil { return err } diff --git a/internal/controller/helper_test.go b/internal/controller/helper_test.go index a734539a..f12e5dd0 100644 --- a/internal/controller/helper_test.go +++ b/internal/controller/helper_test.go @@ -5,11 +5,7 @@ package controller import ( "time" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" - - kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" ) const ( @@ -22,36 +18,10 @@ const ( DefaultValidationDuration = time.Millisecond * 500 ) -func testBuildManagedZone(name, ns, domainName, secretName string) *kuadrantdnsv1alpha1.ManagedZone { - return &kuadrantdnsv1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Spec: kuadrantdnsv1alpha1.ManagedZoneSpec{ - DomainName: domainName, - Description: domainName, - SecretRef: kuadrantdnsv1alpha1.ProviderRef{ - Name: secretName, - }, - }, - } -} - -func testBuildInMemoryCredentialsSecret(name, ns string) *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Data: map[string][]byte{}, - Type: "kuadrant.io/inmemory", - } -} - func getDefaultTestEndpoints() []*externaldnsendpoint.Endpoint { return getTestEndpoints("foo.example.com", "127.0.0.1") } + func getTestEndpoints(dnsName, ip string) []*externaldnsendpoint.Endpoint { return []*externaldnsendpoint.Endpoint{ { diff --git a/internal/controller/managedzone_controller.go b/internal/controller/managedzone_controller.go deleted file mode 100644 index ff7cbc63..00000000 --- a/internal/controller/managedzone_controller.go +++ /dev/null @@ -1,435 +0,0 @@ -/* -Copyright 2024. - -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 controller - -import ( - "context" - "errors" - "fmt" - "strings" - - v1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - externaldns "sigs.k8s.io/external-dns/endpoint" - - "github.com/kuadrant/dns-operator/api/v1alpha1" - "github.com/kuadrant/dns-operator/internal/common" - "github.com/kuadrant/dns-operator/internal/metrics" - "github.com/kuadrant/dns-operator/internal/provider" -) - -const ( - ManagedZoneFinalizer = "kuadrant.io/managed-zone" -) - -var ( - ErrProvider = errors.New("ProviderError") - ErrZoneValidation = errors.New("ZoneValidationError") - ErrProviderSecretMissing = errors.New("ProviderSecretMissing") -) - -// ManagedZoneReconciler reconciles a ManagedZone object -type ManagedZoneReconciler struct { - client.Client - Scheme *runtime.Scheme - ProviderFactory provider.Factory -} - -//+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones/finalizers,verbs=update - -func (r *ManagedZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithName("managedzone_controller") - ctx = log.IntoContext(ctx, logger) - - logger.V(1).Info("Reconciling ManagedZone") - - previous := &v1alpha1.ManagedZone{} - err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, previous) - if err != nil { - if err := client.IgnoreNotFound(err); err == nil { - return ctrl.Result{}, nil - } else { - return ctrl.Result{}, err - } - } - - managedZone := previous.DeepCopy() - - if managedZone.DeletionTimestamp != nil && !managedZone.DeletionTimestamp.IsZero() { - if err := r.deleteParentZoneNSRecord(ctx, managedZone); err != nil { - logger.Error(err, "Failed to delete parent Zone NS Record") - return ctrl.Result{}, err - } - - if recordsExist, err := r.hasOwnedRecords(ctx, managedZone); err != nil { - logger.Error(err, "Failed to check owned records") - return ctrl.Result{}, err - } else if recordsExist { - logger.Info("ManagedZone deletion awaiting removal of owned DNS records") - return ctrl.Result{Requeue: true}, nil - } - - if err := r.deleteManagedZone(ctx, managedZone); err != nil { - logger.Error(err, "Failed to delete ManagedZone") - return ctrl.Result{}, err - } - controllerutil.RemoveFinalizer(managedZone, ManagedZoneFinalizer) - - err = r.Update(ctx, managedZone) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{}, nil - } - - if !controllerutil.ContainsFinalizer(managedZone, ManagedZoneFinalizer) { - controllerutil.AddFinalizer(managedZone, ManagedZoneFinalizer) - - err = r.setParentZoneOwner(ctx, managedZone) - if err != nil { - return ctrl.Result{}, err - } - - err = r.Update(ctx, managedZone) - if err != nil { - return ctrl.Result{}, err - } - } - - err = r.publishManagedZone(ctx, managedZone) - if err != nil { - reason := "UnknownError" - message := fmt.Sprintf("unexpected error %v ", provider.SanitizeError(err)) - if errors.Is(err, ErrProvider) { - reason = ErrProvider.Error() - message = err.Error() - } - if errors.Is(err, ErrZoneValidation) { - reason = ErrZoneValidation.Error() - message = err.Error() - } - if errors.Is(err, ErrProviderSecretMissing) { - metrics.SecretMissing.WithLabelValues(managedZone.Name, managedZone.Namespace, managedZone.Spec.SecretRef.Name).Set(1) - reason = "DNSProviderSecretNotFound" - message = fmt.Sprintf( - "Could not find secret: %v/%v for managedzone: %v/%v", - managedZone.Namespace, managedZone.Spec.SecretRef.Name, - managedZone.Namespace, managedZone.Name) - } else { - metrics.SecretMissing.WithLabelValues(managedZone.Name, managedZone.Namespace, managedZone.Spec.SecretRef.Name).Set(0) - } - - setManagedZoneCondition(managedZone, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, reason, message) - statusUpdateErr := r.Status().Update(ctx, managedZone) - if statusUpdateErr != nil { - return ctrl.Result{}, fmt.Errorf("zone error %v : status update failed error %v", err, statusUpdateErr) - } - return ctrl.Result{}, err - } - - err = r.createParentZoneNSRecord(ctx, managedZone) - if err != nil { - message := fmt.Sprintf("Failed to create the NS record in the parent managed zone: %v", provider.SanitizeError(err)) - setManagedZoneCondition(managedZone, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, "ParentZoneNSRecordError", message) - statusUpdateErr := r.Status().Update(ctx, managedZone) - if statusUpdateErr != nil { - // if we fail to update the status we want an immediate requeue to ensure the end user sees the error - return ctrl.Result{}, fmt.Errorf("provider failed error %v : status update failed error %v", err, statusUpdateErr) - } - return ctrl.Result{}, err - } - - // Check the parent zone NS record status - err = r.parentZoneNSRecordReady(ctx, managedZone) - if err != nil { - message := fmt.Sprintf("NS Record ready status check failed: %v", provider.SanitizeError(err)) - setManagedZoneCondition(managedZone, string(v1alpha1.ConditionTypeReady), metav1.ConditionFalse, "ParentZoneNSRecordNotReady", message) - statusUpdateErr := r.Status().Update(ctx, managedZone) - if statusUpdateErr != nil { - // if we fail to update the status we want an immediate requeue to ensure the end user sees the error - return ctrl.Result{}, fmt.Errorf("provider failed error %v : status update failed error %v", err, statusUpdateErr) - } - return ctrl.Result{}, err - } - - //We are all good set ready status true - managedZone.Status.ObservedGeneration = managedZone.Generation - setManagedZoneCondition(managedZone, string(v1alpha1.ConditionTypeReady), metav1.ConditionTrue, "ProviderSuccess", "Provider ensured the managed zone") - err = r.Status().Update(ctx, managedZone) - if err != nil { - return ctrl.Result{}, err - } - logger.Info("Reconciled ManagedZone") - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *ManagedZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.ManagedZone{}). - Owns(&v1alpha1.ManagedZone{}). - Owns(&v1alpha1.DNSRecord{}). - Watches(&v1.Secret{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { - logger := log.FromContext(ctx) - var toReconcile []reconcile.Request - - zones := &v1alpha1.ManagedZoneList{} - if err := mgr.GetClient().List(ctx, zones, &client.ListOptions{Namespace: o.GetNamespace()}); err != nil { - logger.Error(err, "failed to list zones ", "namespace", o.GetNamespace()) - return toReconcile - } - for _, zone := range zones.Items { - if zone.Spec.SecretRef.Name == o.GetName() { - logger.Info("managed zone secret updated", "secret", o.GetNamespace()+"/"+o.GetName(), "enqueuing zone ", zone.GetName()) - toReconcile = append(toReconcile, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&zone)}) - } - } - return toReconcile - })). - Complete(r) -} - -func (r *ManagedZoneReconciler) publishManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - - dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone, provider.Config{}) - if err != nil { - if k8serrors.IsNotFound(err) { - return fmt.Errorf("%w, the secret '%s/%s', referenced in the managedZone '%s/%s' does not exist", - ErrProviderSecretMissing, - managedZone.Namespace, managedZone.Spec.SecretRef.Name, - managedZone.Namespace, managedZone.Name) - } - return fmt.Errorf("failed to get provider for the zone: %w", provider.SanitizeError(err)) - } - - mzResp, err := dnsProvider.EnsureManagedZone(ctx, managedZone) - if err != nil { - err = fmt.Errorf("%w, The DNS provider failed to ensure the managed zone: %v", ErrProvider, provider.SanitizeError(err)) - } else if managedZone.Spec.ID != "" && mzResp.DNSName != managedZone.Spec.DomainName { - err = fmt.Errorf("%w, zone DNS name '%s' and managed zone domain name '%s' do not match for zone id '%s'", ErrZoneValidation, mzResp.DNSName, managedZone.Spec.DomainName, managedZone.Spec.ID) - } - - if err != nil { - managedZone.Status.ID = "" - managedZone.Status.RecordCount = 0 - managedZone.Status.NameServers = nil - return err - } - - managedZone.Status.ID = mzResp.ID - managedZone.Status.RecordCount = mzResp.RecordCount - managedZone.Status.NameServers = mzResp.NameServers - - return nil -} - -func (r *ManagedZoneReconciler) deleteManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - logger := log.FromContext(ctx) - - if managedZone.Spec.ID != "" { - logger.Info("Skipping deletion of managed zone with provider ID specified in spec") - return nil - } - - dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone, provider.Config{}) - if err != nil { - return fmt.Errorf("failed to get DNS provider instance : %v", err) - } - err = dnsProvider.DeleteManagedZone(managedZone) - if err != nil { - if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "notFound") { - logger.Info("ManagedZone was not found, continuing") - return nil - } - return fmt.Errorf("%w, Failed to delete from provider. Provider Error: %v", ErrProvider, provider.SanitizeError(err)) - } - logger.Info("Deleted ManagedZone") - - return nil -} - -func (r *ManagedZoneReconciler) getParentZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (*v1alpha1.ManagedZone, error) { - if managedZone.Spec.ParentManagedZone == nil { - return nil, nil - } - parentZone := &v1alpha1.ManagedZone{} - err := r.Client.Get(ctx, client.ObjectKey{Namespace: managedZone.Namespace, Name: managedZone.Spec.ParentManagedZone.Name}, parentZone) - if err != nil { - return parentZone, err - } - return parentZone, nil -} - -func (r *ManagedZoneReconciler) setParentZoneOwner(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - parentZone, err := r.getParentZone(ctx, managedZone) - if err != nil { - return err - } - if parentZone == nil { - return nil - } - - err = controllerutil.SetControllerReference(parentZone, managedZone, r.Scheme) - if err != nil { - return err - } - - return err -} - -func (r *ManagedZoneReconciler) createParentZoneNSRecord(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - parentZone, err := r.getParentZone(ctx, managedZone) - if err != nil { - return err - } - if parentZone == nil { - return nil - } - - recordName := managedZone.Spec.DomainName - //Ensure NS record is created in parent managed zone if one is set - recordTargets := make([]string, len(managedZone.Status.NameServers)) - for index := range managedZone.Status.NameServers { - recordTargets[index] = *managedZone.Status.NameServers[index] - } - recordType := string(v1alpha1.NSRecordType) - - nsRecord := &v1alpha1.DNSRecord{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: recordName, - Namespace: parentZone.Namespace, - }, - Spec: v1alpha1.DNSRecordSpec{ - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: parentZone.Name, - }, - Endpoints: []*externaldns.Endpoint{ - { - DNSName: recordName, - Targets: recordTargets, - RecordType: recordType, - RecordTTL: 172800, - }, - }, - }, - } - err = controllerutil.SetControllerReference(parentZone, nsRecord, r.Scheme) - if err != nil { - return err - } - err = r.Client.Create(ctx, nsRecord, &client.CreateOptions{}) - if err != nil && !k8serrors.IsAlreadyExists(err) { - return err - } - - return nil -} - -func (r *ManagedZoneReconciler) deleteParentZoneNSRecord(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - parentZone, err := r.getParentZone(ctx, managedZone) - if err := client.IgnoreNotFound(err); err != nil { - return err - } - if parentZone == nil { - return nil - } - - recordName := managedZone.Spec.DomainName - - nsRecord := &v1alpha1.DNSRecord{} - err = r.Client.Get(ctx, client.ObjectKey{Namespace: parentZone.Namespace, Name: recordName}, nsRecord) - if err != nil { - if err := client.IgnoreNotFound(err); err == nil { - return nil - } else { - return err - } - } - - err = r.Client.Delete(ctx, nsRecord, &client.DeleteOptions{}) - if err != nil { - return err - } - - return nil -} - -func (r *ManagedZoneReconciler) parentZoneNSRecordReady(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - parentZone, err := r.getParentZone(ctx, managedZone) - if err := client.IgnoreNotFound(err); err != nil { - return err - } - if parentZone == nil { - return nil - } - - recordName := managedZone.Spec.DomainName - - nsRecord := &v1alpha1.DNSRecord{} - err = r.Client.Get(ctx, client.ObjectKey{Namespace: parentZone.Namespace, Name: recordName}, nsRecord) - if err != nil { - if err := client.IgnoreNotFound(err); err == nil { - return nil - } else { - return err - } - } - - nsRecordReady := meta.IsStatusConditionTrue(nsRecord.Status.Conditions, string(v1alpha1.ConditionTypeReady)) - if !nsRecordReady { - return fmt.Errorf("the ns record is not in a ready state : %s", nsRecord.Name) - } - return nil -} - -func (r *ManagedZoneReconciler) hasOwnedRecords(ctx context.Context, zone *v1alpha1.ManagedZone) (bool, error) { - records := &v1alpha1.DNSRecordList{} - if err := r.List(ctx, records, client.InNamespace(zone.GetNamespace())); err != nil { - return false, err - } - - for _, record := range records.Items { - if common.Owns(zone, &record) { - return true, nil - } - } - return false, nil -} - -// setManagedZoneCondition adds or updates a given condition in the ManagedZone status. -func setManagedZoneCondition(managedZone *v1alpha1.ManagedZone, conditionType string, status metav1.ConditionStatus, reason, message string) { - cond := metav1.Condition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - ObservedGeneration: managedZone.Generation, - } - meta.SetStatusCondition(&managedZone.Status.Conditions, cond) -} diff --git a/internal/controller/managedzone_controller_test.go b/internal/controller/managedzone_controller_test.go deleted file mode 100644 index 8c6b1f47..00000000 --- a/internal/controller/managedzone_controller_test.go +++ /dev/null @@ -1,316 +0,0 @@ -//go:build integration - -/* -Copyright 2024. - -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 controller - -import ( - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/kuadrant/dns-operator/api/v1alpha1" - //+kubebuilder:scaffold:imports -) - -var _ = Describe("ManagedZoneReconciler", func() { - Context("testing ManagedZone controller", func() { - var dnsProviderSecret *v1.Secret - var managedZone *v1alpha1.ManagedZone - var dnsRecord *v1alpha1.DNSRecord - var testNamespace string - - BeforeEach(func() { - CreateNamespace(&testNamespace) - - dnsProviderSecret = testBuildInMemoryCredentialsSecret("inmemory-credentials", testNamespace) - managedZone = testBuildManagedZone("mz-example-com", testNamespace, "example.com", dnsProviderSecret.Name) - - dnsRecord = &v1alpha1.DNSRecord{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo.example.com", - Namespace: testNamespace, - }, - Spec: v1alpha1.DNSRecordSpec{ - OwnerID: "owner1", - RootHost: "foo.example.com", - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, - }, - Endpoints: getDefaultTestEndpoints(), - }, - } - - Expect(k8sClient.Create(ctx, dnsProviderSecret)).To(Succeed()) - }) - - AfterEach(func() { - if dnsRecord != nil { - err := k8sClient.Delete(ctx, dnsRecord) - Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) - } - - // Clean up managedZones - mzList := &v1alpha1.ManagedZoneList{} - err := k8sClient.List(ctx, mzList, client.InNamespace(testNamespace)) - Expect(err).NotTo(HaveOccurred()) - for _, mz := range mzList.Items { - err = k8sClient.Delete(ctx, &mz, client.PropagationPolicy(metav1.DeletePropagationForeground)) - Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) - } - - if dnsProviderSecret != nil { - err := k8sClient.Delete(ctx, dnsProviderSecret, client.PropagationPolicy(metav1.DeletePropagationForeground)) - Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) - } - }) - - It("should accept a managed zone for this controller and allow deletion", func() { - Expect(k8sClient.Create(ctx, managedZone)).To(Succeed()) - - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(managedZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "ObservedGeneration": Equal(managedZone.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - Expect(k8sClient.Delete(ctx, managedZone)).To(Succeed()) - Eventually(func() error { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - if err != nil && !errors.IsNotFound(err) { - return err - } - return nil - }, TestTimeoutMedium, TestRetryIntervalMedium).Should(Succeed()) - }) - - It("should reject a managed zone with an invalid domain name", func() { - invalidDomainNameManagedZone := &v1alpha1.ManagedZone{ - ObjectMeta: metav1.ObjectMeta{ - Name: "invalid_domain", - Namespace: testNamespace, - }, - Spec: v1alpha1.ManagedZoneSpec{ - ID: "invalid_domain", - DomainName: "invalid_domain", - }, - } - err := k8sClient.Create(ctx, invalidDomainNameManagedZone) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.domainName in body should match")) - }) - - It("managed zone should not become ready with a spec.ID that does not exist", func() { - //Create a managed zone with no spec.ID - Expect(k8sClient.Create(ctx, managedZone)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(managedZone.Status.ID).To(Equal("example.com")) - g.Expect(managedZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "ObservedGeneration": Equal(managedZone.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - //Create a second managed zone with spec.ID referencing a zone id that does not exist - mz := testBuildManagedZone("mz-example-com-2", testNamespace, "example.com", dnsProviderSecret.Name) - mz.Spec.ID = "dosnotexist" - Expect(k8sClient.Create(ctx, mz)).To(Succeed()) - Eventually(func(g Gomega) error { - err := k8sClient.Get(ctx, client.ObjectKey{Namespace: mz.Namespace, Name: mz.Name}, mz) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(mz.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionFalse), - "Reason": Equal("ProviderError"), - "Message": And( - ContainSubstring("The DNS provider failed to ensure the managed zone"), - ContainSubstring("not found")), - })), - ) - g.Expect(mz.Finalizers).To(ContainElement(ManagedZoneFinalizer)) - return nil - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - //Update second managed zone to use the known existing managed zone id (managedZone.Status.ID) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(mz), mz) - g.Expect(err).NotTo(HaveOccurred()) - - mz.Spec.ID = managedZone.Status.ID - err = k8sClient.Update(ctx, mz) - g.Expect(err).NotTo(HaveOccurred()) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - Eventually(func(g Gomega) error { - err := k8sClient.Get(ctx, client.ObjectKey{Namespace: mz.Namespace, Name: mz.Name}, mz) - Expect(err).NotTo(HaveOccurred()) - g.Expect(mz.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "Reason": Equal("ProviderSuccess"), - "ObservedGeneration": Equal(mz.Generation), - })), - ) - g.Expect(mz.Finalizers).To(ContainElement(ManagedZoneFinalizer)) - return nil - }, TestTimeoutMedium, time.Second).Should(Succeed()) - }) - - It("managed zone should not become ready with a spec.DomainName and spec.ID that do no match provider zone", func() { - //Create a managed zone with no spec.ID - Expect(k8sClient.Create(ctx, managedZone)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(managedZone.Status.ID).To(Equal("example.com")) - g.Expect(managedZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "ObservedGeneration": Equal(managedZone.Generation), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - //Create a second managed zone with spec.ID of known existing managed zone (managedZone.Status.ID) but with a different domainName i.e. !example.com - mz := testBuildManagedZone("mz-example-com-2", testNamespace, "foo.example.com", dnsProviderSecret.Name) - mz.Spec.ID = managedZone.Status.ID - Expect(k8sClient.Create(ctx, mz)).To(Succeed()) - Eventually(func(g Gomega) error { - err := k8sClient.Get(ctx, client.ObjectKey{Namespace: mz.Namespace, Name: mz.Name}, mz) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(mz.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionFalse), - "Reason": Equal("ZoneValidationError"), - "Message": Equal("ZoneValidationError, zone DNS name 'example.com' and managed zone domain name 'foo.example.com' do not match for zone id 'example.com'"), - })), - ) - g.Expect(mz.Finalizers).To(ContainElement(ManagedZoneFinalizer)) - return nil - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - //Update second managed zone to use the known existing managed zone domainName (managedZone.Spec.DomainName) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(mz), mz) - g.Expect(err).NotTo(HaveOccurred()) - mz.Spec.DomainName = managedZone.Spec.DomainName - err = k8sClient.Update(ctx, mz) - g.Expect(err).NotTo(HaveOccurred()) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - Eventually(func(g Gomega) error { - err := k8sClient.Get(ctx, client.ObjectKey{Namespace: mz.Namespace, Name: mz.Name}, mz) - Expect(err).NotTo(HaveOccurred()) - g.Expect(mz.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "Reason": Equal("ProviderSuccess"), - "ObservedGeneration": Equal(mz.Generation), - })), - ) - g.Expect(mz.Finalizers).To(ContainElement(ManagedZoneFinalizer)) - return nil - }, TestTimeoutMedium, time.Second).Should(Succeed()) - }) - It("reports an error when managed zone secret is absent", func(ctx SpecContext) { - badSecretMZ := testBuildManagedZone("mz-example-com", testNamespace, "example.com", "badSecretName") - Expect(k8sClient.Create(ctx, badSecretMZ)).To(Succeed()) - - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(badSecretMZ), badSecretMZ) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(badSecretMZ.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionFalse), - "ObservedGeneration": Equal(badSecretMZ.Generation), - "Reason": Equal("DNSProviderSecretNotFound"), - })), - ) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - }) - It("Should block deletion of a managed zone when it still owns DNS Records", func(ctx SpecContext) { - Expect(k8sClient.Create(ctx, managedZone)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(managedZone.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - })), - ) - g.Expect(managedZone.Finalizers).To(ContainElement(ManagedZoneFinalizer)) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - // create DNS Record - Expect(k8sClient.Create(ctx, dnsRecord)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - // Delete managed zone - Expect(k8sClient.Delete(ctx, managedZone)).To(Succeed()) - - // confirm DNS Record and managed zone have not deleted - Consistently(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) - g.Expect(err).To(BeNil()) - - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(err).To(BeNil()) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - - // delete the DNS Record - Expect(k8sClient.Delete(ctx, dnsRecord)).To(Succeed()) - - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) - g.Expect(errors.IsNotFound(err)).To(BeTrue()) - - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone) - g.Expect(errors.IsNotFound(err)).To(BeTrue()) - }, TestTimeoutMedium, time.Second).Should(Succeed()) - }) - }) -}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 3561f95a..adc88ad2 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -100,13 +100,6 @@ var _ = BeforeSuite(func() { providerFactory, err := provider.NewFactory(mgr.GetClient(), []string{"inmemory"}) Expect(err).ToNot(HaveOccurred()) - err = (&ManagedZoneReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ProviderFactory: providerFactory, - }).SetupWithManager(mgr) - Expect(err).ToNot(HaveOccurred()) - err = (&DNSRecordReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/internal/external-dns/plan/plan.go b/internal/external-dns/plan/plan.go index 0d9c55eb..5207039f 100644 --- a/internal/external-dns/plan/plan.go +++ b/internal/external-dns/plan/plan.go @@ -76,8 +76,9 @@ type Plan struct { // NewPlan returns new Plan object func NewPlan(ctx context.Context, current []*endpoint.Endpoint, previous []*endpoint.Endpoint, desired []*endpoint.Endpoint, policies []Policy, domainFilter endpoint.MatchAllDomainFilters, managedRecords []string, excludeRecords []string, ownerID string, rootHost *string) *Plan { - logger := logr.FromContextOrDiscard(ctx) - logger.WithName("plan").V(1).Info("initializing plan", "ownerID", ownerID, "rooHost", rootHost, "policies", policies, "domainFilter", domainFilter) + logger := logr.FromContextOrDiscard(ctx). + WithName("plan") + logger.V(1).Info("initializing plan", "ownerID", ownerID, "rooHost", rootHost, "policies", policies, "domainFilter", domainFilter) return &Plan{ Current: current, diff --git a/internal/external-dns/provider/inmemory/inmemory.go b/internal/external-dns/provider/inmemory/inmemory.go index c1b2105a..732b1d48 100644 --- a/internal/external-dns/provider/inmemory/inmemory.go +++ b/internal/external-dns/provider/inmemory/inmemory.go @@ -90,8 +90,8 @@ func InMemoryWithDomain(domainFilter endpoint.DomainFilter) InMemoryOption { func InMemoryInitZones(zones []string) InMemoryOption { return func(p *InMemoryProvider) { for _, z := range zones { - if err := p.CreateZone(z); err != nil { - p.logger.Info("Unable to initialize zones for inmemory provider") + if err := p.CreateZone(z); err != nil && !errors.Is(err, ErrZoneAlreadyExists) { + p.logger.Error(err, "Unable to initialize zone for inmemory provider", "zone", z) } } } @@ -140,7 +140,17 @@ func (im *InMemoryProvider) GetZone(zone string) (Zone, error) { // Zones returns filtered zones as specified by domain func (im *InMemoryProvider) Zones() map[string]string { - return im.filter.Zones(im.client.Zones()) + zones := make(map[string]string) + for _, zone := range im.client.Zones() { + //ToDo Add id filtering here + if im.domain.Match(zone) { + zones[zone] = zone + im.logger.V(1).Info(fmt.Sprintf("Matched zone %s", zone)) + } else { + im.logger.V(1).Info(fmt.Sprintf("Filtered zone %s", zone)) + } + } + return zones } // Records returns the list of endpoints diff --git a/internal/external-dns/provider/inmemory/inmemory_test.go b/internal/external-dns/provider/inmemory/inmemory_test.go index 9a801afd..42e33948 100644 --- a/internal/external-dns/provider/inmemory/inmemory_test.go +++ b/internal/external-dns/provider/inmemory/inmemory_test.go @@ -99,6 +99,7 @@ func testInMemoryRecords(t *testing.T) { c.zones = ti.init im := NewInMemoryProvider(context.Background()) im.client = c + im.domain = endpoint.NewDomainFilter([]string{ti.zone}) f := filter{domain: ti.zone} im.filter = &f records, err := im.Records(context.Background()) diff --git a/internal/external-dns/registry/txt.go b/internal/external-dns/registry/txt.go index ebc48bca..ba788d4a 100644 --- a/internal/external-dns/registry/txt.go +++ b/internal/external-dns/registry/txt.go @@ -62,8 +62,9 @@ type TXTRegistry struct { // NewTXTRegistry returns new TXTRegistry object func NewTXTRegistry(ctx context.Context, provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) { - logger := logr.FromContextOrDiscard(ctx) - logger.WithName("registry").V(1).Info("initializing TXT registry", "ownerID", ownerID, "txtPrefix", txtPrefix, "txtSuffix", txtSuffix) + logger := logr.FromContextOrDiscard(ctx). + WithName("registry") + logger.V(1).Info("initializing TXT registry", "ownerID", ownerID, "txtPrefix", txtPrefix, "txtSuffix", txtSuffix) if ownerID == "" { return nil, errors.New("owner id cannot be empty") diff --git a/internal/provider/aws/aws.go b/internal/provider/aws/aws.go index 88800388..5beb06b3 100644 --- a/internal/provider/aws/aws.go +++ b/internal/provider/aws/aws.go @@ -65,18 +65,18 @@ func NewProviderFromSecret(ctx context.Context, s *v1.Secret, c provider.Config) sessionOpts := session.Options{ Config: *config, } - if string(s.Data["AWS_ACCESS_KEY_ID"]) == "" || string(s.Data["AWS_SECRET_ACCESS_KEY"]) == "" { + if string(s.Data[v1alpha1.AWSAccessKeyIDKey]) == "" || string(s.Data[v1alpha1.AWSSecretAccessKeyKey]) == "" { return nil, fmt.Errorf("AWS Provider credentials is empty") } - sessionOpts.Config.Credentials = credentials.NewStaticCredentials(string(s.Data["AWS_ACCESS_KEY_ID"]), string(s.Data["AWS_SECRET_ACCESS_KEY"]), "") + sessionOpts.Config.Credentials = credentials.NewStaticCredentials(string(s.Data[v1alpha1.AWSAccessKeyIDKey]), string(s.Data[v1alpha1.AWSSecretAccessKeyKey]), "") sessionOpts.SharedConfigState = session.SharedConfigDisable sess, err := session.NewSessionWithOptions(sessionOpts) if err != nil { return nil, fmt.Errorf("unable to create aws session: %s", err) } - if string(s.Data["REGION"]) != "" { - sess.Config.WithRegion(string(s.Data["REGION"])) + if string(s.Data[v1alpha1.AWSRegionKey]) != "" { + sess.Config.WithRegion(string(s.Data[v1alpha1.AWSRegionKey])) } route53Client := route53.New(sess, config) @@ -139,114 +139,29 @@ func (p *Route53DNSProvider) AdjustEndpoints(endpoints []*externaldnsendpoint.En // #### DNS Operator Provider #### -func (p *Route53DNSProvider) EnsureManagedZone(_ context.Context, zone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { - var zoneID string - if zone.Spec.ID != "" { - zoneID = zone.Spec.ID - } else { - zoneID = zone.Status.ID - } - - var managedZoneOutput provider.ManagedZoneOutput - - if zoneID != "" { - getResp, err := p.route53Client.GetHostedZone(&route53.GetHostedZoneInput{ - Id: &zoneID, - }) - if err != nil { - p.logger.Error(err, "failed to get hosted zone") - return managedZoneOutput, err - } - - if getResp.HostedZone == nil { - err = fmt.Errorf("aws zone issue. No hosted zone info in response") - p.logger.Error(err, "unexpected response") - return managedZoneOutput, err - } - if getResp.HostedZone.Id == nil { - err = fmt.Errorf("aws zone issue. No hosted zone id in response") - return managedZoneOutput, err - } - if getResp.HostedZone.Name == nil { - err = fmt.Errorf("aws zone issue. No hosted zone name in response") - return managedZoneOutput, err - } - - _, err = p.route53Client.UpdateHostedZoneComment(&route53.UpdateHostedZoneCommentInput{ - Comment: &zone.Spec.Description, - Id: &zoneID, - }) - if err != nil { - p.logger.Error(err, "failed to update hosted zone comment") - } - - managedZoneOutput.ID = *getResp.HostedZone.Id - managedZoneOutput.DNSName = strings.ToLower(strings.TrimSuffix(*getResp.HostedZone.Name, ".")) - - if getResp.HostedZone.ResourceRecordSetCount != nil { - managedZoneOutput.RecordCount = *getResp.HostedZone.ResourceRecordSetCount - } - - managedZoneOutput.NameServers = []*string{} - if getResp.DelegationSet != nil { - managedZoneOutput.NameServers = getResp.DelegationSet.NameServers - } - - return managedZoneOutput, nil - } - //ToDo callerRef must be unique, but this can cause duplicates if the status can't be written back during a - //reconciliation that successfully created a new hosted zone i.e. the object has been modified; please apply your - //changes to the latest version and try again - callerRef := time.Now().Format("20060102150405") - // Create the hosted zone - createResp, err := p.route53Client.CreateHostedZone(&route53.CreateHostedZoneInput{ - CallerReference: &callerRef, - Name: &zone.Spec.DomainName, - HostedZoneConfig: &route53.HostedZoneConfig{ - Comment: &zone.Spec.Description, - PrivateZone: aws.Bool(false), - }, - }) +func (p *Route53DNSProvider) DNSZones(ctx context.Context) ([]provider.DNSZone, error) { + var hzs []provider.DNSZone + zones, err := p.Zones(ctx) if err != nil { - p.logger.Error(err, "failed to create hosted zone") - return managedZoneOutput, err - } - if createResp.HostedZone == nil { - err = fmt.Errorf("aws zone creation issue. No hosted zone info in response") - p.logger.Error(err, "unexpected response") - return managedZoneOutput, err - } - if createResp.HostedZone.Id == nil { - err = fmt.Errorf("aws zone creation issue. No hosted zone id in response") - return managedZoneOutput, err - } - if createResp.HostedZone.Name == nil { - err = fmt.Errorf("aws zone creation issue. No hosted zone name in response") - return managedZoneOutput, err - } - - managedZoneOutput.ID = *createResp.HostedZone.Id - managedZoneOutput.DNSName = strings.ToLower(strings.TrimSuffix(*createResp.HostedZone.Name, ".")) - - if createResp.HostedZone.ResourceRecordSetCount != nil { - managedZoneOutput.RecordCount = *createResp.HostedZone.ResourceRecordSetCount + return nil, err } - if createResp.DelegationSet != nil { - managedZoneOutput.NameServers = createResp.DelegationSet.NameServers + for _, z := range zones { + hz := provider.DNSZone{ + ID: *z.Id, + DNSName: strings.ToLower(strings.TrimSuffix(*z.Name, ".")), + } + hzs = append(hzs, hz) } - return managedZoneOutput, nil + return hzs, nil } -func (p *Route53DNSProvider) DeleteManagedZone(zone *v1alpha1.ManagedZone) error { - _, err := p.route53Client.DeleteHostedZone(&route53.DeleteHostedZoneInput{ - Id: &zone.Status.ID, - }) +func (p *Route53DNSProvider) DNSZoneForHost(ctx context.Context, host string) (*provider.DNSZone, error) { + zones, err := p.DNSZones(ctx) if err != nil { - p.logger.Error(err, "failed to delete hosted zone") - return err + return nil, err } - return nil + return provider.FindDNSZoneForHost(ctx, host, zones) } func (p *Route53DNSProvider) HealthCheckReconciler() provider.HealthCheckReconciler { diff --git a/internal/provider/azure/azure.go b/internal/provider/azure/azure.go index 052e638b..46cc073a 100644 --- a/internal/provider/azure/azure.go +++ b/internal/provider/azure/azure.go @@ -24,11 +24,11 @@ type AzureProvider struct { var _ provider.Provider = &AzureProvider{} func NewAzureProviderFromSecret(ctx context.Context, s *v1.Secret, c provider.Config) (provider.Provider, error) { - if string(s.Data["azure.json"]) == "" { + if string(s.Data[v1alpha1.AzureJsonKey]) == "" { return nil, fmt.Errorf("the Azure provider credentials is empty") } - configString := string(s.Data["azure.json"]) + configString := string(s.Data[v1alpha1.AzureJsonKey]) var azureConfig externaldnsproviderazure.Config err := yaml.Unmarshal([]byte(configString), &azureConfig) if err != nil { @@ -61,65 +61,40 @@ func NewAzureProviderFromSecret(ctx context.Context, s *v1.Secret, c provider.Co } -// 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 +func (p *AzureProvider) DNSZones(ctx context.Context) ([]provider.DNSZone, error) { + var hzs []provider.DNSZone + zones, err := p.Zones(ctx) + if err != nil { + return nil, err } - if zoneID != "" { - //Get existing managed zone - return p.getManagedZone(ctx, zoneID) + for _, z := range zones { + hz := provider.DNSZone{ + ID: *z.ID, + DNSName: *z.Name, + } + hzs = append(hzs, hz) } - //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() + return hzs, nil } -func (p *AzureProvider) getManagedZone(ctx context.Context, zoneID string) (provider.ManagedZoneOutput, error) { - logger := crlog.FromContext(ctx).WithName("getManagedZone") - zones, err := p.Zones(ctx) +func (p *AzureProvider) DNSZoneForHost(ctx context.Context, host string) (*provider.DNSZone, error) { + zones, err := p.DNSZones(ctx) if err != nil { - return provider.ManagedZoneOutput{}, err + return nil, err } + return provider.FindDNSZoneForHost(ctx, host, zones) +} - 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 - } - } +func (p *AzureProvider) HealthCheckReconciler() provider.HealthCheckReconciler { + return NewAzureHealthCheckReconciler() +} - return provider.ManagedZoneOutput{}, fmt.Errorf("zone %s not found", zoneID) +func (p *AzureProvider) ProviderSpecific() provider.ProviderSpecificLabels { + return provider.ProviderSpecificLabels{} } -// createManagedZone not implemented as managed zones are going away -func (p *AzureProvider) createManagedZone(_ context.Context, _ *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { - return provider.ManagedZoneOutput{}, nil +// Register this Provider with the provider factory +func init() { + provider.RegisterProvider("azure", NewAzureProviderFromSecret, false) } diff --git a/internal/provider/factory.go b/internal/provider/factory.go index c0270fcc..c0f4c57f 100644 --- a/internal/provider/factory.go +++ b/internal/provider/factory.go @@ -12,6 +12,7 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "github.com/kuadrant/dns-operator/api/v1alpha1" ) @@ -74,6 +75,7 @@ func NewFactory(c client.Client, p []string) (Factory, error) { // ProviderFor will return a Provider interface for the given ProviderAccessor secret. // If the requested ProviderAccessor secret does not exist, an error will be returned. func (f *factory) ProviderFor(ctx context.Context, pa v1alpha1.ProviderAccessor, c Config) (Provider, error) { + logger := log.FromContext(ctx) providerSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: pa.GetProviderRef().Name, @@ -95,6 +97,7 @@ func (f *factory) ProviderFor(ctx context.Context, pa v1alpha1.ProviderAccessor, if !slices.Contains(f.providers, provider) { return nil, fmt.Errorf("provider '%s' not enabled", provider) } + logger.V(1).Info(fmt.Sprintf("initializing %s provider with config", provider), "config", c) return constructor(ctx, providerSecret, c) } @@ -103,13 +106,13 @@ func (f *factory) ProviderFor(ctx context.Context, pa v1alpha1.ProviderAccessor, func NameForProviderSecret(secret *v1.Secret) (string, error) { switch secret.Type { - case "kuadrant.io/aws": + case v1alpha1.SecretTypeKuadrantAWS: return "aws", nil - case "kuadrant.io/azure": + case v1alpha1.SecretTypeKuadrantAzure: return "azure", nil - case "kuadrant.io/gcp": + case v1alpha1.SecretTypeKuadrantGCP: return "google", nil - case "kuadrant.io/inmemory": + case v1alpha1.SecretTypeKuadrantInmemory: return "inmemory", nil } return "", errUnsupportedProvider diff --git a/internal/provider/google/google.go b/internal/provider/google/google.go index 8c7a48a6..5049aa8a 100644 --- a/internal/provider/google/google.go +++ b/internal/provider/google/google.go @@ -31,7 +31,6 @@ import ( corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/log" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" - externaldnsprovider "sigs.k8s.io/external-dns/provider" "github.com/kuadrant/dns-operator/api/v1alpha1" externaldnsgoogle "github.com/kuadrant/dns-operator/internal/external-dns/provider/google" @@ -117,17 +116,16 @@ type GoogleDNSProvider struct { var _ provider.Provider = &GoogleDNSProvider{} func NewProviderFromSecret(ctx context.Context, s *corev1.Secret, c provider.Config) (provider.Provider, error) { - - if string(s.Data["GOOGLE"]) == "" || string(s.Data["PROJECT_ID"]) == "" { + if string(s.Data[v1alpha1.GoogleJsonKey]) == "" || string(s.Data[v1alpha1.GoogleProjectIDKey]) == "" { return nil, fmt.Errorf("GCP Provider credentials is empty") } - dnsClient, err := dnsv1.NewService(ctx, option.WithCredentialsJSON(s.Data["GOOGLE"])) + dnsClient, err := dnsv1.NewService(ctx, option.WithCredentialsJSON(s.Data[v1alpha1.GoogleJsonKey])) if err != nil { return nil, err } - project := string(s.Data["PROJECT_ID"]) + project := string(s.Data[v1alpha1.GoogleProjectIDKey]) googleConfig := externaldnsgoogle.GoogleConfig{ Project: project, @@ -289,26 +287,29 @@ func endpointsToGoogleFormat(eps []*externaldnsendpoint.Endpoint) []*externaldns // #### DNS Operator Provider #### -func (p *GoogleDNSProvider) EnsureManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { - p.logger.V(1).Info("EnsureManagedZone") - var zoneID string - - if managedZone.Spec.ID != "" { - zoneID = managedZone.Spec.ID - } else { - zoneID = managedZone.Status.ID +func (p *GoogleDNSProvider) DNSZones(ctx context.Context) ([]provider.DNSZone, error) { + var hzs []provider.DNSZone + zones, err := p.Zones(ctx) + if err != nil { + return nil, err } - if zoneID != "" { - //Get existing managed zone - return p.getManagedZone(ctx, zoneID) + for _, z := range zones { + hz := provider.DNSZone{ + ID: z.Name, + DNSName: strings.ToLower(strings.TrimSuffix(z.DnsName, ".")), + } + hzs = append(hzs, hz) } - //Create new managed zone - return p.createManagedZone(ctx, managedZone) + return hzs, nil } -func (p *GoogleDNSProvider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { - return p.managedZonesClient.Delete(p.googleConfig.Project, managedZone.Status.ID).Do() +func (p *GoogleDNSProvider) DNSZoneForHost(ctx context.Context, host string) (*provider.DNSZone, error) { + zones, err := p.DNSZones(ctx) + if err != nil { + return nil, err + } + return provider.FindDNSZoneForHost(ctx, host, zones) } func (p *GoogleDNSProvider) HealthCheckReconciler() provider.HealthCheckReconciler { @@ -319,66 +320,6 @@ func (p *GoogleDNSProvider) ProviderSpecific() provider.ProviderSpecificLabels { return provider.ProviderSpecificLabels{} } -func (p *GoogleDNSProvider) createManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { - zoneID := strings.Replace(managedZone.Spec.DomainName, ".", "-", -1) - zone := dnsv1.ManagedZone{ - Name: zoneID, - DnsName: externaldnsprovider.EnsureTrailingDot(managedZone.Spec.DomainName), - Description: managedZone.Spec.Description, - } - mz, err := p.managedZonesClient.Create(p.googleConfig.Project, &zone).Do() - if err != nil { - return provider.ManagedZoneOutput{}, err - } - return p.toManagedZoneOutput(ctx, mz) -} - -func (p *GoogleDNSProvider) getManagedZone(ctx context.Context, zoneID string) (provider.ManagedZoneOutput, error) { - mz, err := p.managedZonesClient.Get(p.googleConfig.Project, zoneID).Do() - if err != nil { - return provider.ManagedZoneOutput{}, err - } - return p.toManagedZoneOutput(ctx, mz) -} - -func (p *GoogleDNSProvider) toManagedZoneOutput(ctx context.Context, mz *dnsv1.ManagedZone) (provider.ManagedZoneOutput, error) { - var managedZoneOutput provider.ManagedZoneOutput - - zoneID := mz.Name - var nameservers []*string - for i := range mz.NameServers { - nameservers = append(nameservers, &mz.NameServers[i]) - } - managedZoneOutput.ID = zoneID - managedZoneOutput.DNSName = strings.ToLower(strings.TrimSuffix(mz.DnsName, ".")) - managedZoneOutput.NameServers = nameservers - - currentRecords, err := p.getResourceRecordSets(ctx, zoneID) - if err != nil { - return managedZoneOutput, err - } - managedZoneOutput.RecordCount = int64(len(currentRecords)) - - return managedZoneOutput, nil -} - -// ToDo Can be replaced with a call to Records if/when we update that to optionally accept a zone id -// getResourceRecordSets returns the records for a managed zone of the currently configured provider. -func (p *GoogleDNSProvider) getResourceRecordSets(ctx context.Context, zoneID string) ([]*dnsv1.ResourceRecordSet, error) { - var records []*dnsv1.ResourceRecordSet - - f := func(resp *dnsv1.ResourceRecordSetsListResponse) error { - records = append(records, resp.Rrsets...) - return nil - } - - if err := p.resourceRecordSetsClient.List(p.googleConfig.Project, zoneID).Pages(ctx, f); err != nil { - return nil, err - } - - return records, nil -} - // Register this Provider with the provider factory func init() { provider.RegisterProvider("google", NewProviderFromSecret, true) diff --git a/internal/provider/google/google_test.go b/internal/provider/google/google_test.go index 5644f2a7..137feade 100644 --- a/internal/provider/google/google_test.go +++ b/internal/provider/google/google_test.go @@ -4,134 +4,15 @@ package google import ( "context" - "fmt" "sort" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/google/go-cmp/cmp" dnsv1 "google.golang.org/api/dns/v1" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" - - "github.com/kuadrant/dns-operator/internal/provider" ) -func TestGoogleDNSProvider_toManagedZoneOutput(t *testing.T) { - mockListCall := &MockResourceRecordSetsListCall{ - PagesFunc: func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error { - mockResponse := &dnsv1.ResourceRecordSetsListResponse{ - Rrsets: []*dnsv1.ResourceRecordSet{ - { - Name: "TestRecordSet1", - }, - { - Name: "TestRecordSet2", - }, - }, - } - return f(mockResponse) - }, - } - mockClient := &MockResourceRecordSetsClient{ - ListFunc: func(project string, managedZone string) resourceRecordSetsListCallInterface { - return mockListCall - }, - } - - mockListCallErr := &MockResourceRecordSetsListCall{ - PagesFunc: func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error { - - error := fmt.Errorf("status 400 ") - return error - }, - } - mockClientErr := &MockResourceRecordSetsClient{ - ListFunc: func(project string, managedZone string) resourceRecordSetsListCallInterface { - return mockListCallErr - }, - } - - type fields struct { - resourceRecordSetsClient resourceRecordSetsClientInterface - } - type args struct { - mz *dnsv1.ManagedZone - } - tests := []struct { - name string - fields fields - args args - want provider.ManagedZoneOutput - wantErr bool - }{ - - { - name: "Successful test", - fields: fields{ - resourceRecordSetsClient: mockClient, - }, - args: args{ - &dnsv1.ManagedZone{ - Name: "testname", - NameServers: []string{ - "nameserver1", - "nameserver2", - }, - }, - }, - want: provider.ManagedZoneOutput{ - ID: "testname", - NameServers: []*string{ - aws.String("nameserver1"), - aws.String("nameserver2"), - }, - RecordCount: 2, - }, - wantErr: false, - }, - { - name: "UnSuccessful test", - fields: fields{ - resourceRecordSetsClient: mockClientErr, - }, - args: args{ - &dnsv1.ManagedZone{ - Name: "testname", - NameServers: []string{ - "nameserver1", - "nameserver2", - }, - }, - }, - want: provider.ManagedZoneOutput{ - ID: "testname", - NameServers: []*string{ - aws.String("nameserver1"), - aws.String("nameserver2"), - }, - RecordCount: 0, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := &GoogleDNSProvider{ - resourceRecordSetsClient: tt.fields.resourceRecordSetsClient, - } - got, err := g.toManagedZoneOutput(context.Background(), tt.args.mz) - if (err != nil) != tt.wantErr { - t.Errorf("GoogleDNSProvider.toManagedZoneOutput() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("GoogleDNSProvider.toManagedZoneOutput() (-want +got):\n%s", diff) - } - }) - } -} - type MockResourceRecordSetsListCall struct { PagesFunc func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error } diff --git a/internal/provider/inmemory/inmemory.go b/internal/provider/inmemory/inmemory.go index 1b6b2905..163b0f8e 100644 --- a/internal/provider/inmemory/inmemory.go +++ b/internal/provider/inmemory/inmemory.go @@ -18,6 +18,7 @@ package inmemory import ( "context" + "strings" v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/log" @@ -35,62 +36,65 @@ var client *inmemory.InMemoryClient var _ provider.Provider = &InMemoryDNSProvider{} -func NewProviderFromSecret(ctx context.Context, _ *v1.Secret, c provider.Config) (provider.Provider, error) { +func NewProviderFromSecret(ctx context.Context, s *v1.Secret, c provider.Config) (provider.Provider, error) { logger := log.FromContext(ctx).WithName("inmemory-dns") ctx = log.IntoContext(ctx, logger) + initZones := []string{} + if z := string(s.Data[v1alpha1.InmemInitZonesKey]); z != "" { + initZones = strings.Split(z, ",") + } + inmemoryProvider := inmemory.NewInMemoryProvider( ctx, inmemory.InMemoryWithClient(client), + inmemory.InMemoryInitZones(initZones), inmemory.InMemoryWithDomain(c.DomainFilter), inmemory.InMemoryWithLogging()) p := &InMemoryDNSProvider{ InMemoryProvider: inmemoryProvider, } - return p, nil -} -func (i InMemoryDNSProvider) EnsureManagedZone(_ context.Context, mz *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { - var zoneID string - if mz.Spec.ID != "" { - zoneID = mz.Spec.ID - } else { - zoneID = mz.Status.ID + availableZones := []string{} + z, err := p.DNSZones(ctx) + if err != nil { + return nil, err } - if zoneID != "" { - z, err := i.GetZone(zoneID) - if err != nil { - return provider.ManagedZoneOutput{}, err - } - return provider.ManagedZoneOutput{ - ID: zoneID, - DNSName: zoneID, - NameServers: nil, - RecordCount: int64(len(z)), - }, nil + for _, zone := range z { + availableZones = append(availableZones, zone.DNSName) } - err := i.CreateZone(mz.Spec.DomainName) - if err != nil { - return provider.ManagedZoneOutput{}, err + logger.V(1).Info("provider initialised", "availableZones", availableZones) + + return p, nil +} + +func (p *InMemoryDNSProvider) DNSZones(_ context.Context) ([]provider.DNSZone, error) { + var hzs []provider.DNSZone + zones := p.Zones() + for id, name := range zones { + hz := provider.DNSZone{ + ID: id, + DNSName: name, + } + hzs = append(hzs, hz) } - return provider.ManagedZoneOutput{ - ID: mz.Spec.DomainName, - DNSName: mz.Spec.DomainName, - NameServers: nil, - RecordCount: 0, - }, nil + return hzs, nil } -func (i InMemoryDNSProvider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { - return i.DeleteZone(managedZone.Spec.DomainName) +func (p *InMemoryDNSProvider) DNSZoneForHost(ctx context.Context, host string) (*provider.DNSZone, error) { + zones, err := p.DNSZones(ctx) + if err != nil { + return nil, err + } + return provider.FindDNSZoneForHost(ctx, host, zones) } -func (i InMemoryDNSProvider) HealthCheckReconciler() provider.HealthCheckReconciler { +func (i *InMemoryDNSProvider) HealthCheckReconciler() provider.HealthCheckReconciler { return &provider.FakeHealthCheckReconciler{} } -func (i InMemoryDNSProvider) ProviderSpecific() provider.ProviderSpecificLabels { +func (i *InMemoryDNSProvider) ProviderSpecific() provider.ProviderSpecificLabels { return provider.ProviderSpecificLabels{} } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 73d79523..d918012c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,32 +3,37 @@ package provider import ( "context" "errors" + "fmt" "regexp" + "slices" "strings" + "golang.org/x/net/publicsuffix" + + "sigs.k8s.io/controller-runtime/pkg/log" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" externaldnsprovider "sigs.k8s.io/external-dns/provider" - - "github.com/kuadrant/dns-operator/api/v1alpha1" ) var ( statusCodeRegexp = regexp.MustCompile(`status code: [^\s]+`) requestIDRegexp = regexp.MustCompile(`request id: [^\s]+`) saxParseExceptionRegexp = regexp.MustCompile(`Invalid XML ; javax.xml.stream.XMLStreamException: org.xml.sax.SAXParseException; lineNumber: [^\s]+; columnNumber: [^\s]+`) + + ErrNoZoneForHost = fmt.Errorf("no zone for host") ) // Provider knows how to manage DNS zones only as pertains to routing. type Provider interface { externaldnsprovider.Provider - // Ensure will create or update a managed zone, returns an array of NameServers for that zone. - EnsureManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (ManagedZoneOutput, error) + // DNSZones returns a list of dns zones accessible for this provider + DNSZones(ctx context.Context) ([]DNSZone, error) - // Delete will delete a managed zone. - DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error + // DNSZoneForHost returns the DNSZone that best matches the given host in the providers list of zones + DNSZoneForHost(ctx context.Context, host string) (*DNSZone, error) - // Get an instance of HealthCheckReconciler for this provider + // HealthCheckReconciler Get an instance of HealthCheckReconciler for this provider HealthCheckReconciler() HealthCheckReconciler ProviderSpecific() ProviderSpecificLabels @@ -48,7 +53,7 @@ type ProviderSpecificLabels struct { HealthCheckID string } -type ManagedZoneOutput struct { +type DNSZone struct { ID string DNSName string NameServers []*string @@ -66,3 +71,48 @@ func SanitizeError(err error) error { sanitizedErr = strings.TrimSpace(sanitizedErr) return errors.New(sanitizedErr) } + +// FindDNSZoneForHost finds the most suitable zone for the given host in the given list of DNSZones +func FindDNSZoneForHost(ctx context.Context, host string, zones []DNSZone) (*DNSZone, error) { + log.FromContext(ctx).V(1).Info(fmt.Sprintf("finding most suitable zone for %s from %v possible zones %v", host, len(zones), zones)) + z, _, err := findDNSZoneForHost(host, host, zones) + return z, err +} + +func findDNSZoneForHost(originalHost, host string, zones []DNSZone) (*DNSZone, string, error) { + if len(zones) == 0 { + return nil, "", fmt.Errorf("%w : %s", ErrNoZoneForHost, host) + } + host = strings.ToLower(host) + //get the TLD from this host + tld, _ := publicsuffix.PublicSuffix(host) + + //The host is a TLD, so we now know `originalHost` can't possibly have a valid `DNSZone` available. + if host == tld { + return nil, "", fmt.Errorf("no valid zone found for host: %v", originalHost) + } + + hostParts := strings.SplitN(host, ".", 2) + if len(hostParts) < 2 { + return nil, "", fmt.Errorf("no valid zone found for host: %s", originalHost) + } + parentDomain := hostParts[1] + + // We do not currently support creating records for Apex domains, and a DNSZone represents an Apex domain, as such + // we should never be trying to find a zone that matches the `originalHost` exactly. Instead, we just continue + // on to the next possible valid host to try i.e. the parent domain. + if host == originalHost { + return findDNSZoneForHost(originalHost, parentDomain, zones) + } + + idx := slices.IndexFunc(zones, func(zone DNSZone) bool { + return strings.ToLower(zone.DNSName) == host + }) + + if idx != -1 { + zone := zones[idx] + subdomain := strings.Replace(strings.ToLower(originalHost), "."+strings.ToLower(zone.DNSName), "", 1) + return &zone, subdomain, nil + } + return findDNSZoneForHost(originalHost, parentDomain, zones) +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index f7cd4f69..00ef521d 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -48,3 +48,144 @@ func TestSanitizeError(t *testing.T) { }) } } + +func Test_findDNSZoneForHost(t *testing.T) { + testCases := []struct { + name string + host string + zones []DNSZone + want string + want1 string + wantErr bool + }{ + { + name: "no zones", + host: "sub.domain.test.example.com", + zones: []DNSZone{}, + want: "", + want1: "", + wantErr: true, + }, + { + name: "single zone with match", + host: "sub.domain.test.example.com", + zones: []DNSZone{ + { + DNSName: "example.com", + }, + }, + want: "example.com", + want1: "sub.domain.test", + wantErr: false, + }, + { + name: "does not match exact dns name", + host: "sub.domain.test.example.com", + zones: []DNSZone{ + { + DNSName: "sub.domain.test.example.com", + }, + }, + want: "", + want1: "", + wantErr: true, + }, + { + name: "multiple zones that all match", + host: "sub.domain.test.example.com", + zones: []DNSZone{ + { + DNSName: "example.com", + }, + { + DNSName: "test.example.com", + }, + { + DNSName: "domain.test.example.com", + }, + }, + want: "domain.test.example.com", + want1: "sub", + wantErr: false, + }, + { + name: "multiple zones some match", + host: "sub.domain.test.example.com", + zones: []DNSZone{ + { + DNSName: "example.com", + }, + { + DNSName: "test.example.com", + }, + { + DNSName: "test.otherdomain.com", + }, + }, + want: "test.example.com", + want1: "sub.domain", + wantErr: false, + }, + { + name: "multiple zones no match", + host: "sub.domain.test.example2.com", + zones: []DNSZone{ + { + DNSName: "example.com", + }, + { + DNSName: "test.example.com", + }, + { + DNSName: "test.otherdomain.com", + }, + }, + want: "", + want1: "", + wantErr: true, + }, + { + name: "handles tld with a dot", + host: "sub.domain.test.example.co.uk", + zones: []DNSZone{ + { + DNSName: "example.co.uk", + }, + }, + want: "example.co.uk", + want1: "sub.domain.test", + wantErr: false, + }, + { + name: "tld with a dot will not match against a zone of the tld", + host: "sub.domain.test.example.co.uk", + zones: []DNSZone{ + { + DNSName: "co.uk", + }, + }, + want: "", + want1: "", + wantErr: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := findDNSZoneForHost(tt.host, tt.host, tt.zones) + if (err != nil) != tt.wantErr { + t.Errorf("findDNSZoneForHost() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil { + if tt.want != "" { + t.Errorf("findDNSZoneForHost() got = unexpetd nil value") + } + } else if got.DNSName != tt.want { + t.Errorf("findDNSZoneForHost() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("findDNSZoneForHost() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/make/dnsproviders.mk b/make/dnsproviders.mk new file mode 100644 index 00000000..124488ed --- /dev/null +++ b/make/dnsproviders.mk @@ -0,0 +1,80 @@ + +##@ DNS Providers + +## Targets to help configure DNS Providers for local-setup + +define patch-config + envsubst \ + < $1 \ + > $2 +endef + +ndef = $(if $(value $(1)),,$(error $(1) not set)) + +LOCAL_SETUP_AWS_DIR=config/local-setup/dns-provider/aws +LOCAL_SETUP_GCP_DIR=config/local-setup/dns-provider/gcp +LOCAL_SETUP_AZURE_DIR=config/local-setup/dns-provider/azure +LOCAL_SETUP_INMEM_DIR=config/local-setup/dns-provider/inmemory +LOCAL_SETUP_AWS_CREDS=${LOCAL_SETUP_AWS_DIR}/aws-credentials.env +LOCAL_SETUP_GCP_CREDS=${LOCAL_SETUP_GCP_DIR}/gcp-credentials.env +LOCAL_SETUP_AZURE_CREDS=${LOCAL_SETUP_AZURE_DIR}/azure-credentials.env + +.PHONY: local-setup-aws-generate +local-setup-aws-generate: local-setup-aws-credentials ## Generate AWS DNS Provider credentials for local-setup + +.PHONY: local-setup-aws-clean +local-setup-aws-clean: ## Remove AWS DNS Provider credentials + rm -f ${LOCAL_SETUP_AWS_CREDS} + +.PHONY: local-setup-aws-credentials +local-setup-aws-credentials: $(LOCAL_SETUP_AWS_CREDS) +$(LOCAL_SETUP_AWS_CREDS): + $(call ndef,AWS_ACCESS_KEY_ID) + $(call ndef,AWS_SECRET_ACCESS_KEY) + $(call patch-config,${LOCAL_SETUP_AWS_CREDS}.template,${LOCAL_SETUP_AWS_CREDS}) + +.PHONY: local-setup-gcp-generate +local-setup-gcp-generate: local-setup-gcp-credentials ## Generate GCP DNS Provider credentials for local-setup + +.PHONY: local-setup-gcp-clean +local-setup-gcp-clean: ## Remove GCP DNS Provider credentials + rm -f ${LOCAL_SETUP_GCP_CREDS} + +.PHONY: local-setup-gcp-credentials +local-setup-gcp-credentials: $(LOCAL_SETUP_GCP_CREDS) +$(LOCAL_SETUP_GCP_CREDS): + $(call ndef,GCP_GOOGLE_CREDENTIALS) + $(call ndef,GCP_PROJECT_ID) + $(call patch-config,${LOCAL_SETUP_GCP_CREDS}.template,${LOCAL_SETUP_GCP_CREDS}) + + +.PHONY: local-setup-azure-generate +local-setup-azure-generate: local-setup-azure-credentials ## Generate Azure DNS Provider credentials for local-setup + +.PHONY: local-setup-azure-clean +local-setup-azure-clean: ## Remove Azure DNS Provider credentials + rm -f ${LOCAL_SETUP_AZURE_CREDS} + +.PHONY: local-setup-azure-credentials +local-setup-azure-credentials: $(LOCAL_SETUP_AZURE_CREDS) +$(LOCAL_SETUP_AZURE_CREDS): + $(call ndef,KUADRANT_AZURE_CREDENTIALS) + $(call patch-config,${LOCAL_SETUP_AZURE_CREDS}.template,${LOCAL_SETUP_AZURE_CREDS}) + +.PHONY: local-setup-dns-providers +local-setup-dns-providers: TARGET_NAMESPACE=dnstest +local-setup-dns-providers: kustomize ## Create AWS, Azure and GCP DNS Providers in the 'TARGET_NAMESPACE' namespace + @if [[ -f ${LOCAL_SETUP_GCP_CREDS} ]]; then\ + echo "local-setup: creating dns provider for gcp in ${TARGET_NAMESPACE}";\ + ${KUSTOMIZE} build ${LOCAL_SETUP_GCP_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + fi + @if [[ -f ${LOCAL_SETUP_AWS_CREDS} ]]; then\ + echo "local-setup: creating dns provider for aws in ${TARGET_NAMESPACE}";\ + ${KUSTOMIZE} build ${LOCAL_SETUP_AWS_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + fi + @if [[ -f ${LOCAL_SETUP_AZURE_CREDS} ]]; then\ + echo "local-setup: creating dns provider for azure in ${TARGET_NAMESPACE}";\ + ${KUSTOMIZE} build ${LOCAL_SETUP_AZURE_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ + fi + echo "local-setup: creating dns provider for inmemory in ${TARGET_NAMESPACE}";\ + ${KUSTOMIZE} build ${LOCAL_SETUP_INMEM_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f - diff --git a/make/kustomize_overlays.mk b/make/kustomize_overlays.mk index 07c67415..448c4da1 100644 --- a/make/kustomize_overlays.mk +++ b/make/kustomize_overlays.mk @@ -40,26 +40,26 @@ generate-operator-deployment-overlay: ## Generate a DNS Operator deployment over $(KUSTOMIZE) edit set namesuffix -- -$(DEPLOYMENT_NAME_SUFFIX) && \ $(KUSTOMIZE) edit add patch --kind Deployment --patch '[{"op": "replace", "path": "/spec/template/spec/containers/0/env/0", "value": {"name": "WATCH_NAMESPACES", "value": "$(DEPLOYMENT_WATCH_NAMESPACES)"}}]' - # Generate managedzones overlay - mkdir -p $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE)/managedzones - cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE)/managedzones && \ - touch kustomization.yaml - - @if [[ -f "config/local-setup/managedzone/gcp/managed-zone-config.env" && -f "config/local-setup/managedzone/gcp/gcp-credentials.env" ]]; then\ - cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE)/managedzones && \ - $(KUSTOMIZE) edit add resource "../../../../../config/local-setup/managedzone/gcp" ;\ - fi - @if [[ -f "config/local-setup/managedzone/aws/managed-zone-config.env" && -f "config/local-setup/managedzone/aws/aws-credentials.env" ]]; then\ - cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE)/managedzones && \ - $(KUSTOMIZE) edit add resource "../../../../../config/local-setup/managedzone/aws" ;\ - fi - - # Generate namespace overlay with dns-operator and managedzones resources + # Generate namespace overlay with dns-operator and dns provider resources cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE) && \ touch kustomization.yaml && \ $(KUSTOMIZE) edit set namespace $(DEPLOYMENT_NAMESPACE) && \ $(KUSTOMIZE) edit add resource "./dns-operator" && \ - $(KUSTOMIZE) edit add resource "./managedzones" + $(KUSTOMIZE) edit add resource "../../../../config/local-setup/dns-provider/inmemory" + + # Add dns providers + @if [[ -f "config/local-setup/dns-provider/gcp/gcp-credentials.env" ]]; then\ + cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE) && \ + $(KUSTOMIZE) edit add resource "../../../../config/local-setup/dns-provider/gcp" ;\ + fi + @if [[ -f "config/local-setup/dns-provider/aws/aws-credentials.env" ]]; then\ + cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE) && \ + $(KUSTOMIZE) edit add resource "../../../../config/local-setup/dns-provider/aws" ;\ + fi + @if [[ -f "config/local-setup/dns-provider/azure/azure-credentials.env" ]]; then\ + cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME)/namespace-$(DEPLOYMENT_NAMESPACE) && \ + $(KUSTOMIZE) edit add resource "../../../../config/local-setup/dns-provider/azure" ;\ + fi # Generate cluster overlay with namespace resources cd $(CLUSTER_OVERLAY_DIR)/$(CLUSTER_NAME) && \ diff --git a/make/managedzones.mk b/make/managedzones.mk deleted file mode 100644 index 04fd2387..00000000 --- a/make/managedzones.mk +++ /dev/null @@ -1,106 +0,0 @@ - -##@ 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_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 - -.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-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, 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 ${LOCAL_SETUP_GCP_MZ_DIR} | $(KUBECTL) -n ${TARGET_NAMESPACE} apply -f -;\ - fi - @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 ${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/pkg/builder/provider.go b/pkg/builder/provider.go new file mode 100644 index 00000000..5b89f6f4 --- /dev/null +++ b/pkg/builder/provider.go @@ -0,0 +1,77 @@ +/* +Copyright 2024. + +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 builder + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kuadrant/dns-operator/api/v1alpha1" +) + +// ProviderBuilder builds a Provider. +type ProviderBuilder struct { + name string + namespace string + secretType corev1.SecretType + strDataItems map[string]string +} + +// NewProviderBuilder returns a new provider builder with the name and namespace provided +func NewProviderBuilder(name, namespace string) *ProviderBuilder { + return &ProviderBuilder{name: name, namespace: namespace} +} + +// For defines the type of provider secret being created +func (pb *ProviderBuilder) For(secretType corev1.SecretType) *ProviderBuilder { + pb.secretType = secretType + return pb +} + +// WithDataItem adds key/values that will be included in the provider secret data. +// Defaults to an empty map. +func (pb *ProviderBuilder) WithDataItem(key, value string) *ProviderBuilder { + if pb.strDataItems == nil { + pb.strDataItems = map[string]string{} + } + pb.strDataItems[key] = value + return pb +} + +// WithZonesInitialisedFor sets the domains of zones to initialize in the provider. +// Only used by v1alpha1.SecretTypeKuadrantInmemory provider, ignored by all others. +// Defaults to the empty list. +func (pb *ProviderBuilder) WithZonesInitialisedFor(domains ...string) *ProviderBuilder { + if val, ok := pb.strDataItems[v1alpha1.InmemInitZonesKey]; ok { + domains = append(strings.Split(val, ","), domains...) + } + return pb.WithDataItem(v1alpha1.InmemInitZonesKey, strings.Join(domains, ",")) +} + +// Build builds and returns the provider secret. +func (pb *ProviderBuilder) Build() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pb.name, + Namespace: pb.namespace, + }, + StringData: pb.strDataItems, + Type: pb.secretType, + } +} diff --git a/test/e2e/README.md b/test/e2e/README.md index 068ac848..5976fb2c 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -19,26 +19,32 @@ Deploy the operator on a single kind cluster with two operator instances in two make local-setup DEPLOY=true DEPLOYMENT_SCOPE=namespace DEPLOYMENT_COUNT=2 ``` -The above will create two dns operator deployments on the kind cluster, each configured to watch its own namespace, with the development managedzones (Assuming you have configured them locally) created in each deployment namespace. +The above will create two dns operator deployments on the kind cluster, each configured to watch its own namespace, with the development provider secrets (Assuming you have configured them locally) created in each deployment namespace. DNS Operator Deployments: ```shell kubectl get deployments -l app.kubernetes.io/part-of=dns-operator -A NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE -dns-operator-1 dns-operator-controller-manager-1 1/1 1 1 96s -dns-operator-2 dns-operator-controller-manager-2 1/1 1 1 96s +dns-operator-1 dns-operator-controller-manager-1 1/1 1 1 21s +dns-operator-2 dns-operator-controller-manager-2 1/1 1 1 21s ``` -ManagedZones: +DNS Provider Secrets: ```shell -kubectl get managedzones -A -NAMESPACE NAME DOMAIN NAME ... READY -dns-operator-1 dev-mz-aws mn.hcpapps.net ... True -dns-operator-1 dev-mz-gcp mn.google.hcpapps.net ... True -dns-operator-2 dev-mz-aws mn.hcpapps.net ... True -dns-operator-2 dev-mz-gcp mn.google.hcpapps.net ... True -dnstest dev-mz-aws mn.hcpapps.net -dnstest dev-mz-gcp mn.google.hcpapps.net +kubectl get secrets -l app.kubernetes.io/part-of=dns-operator -A +NAMESPACE NAME TYPE DATA AGE +dns-operator-1 dns-provider-credentials-aws kuadrant.io/aws 5 21s +dns-operator-1 dns-provider-credentials-azure kuadrant.io/azure 3 21s +dns-operator-1 dns-provider-credentials-gcp kuadrant.io/gcp 4 21s +dns-operator-1 dns-provider-credentials-inmemory kuadrant.io/inmemory 0 21s +dns-operator-2 dns-provider-credentials-aws kuadrant.io/aws 5 21s +dns-operator-2 dns-provider-credentials-azure kuadrant.io/azure 3 21s +dns-operator-2 dns-provider-credentials-gcp kuadrant.io/gcp 4 21s +dns-operator-2 dns-provider-credentials-inmemory kuadrant.io/inmemory 0 21s +dnstest dns-provider-credentials-aws kuadrant.io/aws 5 68s +dnstest dns-provider-credentials-azure kuadrant.io/azure 3 68s +dnstest dns-provider-credentials-gcp kuadrant.io/gcp 4 68s +dnstest dns-provider-credentials-inmemory kuadrant.io/inmemory 0 68s ``` ### Cluster scoped on multiple clusters @@ -59,22 +65,22 @@ make local-setup DEPLOY=true DEPLOYMENT_SCOPE=namespace DEPLOYMENT_COUNT=2 CLUST ### Cluster scoped on single cluster ```shell -make test-e2e TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws TEST_DNS_NAMESPACES=dnstest +make test-e2e TEST_DNS_ZONE_DOMAIN_NAME=mn.hcpapps.net TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-aws TEST_DNS_NAMESPACES=dnstest ``` ### Namespace scoped on single cluster ```shell -make test-e2e TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT=2 +make test-e2e TEST_DNS_ZONE_DOMAIN_NAME=mn.hcpapps.net TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-aws TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT=2 ``` ### Cluster scoped on multiple clusters ```shell -make test-e2e TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws TEST_DNS_NAMESPACES=dnstest TEST_DNS_CLUSTER_CONTEXTS=kind-kuadrant-dns-local CLUSTER_COUNT=2 +make test-e2e TEST_DNS_ZONE_DOMAIN_NAME=mn.hcpapps.net TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-aws TEST_DNS_NAMESPACES=dnstest TEST_DNS_CLUSTER_CONTEXTS=kind-kuadrant-dns-local CLUSTER_COUNT=2 ``` ### Namespace scoped on multiple clusters ```shell -make test-e2e TEST_DNS_MANAGED_ZONE_NAME=dev-mz-aws TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT=2 TEST_DNS_CLUSTER_CONTEXTS=kind-kuadrant-dns-local CLUSTER_COUNT=2 +make test-e2e TEST_DNS_ZONE_DOMAIN_NAME=mn.hcpapps.net TEST_DNS_PROVIDER_SECRET_NAME=dns-provider-credentials-aws TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT=2 TEST_DNS_CLUSTER_CONTEXTS=kind-kuadrant-dns-local CLUSTER_COUNT=2 ``` ## Tailing operator pod logs diff --git a/test/e2e/fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml b/test/e2e/fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml index a2df6420..0fd63026 100644 --- a/test/e2e/fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml +++ b/test/e2e/fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml @@ -47,7 +47,7 @@ spec: setIdentifier: default targets: - eu.klb.${testHostname} - managedZone: - name: ${TEST_DNS_MANAGED_ZONE_NAME} + providerRef: + name: ${TEST_DNS_PROVIDER_SECRET_NAME} ownerID: 2bq03i rootHost: ${testHostname} diff --git a/test/e2e/healthcheck_test.go b/test/e2e/healthcheck_test.go index 14e13b9f..56911526 100644 --- a/test/e2e/healthcheck_test.go +++ b/test/e2e/healthcheck_test.go @@ -11,6 +11,7 @@ import ( . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/strings/slices" @@ -30,7 +31,7 @@ var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { var testHostname string var k8sClient client.Client - var testManagedZone *v1alpha1.ManagedZone + var testDNSProviderSecret *v1.Secret var dnsRecord *v1alpha1.DNSRecord @@ -39,10 +40,10 @@ var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { testDomainName = strings.Join([]string{testSuiteID, testZoneDomainName}, ".") testHostname = strings.Join([]string{testID, testDomainName}, ".") k8sClient = testClusters[0].k8sClient - testManagedZone = testClusters[0].testManagedZones[0] + testDNSProviderSecret = testClusters[0].testDNSProviderSecrets[0] SetTestEnv("testID", testID) SetTestEnv("testHostname", testHostname) - SetTestEnv("testNamespace", testManagedZone.Namespace) + SetTestEnv("testNamespace", testDNSProviderSecret.Namespace) }) AfterEach(func(ctx SpecContext) { @@ -64,11 +65,8 @@ var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { healthChecksSupported = true } - provider, err := ProviderForManagedZone(ctx, testManagedZone, k8sClient) - Expect(err).To(BeNil()) - dnsRecord = &v1alpha1.DNSRecord{} - err = ResourceFromFile("./fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml", dnsRecord, GetTestEnv) + err := ResourceFromFile("./fixtures/healthcheck_test/geo-dnsrecord-healthchecks.yaml", dnsRecord, GetTestEnv) Expect(err).ToNot(HaveOccurred()) By("creating dnsrecord " + dnsRecord.Name) @@ -120,6 +118,9 @@ var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { } }, TestTimeoutMedium, time.Second).Should(Succeed()) + provider, err := ProviderForDNSRecord(ctx, dnsRecord, k8sClient) + Expect(err).To(BeNil()) + By("confirming the health checks exist in the provider") Eventually(func(g Gomega) { if !healthChecksSupported { diff --git a/test/e2e/helpers/common.go b/test/e2e/helpers/common.go index e9b3ff8b..9bf2a9d9 100644 --- a/test/e2e/helpers/common.go +++ b/test/e2e/helpers/common.go @@ -72,17 +72,16 @@ func EndpointsForHost(ctx context.Context, provider provider.Provider, host stri return filtered, nil } -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 +func ProviderForDNSRecord(ctx context.Context, record *v1alpha1.DNSRecord, c client.Client) (provider.Provider, error) { providerFactory, err := provider.NewFactory(c, []string{"aws", "google", "azure"}) if err != nil { return nil, err } providerConfig := provider.Config{ - DomainFilter: externaldnsendpoint.NewDomainFilter([]string{mz.Spec.DomainName}), + DomainFilter: externaldnsendpoint.NewDomainFilter([]string{record.Status.ZoneDomainName}), ZoneTypeFilter: externaldnsprovider.NewZoneTypeFilter(""), - ZoneIDFilter: externaldnsprovider.NewZoneIDFilter([]string{mz.Status.ID}), + ZoneIDFilter: externaldnsprovider.NewZoneIDFilter([]string{record.Status.ZoneID}), } //Disable provider logging in test output - return providerFactory.ProviderFor(logr.NewContext(ctx, logr.Discard()), mz, providerConfig) + return providerFactory.ProviderFor(logr.NewContext(ctx, logr.Discard()), record, providerConfig) } diff --git a/test/e2e/multi_record_test.go b/test/e2e/multi_record_test.go index 482222bb..c0698117 100644 --- a/test/e2e/multi_record_test.go +++ b/test/e2e/multi_record_test.go @@ -77,19 +77,19 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { It("creates and deletes distributed dns records", func(ctx SpecContext) { By(fmt.Sprintf("creating %d simple dnsrecords accross %d clusters", len(testNamespaces)*len(testClusters), len(testClusters))) for ci, tc := range testClusters { - for mi, mz := range tc.testManagedZones { + for si, s := range tc.testDNSProviderSecrets { config := testConfig{ - testTargetIP: fmt.Sprintf("127.0.%d.%d", ci+1, mi+1), + testTargetIP: fmt.Sprintf("127.0.%d.%d", ci+1, si+1), } record := &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: testID, - Namespace: mz.Namespace, + Namespace: s.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ RootHost: testHostname, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: mz.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: s.Name, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -105,15 +105,15 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { }, } - By(fmt.Sprintf("creating dns record [name: `%s`, namespace: `%s`, mz: `%s`, endpoint: [dnsname: `%s`, target: `%s`]] on cluster [name: `%s`]", record.Name, record.Namespace, mz.Name, testHostname, config.testTargetIP, tc.name)) + By(fmt.Sprintf("creating dns record [name: `%s`, namespace: `%s`, secret: `%s`, endpoint: [dnsname: `%s`, target: `%s`]] on cluster [name: `%s`]", record.Name, record.Namespace, s.Name, testHostname, config.testTargetIP, tc.name)) err := tc.k8sClient.Create(ctx, record) Expect(err).ToNot(HaveOccurred()) testRecords = append(testRecords, &testDNSRecord{ - cluster: &testClusters[ci], - managedZone: mz, - record: record, - config: config, + cluster: &testClusters[ci], + dnsProviderSecret: s, + record: record, + config: config, }) } } @@ -140,7 +140,7 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { }, recordsReadyMaxDuration, 5*time.Second, ctx).Should(Succeed()) By("checking provider zone records are created as expected") - testProvider, err := ProviderForManagedZone(ctx, testClusters[0].testManagedZones[0], testClusters[0].k8sClient) + testProvider, err := ProviderForDNSRecord(ctx, testRecords[0].record, testClusters[0].k8sClient) Expect(err).NotTo(HaveOccurred()) zoneEndpoints, err := EndpointsForHost(ctx, testProvider, testHostname) @@ -263,7 +263,7 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { By(fmt.Sprintf("creating %d loadbalanced dnsrecords accross %d clusters", len(testNamespaces)*len(testClusters), len(testClusters))) for ci, tc := range testClusters { - for mi, mz := range tc.testManagedZones { + for mi, s := range tc.testDNSProviderSecrets { var geoCode string if (ci+mi)%2 == 0 { @@ -292,12 +292,12 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { record := &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: testID, - Namespace: mz.Namespace, + Namespace: s.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ RootHost: testHostname, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: mz.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: s.Name, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -366,14 +366,14 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { }, } - By(fmt.Sprintf("creating dns record [name: `%s`, namespace: `%s`, managedZone: `%s`, endpoint: [dnsname: `%s`, target: `%s`, geoCode: `%s`]] on cluster [name: `%s`]", record.Name, record.Namespace, mz.Name, testHostname, config.testTargetIP, config.testGeoCode, tc.name)) + By(fmt.Sprintf("creating dns record [name: `%s`, namespace: `%s`, secret: `%s`, endpoint: [dnsname: `%s`, target: `%s`, geoCode: `%s`]] on cluster [name: `%s`]", record.Name, record.Namespace, s.Name, testHostname, config.testTargetIP, config.testGeoCode, tc.name)) err := tc.k8sClient.Create(ctx, record) Expect(err).ToNot(HaveOccurred()) tr := &testDNSRecord{ - cluster: &testClusters[ci], - managedZone: mz, - record: record, - config: config, + cluster: &testClusters[ci], + dnsProviderSecret: s, + record: record, + config: config, } testRecords = append(testRecords, tr) testGeoRecords[config.testGeoCode] = append(testGeoRecords[config.testGeoCode], *tr) @@ -399,7 +399,7 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { }, recordsReadyMaxDuration, 5*time.Second, ctx).Should(Succeed()) By("checking provider zone records are created as expected") - testProvider, err := ProviderForManagedZone(ctx, testClusters[0].testManagedZones[0], testClusters[0].k8sClient) + testProvider, err := ProviderForDNSRecord(ctx, testRecords[0].record, testClusters[0].k8sClient) Expect(err).NotTo(HaveOccurred()) zoneEndpoints, err := EndpointsForHost(ctx, testProvider, testHostname) diff --git a/test/e2e/provider_errors_test.go b/test/e2e/provider_errors_test.go index a746c66e..4d59026f 100644 --- a/test/e2e/provider_errors_test.go +++ b/test/e2e/provider_errors_test.go @@ -11,6 +11,7 @@ import ( . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" @@ -31,7 +32,7 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() var testHostname string var k8sClient client.Client - var testManagedZone *v1alpha1.ManagedZone + var testDNSProviderSecret *v1.Secret var dnsRecord *v1alpha1.DNSRecord @@ -40,7 +41,7 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() testDomainName = strings.Join([]string{testSuiteID, testZoneDomainName}, ".") testHostname = strings.Join([]string{testID, testDomainName}, ".") k8sClient = testClusters[0].k8sClient - testManagedZone = testClusters[0].testManagedZones[0] + testDNSProviderSecret = testClusters[0].testDNSProviderSecrets[0] }) AfterEach(func(ctx SpecContext) { @@ -85,7 +86,7 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() invalidEndpoint, } - dnsRecord = testBuildDNSRecord(testID, testManagedZone.Namespace, testManagedZone.Name, "test-owner", testHostname) + dnsRecord = testBuildDNSRecord(testID, testDNSProviderSecret.Namespace, testDNSProviderSecret.Name, "test-owner", testHostname) dnsRecord.Spec.Endpoints = testEndpoints By("creating dnsrecord " + dnsRecord.Name + " with invalid geo endpoint") @@ -176,7 +177,7 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() invalidEndpoint, } - dnsRecord = testBuildDNSRecord(testID, testManagedZone.Namespace, testManagedZone.Name, "test-owner", testHostname) + dnsRecord = testBuildDNSRecord(testID, testDNSProviderSecret.Namespace, testDNSProviderSecret.Name, "test-owner", testHostname) dnsRecord.Spec.Endpoints = testEndpoints By("creating dnsrecord " + dnsRecord.Name + " with invalid weight endpoint") @@ -238,7 +239,7 @@ var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() }) // testBuildDNSRecord creates a valid dnsrecord with a single valid endpoint -func testBuildDNSRecord(name, ns, mzName, ownerID, rootHost string) *v1alpha1.DNSRecord { +func testBuildDNSRecord(name, ns, dnsProviderSecretName, ownerID, rootHost string) *v1alpha1.DNSRecord { return &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -247,8 +248,8 @@ func testBuildDNSRecord(name, ns, mzName, ownerID, rootHost string) *v1alpha1.DN Spec: v1alpha1.DNSRecordSpec{ OwnerID: ownerID, RootHost: rootHost, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: mzName, + ProviderRef: v1alpha1.ProviderRef{ + Name: dnsProviderSecretName, }, Endpoints: []*externaldnsendpoint.Endpoint{ { diff --git a/test/e2e/single_record_test.go b/test/e2e/single_record_test.go index 543be7bb..9a4ede72 100644 --- a/test/e2e/single_record_test.go +++ b/test/e2e/single_record_test.go @@ -11,6 +11,7 @@ import ( . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" @@ -31,7 +32,7 @@ var _ = Describe("Single Record Test", func() { var testHostname string var k8sClient client.Client - var testManagedZone *v1alpha1.ManagedZone + var testDNSProviderSecret *v1.Secret var geoCode string var dnsRecord *v1alpha1.DNSRecord @@ -41,7 +42,7 @@ var _ = Describe("Single Record Test", func() { testDomainName = strings.Join([]string{testSuiteID, testZoneDomainName}, ".") testHostname = strings.Join([]string{testID, testDomainName}, ".") k8sClient = testClusters[0].k8sClient - testManagedZone = testClusters[0].testManagedZones[0] + testDNSProviderSecret = testClusters[0].testDNSProviderSecrets[0] if testDNSProvider == "google" { geoCode = "us-east1" } else { @@ -65,12 +66,12 @@ var _ = Describe("Single Record Test", func() { dnsRecord = &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: testID, - Namespace: testManagedZone.Namespace, + Namespace: testDNSProviderSecret.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ RootHost: testWCHostname, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: testManagedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: testProviderSecretName, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -116,7 +117,7 @@ var _ = Describe("Single Record Test", func() { Expect(dnsRecord.Status.OwnerID).To(Equal(dnsRecord.GetUIDHash())) By("ensuring zone records are created as expected") - testProvider, err := ProviderForManagedZone(ctx, testManagedZone, k8sClient) + testProvider, err := ProviderForDNSRecord(ctx, dnsRecord, k8sClient) Expect(err).NotTo(HaveOccurred()) zoneEndpoints, err := EndpointsForHost(ctx, testProvider, testHostname) Expect(err).NotTo(HaveOccurred()) @@ -160,12 +161,12 @@ var _ = Describe("Single Record Test", func() { dnsRecord = &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: testID, - Namespace: testManagedZone.Namespace, + Namespace: testDNSProviderSecret.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ RootHost: testHostname, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: testManagedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: testProviderSecretName, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -203,7 +204,7 @@ var _ = Describe("Single Record Test", func() { Expect(dnsRecord.Status.OwnerID).To(Equal(dnsRecord.GetUIDHash())) By("ensuring zone records are created as expected") - testProvider, err := ProviderForManagedZone(ctx, testManagedZone, k8sClient) + testProvider, err := ProviderForDNSRecord(ctx, dnsRecord, k8sClient) Expect(err).NotTo(HaveOccurred()) zoneEndpoints, err := EndpointsForHost(ctx, testProvider, testHostname) Expect(err).NotTo(HaveOccurred()) @@ -251,12 +252,12 @@ var _ = Describe("Single Record Test", func() { dnsRecord = &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: testID, - Namespace: testManagedZone.Namespace, + Namespace: testDNSProviderSecret.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ RootHost: testHostname, - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: testManagedZone.Name, + ProviderRef: v1alpha1.ProviderRef{ + Name: testProviderSecretName, }, Endpoints: []*externaldnsendpoint.Endpoint{ { @@ -347,7 +348,7 @@ var _ = Describe("Single Record Test", func() { Expect(dnsRecord.Status.OwnerID).To(Equal(dnsRecord.GetUIDHash())) By("ensuring zone records are created as expected") - testProvider, err := ProviderForManagedZone(ctx, testManagedZone, k8sClient) + testProvider, err := ProviderForDNSRecord(ctx, dnsRecord, k8sClient) Expect(err).NotTo(HaveOccurred()) zoneEndpoints, err := EndpointsForHost(ctx, testProvider, testHostname) Expect(err).NotTo(HaveOccurred()) diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 1875f40c..fbcc465f 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -12,10 +12,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" @@ -33,41 +31,42 @@ import ( const ( // configuration environment variables - dnsManagedZoneName = "TEST_DNS_MANAGED_ZONE_NAME" - dnsNamespaces = "TEST_DNS_NAMESPACES" - dnsClusterContexts = "TEST_DNS_CLUSTER_CONTEXTS" - deploymentCount = "DEPLOYMENT_COUNT" - clusterCount = "CLUSTER_COUNT" + dnsZoneDomainNameEnvvar = "TEST_DNS_ZONE_DOMAIN_NAME" + dnsProviderSecretNameEnvvar = "TEST_DNS_PROVIDER_SECRET_NAME" + dnsNamespacesEnvvar = "TEST_DNS_NAMESPACES" + dnsClusterContextsEnvvar = "TEST_DNS_CLUSTER_CONTEXTS" + deploymentCountEnvvar = "DEPLOYMENT_COUNT" + clusterCountEnvvar = "CLUSTER_COUNT" ) var ( // 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 - testNamespaces []string - testClusterContexts []string - testDNSProvider string - testClusters []testCluster + testZoneDomainName string + testProviderSecretName string + testNamespaces []string + testClusterContexts []string + testDNSProvider string + testClusters []testCluster supportedHealthCheckProviders = []string{"aws"} ) -// testCluster represents a cluster under test and contains a reference to a configured k8client and all it's managed zones. +// testCluster represents a cluster under test and contains a reference to a configured k8client and all it's dns provider secrets. type testCluster struct { - name string - testManagedZones []*v1alpha1.ManagedZone - k8sClient client.Client + name string + testDNSProviderSecrets []*v1.Secret + k8sClient client.Client } -// testDNSRecord encapsulates a v1alpha1.DNSRecord created in a test case, the v1alpha1.ManagedZone it was created in and the config used to create it. +// testDNSRecord encapsulates a v1alpha1.DNSRecord created in a test case, the v1.Secret (DNS Provider Secret) it was created with and the config used to create it. // The testConfig is used when asserting the expected values set in the providers. type testDNSRecord struct { - cluster *testCluster - managedZone *v1alpha1.ManagedZone - record *v1alpha1.DNSRecord - config testConfig + cluster *testCluster + dnsProviderSecret *v1.Secret + record *v1alpha1.DNSRecord + config testConfig } type testConfig struct { @@ -103,7 +102,7 @@ var _ = BeforeSuite(func(ctx SpecContext) { Expect(testZoneDomainName).NotTo(BeEmpty()) Expect(testClusters).NotTo(BeEmpty()) for i := range testClusters { - Expect(testClusters[i].testManagedZones).NotTo(BeEmpty()) + Expect(testClusters[i].testDNSProviderSecrets).NotTo(BeEmpty()) } testSuiteID = "dns-op-e2e-" + GenerateName() @@ -117,9 +116,10 @@ var _ = BeforeSuite(func(ctx SpecContext) { // setConfigFromEnvVars loads test suite runtime configurations from env vars. // -// dnsManagedZoneName managed zone name expected to exist in each test namespace (i.e. dev-mz-aws). -// dnsNamespaces test namespaces, comma seperated list (i.e. dns-operator-1,dns-operator-2) -// deploymentCount number of test namespaces expected. Appends an index suffix to the dnsNamespaces, only used if dnsNamespaces is a single length array. +// dnsProviderSecretNameEnvvar dns provider secret name expected to exist in each test namespace (i.e. dns-provider-credentials-aws). +// dnsZoneDomainNameEnvvar zone domain name accessible via the provider secret to use for testing (i.e. mn.hcpapps.net). +// dnsNamespacesEnvvar test namespaces, comma seperated list (i.e. dns-operator-1,dns-operator-2) +// deploymentCountEnvvar number of test namespaces expected. Appends an index suffix to the dnsNamespacesEnvvar, only used if dnsNamespacesEnvvar is a single length array. // // Examples: // inputs: TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT= configResult: dnsNamespaces=dns-operator @@ -128,9 +128,9 @@ var _ = BeforeSuite(func(ctx SpecContext) { // inputs: TEST_DNS_NAMESPACES=dns-operator DEPLOYMENT_COUNT=2 configResult: dnsNamespaces=dns-operator-1,dns-operator-2 // inputs: TEST_DNS_NAMESPACES=dns-operator-5,dns-operator-6 DEPLOYMENT_COUNT=1 configResult: dnsNamespaces=dns-operator-5,dns-operator-6 // -// dnsClusterContexts test cluster contexts, comma seperated list (i.e. kind-kuadrant-dns-local-1,kind-kuadrant-dns-local-2), +// dnsClusterContextsEnvvar test cluster contexts, comma seperated list (i.e. kind-kuadrant-dns-local-1,kind-kuadrant-dns-local-2), // if unset the current context is used and a single cluster is assumed. -// clusterCount number of test clusters expected. Appends an index suffix to the dnsClusterContexts, only used if dnsClusterContexts is a single length array. +// clusterCountEnvvar number of test clusters expected. Appends an index suffix to the dnsClusterContextsEnvvar, only used if dnsClusterContextsEnvvar is a single length array. // // Examples: // inputs: TEST_DNS_CLUSTER_CONTEXTS=kind-kuadrant-dns-local CLUSTER_COUNT= configResult: dnsClusterContexts=kind-kuadrant-dns-local @@ -141,25 +141,28 @@ var _ = BeforeSuite(func(ctx SpecContext) { func setConfigFromEnvVars() error { // Load test suite configuration from the environment - if testManagedZoneName = os.Getenv(dnsManagedZoneName); testManagedZoneName == "" { - return fmt.Errorf("env variable '%s' must be set", dnsManagedZoneName) + if testZoneDomainName = os.Getenv(dnsZoneDomainNameEnvvar); testZoneDomainName == "" { + return fmt.Errorf("env variable '%s' must be set", dnsZoneDomainNameEnvvar) + } + if testProviderSecretName = os.Getenv(dnsProviderSecretNameEnvvar); testProviderSecretName == "" { + return fmt.Errorf("env variable '%s' must be set", dnsProviderSecretNameEnvvar) } - namespacesStr := os.Getenv(dnsNamespaces) + namespacesStr := os.Getenv(dnsNamespacesEnvvar) if namespacesStr == "" { //ToDo mnairn: Temporarily keeping a check for "TEST_DNS_NAMESPACE" to allow PR e2e test to work. Remove later. namespacesStr = os.Getenv("TEST_DNS_NAMESPACE") if namespacesStr == "" { - return fmt.Errorf("env variable '%s' must be set", dnsNamespaces) + return fmt.Errorf("env variable '%s' must be set", dnsNamespacesEnvvar) } } namespaces := strings.Split(namespacesStr, ",") if len(namespaces) == 1 { - if dcStr := os.Getenv(deploymentCount); dcStr != "" { + if dcStr := os.Getenv(deploymentCountEnvvar); dcStr != "" { dc, err := strconv.Atoi(dcStr) if err != nil { - return fmt.Errorf("env variable '%s' must be an integar", deploymentCount) + return fmt.Errorf("env variable '%s' must be an integar", deploymentCountEnvvar) } for i := 1; i <= dc; i++ { testNamespaces = append(testNamespaces, fmt.Sprintf("%s-%d", namespaces[0], i)) @@ -171,7 +174,7 @@ func setConfigFromEnvVars() error { testNamespaces = namespaces } - clusterContextsStr := os.Getenv(dnsClusterContexts) + clusterContextsStr := os.Getenv(dnsClusterContextsEnvvar) if clusterContextsStr == "" { testClusterContexts = []string{"current"} return nil @@ -179,10 +182,10 @@ func setConfigFromEnvVars() error { clusterContexts := strings.Split(clusterContextsStr, ",") if len(clusterContexts) == 1 { - if dcStr := os.Getenv(clusterCount); dcStr != "" { + if dcStr := os.Getenv(clusterCountEnvvar); dcStr != "" { dc, err := strconv.Atoi(dcStr) if err != nil { - return fmt.Errorf("env variable '%s' must be an integar", clusterCount) + return fmt.Errorf("env variable '%s' must be an integar", clusterCountEnvvar) } for i := 1; i <= dc; i++ { testClusterContexts = append(testClusterContexts, fmt.Sprintf("%s-%d", clusterContexts[0], i)) @@ -197,7 +200,7 @@ func setConfigFromEnvVars() error { return nil } -// loadClusters iterates each of the configured test clusters, configures a k8s client, loads test managed zones and creates a `testCluster` resource. +// loadClusters iterates each of the configured test clusters, configures a k8s client, loads dns provider secrets and creates a `testCluster` resource. func loadClusters(ctx context.Context) { for _, c := range testClusterContexts { cfgOverrides := &clientcmd.ConfigOverrides{} @@ -218,55 +221,35 @@ func loadClusters(ctx context.Context) { k8sClient: k8sClient, } - loadManagedZones(ctx, tc) + loadProviderSecrets(ctx, tc) //Append the cluster to the list of test clusters testClusters = append(testClusters, *tc) } } -// loadManagedZones iterates each of the configured test namespaces, loads the expected managed zone (TEST_DNS_MANAGED_ZONE_NAME), and asserts the configuration of each is compatible. -// Sets the test suite testDNSProvider and testZoneDomainName directly from the managed zone spec and provider secret. -// If the managed zone does not exist in the namespace, an error is thrown. -// If the managed zone has a different domain name from any previously loaded managed zones, an error is thrown. -// If the managed zone has a different dns provider from any previously loaded managed zones, an error is thrown. -func loadManagedZones(ctx context.Context, tc *testCluster) { +// loadProviderSecrets iterates each of the configured test namespaces, loads the expected provider secret (TEST_DNS_PROVIDER_SECRET_NAME), and asserts the configuration of each is compatible. +// Sets the test suite testDNSProvider directly from the provider secret. +// If the provider secret does not exist in the namespace, an error is thrown. +// If the provider secret has a different dns provider name from any previously loaded provider secret, an error is thrown. +func loadProviderSecrets(ctx context.Context, tc *testCluster) { for _, n := range testNamespaces { - mz := &v1alpha1.ManagedZone{} - - // Ensure managed zone exists and is ready - err := tc.k8sClient.Get(ctx, client.ObjectKey{Namespace: n, Name: testManagedZoneName}, mz) - Expect(err).NotTo(HaveOccurred()) - Expect(mz.Status.Conditions).To( - ContainElement(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(string(v1alpha1.ConditionTypeReady)), - "Status": Equal(metav1.ConditionTrue), - "ObservedGeneration": Equal(mz.Generation), - })), - ) - - // Ensure all managed zone names match - if testZoneDomainName == "" { - testZoneDomainName = mz.Spec.DomainName - } else { - Expect(mz.Spec.DomainName).To(Equal(testZoneDomainName)) - } - + // Ensure provider secret exists s := &v1.Secret{} - err = tc.k8sClient.Get(ctx, client.ObjectKey{Namespace: n, Name: mz.Spec.SecretRef.Name}, s) + err := tc.k8sClient.Get(ctx, client.ObjectKey{Namespace: n, Name: testProviderSecretName}, s) Expect(err).NotTo(HaveOccurred()) p, err := provider.NameForProviderSecret(s) Expect(err).NotTo(HaveOccurred()) - // Ensure all managed zone are suing the same provider + // Ensure all provider secrets are for the same provider if testDNSProvider == "" { testDNSProvider = p } else { Expect(p).To(Equal(testDNSProvider)) } - //Append the managed zone to the list of test zones - tc.testManagedZones = append(tc.testManagedZones, mz) + //Append the provider secret to the list of test provider secrets + tc.testDNSProviderSecrets = append(tc.testDNSProviderSecrets, s) } }