From e12b652cca3d525e9ec252b583359e588b4d3e28 Mon Sep 17 00:00:00 2001 From: Michael Nairn Date: Mon, 23 Oct 2023 09:44:23 +0100 Subject: [PATCH] Use DNSProvider secret ref in DNSPolicy Removes the requirement for a ManagedZone to be configured for a host in order for the DNSPolicy to create dns records for it. Make DNSRecord.zoneID optional Set specific allowed values for providerRef.Kind Allow a providerRef.Kind of ManagedZone that looks up zones from Managedzones in the current ns (Current behaviour) and loads dns client through the managed zone providerRef. Add logic around zoneAssignment in dnsPolicy, some providerRef.kinds will result in zoneAssignment being false which indicates that a zone is not goign to be looked up or assigned to any created DNSRecords. A DNSRecord will be created for all listeners in this case. --- .../controller/aws/managed_zone.yaml | 4 +- .../controller/gcp/managed_zone.yaml | 4 +- .../crd/bases/kuadrant.io_dnspolicies.yaml | 18 +++ .../crd/bases/kuadrant.io_dnsrecords.yaml | 18 ++- .../crd/bases/kuadrant.io_managedzones.yaml | 29 ++-- go.mod | 1 + go.sum | 2 + pkg/apis/v1alpha1/dnspolicy_types.go | 26 +--- pkg/apis/v1alpha1/dnsrecord_types.go | 10 +- pkg/apis/v1alpha1/managedzone_types.go | 8 +- pkg/apis/v1alpha1/shared_types.go | 35 +++++ pkg/apis/v1alpha1/zz_generated.deepcopy.go | 56 ++++--- pkg/controllers/dnspolicy/dns_helper.go | 56 +++---- pkg/controllers/dnspolicy/dns_helper_test.go | 2 +- .../dnspolicy/dnspolicy_dnsrecords.go | 67 ++++++++- .../dnsrecord/dnsrecord_controller.go | 52 ++----- .../managedzone/managedzone_controller.go | 8 +- pkg/dns/aws/client.go | 141 ------------------ pkg/dns/aws/dns.go | 135 +++++++++++++---- pkg/dns/aws/health.go | 5 +- pkg/dns/aws/metrics.go | 105 ------------- pkg/dns/dns.go | 33 ++-- pkg/dns/dnsprovider/dnsProvider.go | 49 +++++- pkg/dns/google/google.go | 54 ++++++- pkg/dns/zone_id_filter.go | 37 +++++ 25 files changed, 488 insertions(+), 467 deletions(-) create mode 100644 pkg/apis/v1alpha1/shared_types.go delete mode 100644 pkg/dns/aws/client.go delete mode 100644 pkg/dns/aws/metrics.go create mode 100644 pkg/dns/zone_id_filter.go diff --git a/config/local-setup/controller/aws/managed_zone.yaml b/config/local-setup/controller/aws/managed_zone.yaml index 831db208d..55d28ed73 100644 --- a/config/local-setup/controller/aws/managed_zone.yaml +++ b/config/local-setup/controller/aws/managed_zone.yaml @@ -6,7 +6,7 @@ spec: id: DUMMY_ID domainName: DUMMY_DOMAIN_NAME description: "Dev Managed Zone" - dnsProviderSecretRef: + providerRef: name: mgc-aws-credentials namespace: multi-cluster-gateways - + kind: Secret diff --git a/config/local-setup/controller/gcp/managed_zone.yaml b/config/local-setup/controller/gcp/managed_zone.yaml index fb6cd3b56..b8cb973c6 100644 --- a/config/local-setup/controller/gcp/managed_zone.yaml +++ b/config/local-setup/controller/gcp/managed_zone.yaml @@ -6,7 +6,7 @@ spec: id: DUMMY_ID domainName: DUMMY_DOMAIN_NAME description: "Dev Managed Zone" - dnsProviderSecretRef: + providerRef: name: mgc-gcp-credentials namespace: multi-cluster-gateways - + kind: Secret diff --git a/config/policy-controller/crd/bases/kuadrant.io_dnspolicies.yaml b/config/policy-controller/crd/bases/kuadrant.io_dnspolicies.yaml index 63f594cdf..e1c9940cb 100644 --- a/config/policy-controller/crd/bases/kuadrant.io_dnspolicies.yaml +++ b/config/policy-controller/crd/bases/kuadrant.io_dnspolicies.yaml @@ -155,6 +155,23 @@ spec: type: integer type: object type: object + providerRef: + properties: + kind: + enum: + - None + - Secret + - ManagedZone + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + - namespace + type: object routingStrategy: default: loadbalanced enum: @@ -199,6 +216,7 @@ spec: - name type: object required: + - providerRef - routingStrategy - targetRef type: object diff --git a/config/policy-controller/crd/bases/kuadrant.io_dnsrecords.yaml b/config/policy-controller/crd/bases/kuadrant.io_dnsrecords.yaml index b75091f0b..8e3f46385 100644 --- a/config/policy-controller/crd/bases/kuadrant.io_dnsrecords.yaml +++ b/config/policy-controller/crd/bases/kuadrant.io_dnsrecords.yaml @@ -86,15 +86,27 @@ spec: type: object minItems: 1 type: array - managedZone: - description: ManagedZoneReference holds a reference to a ManagedZone + providerRef: properties: + kind: + enum: + - None + - Secret + - ManagedZone + type: string name: - description: '`name` is the name of the managed zone. Required' + type: string + namespace: type: string required: + - kind - name + - namespace type: object + zoneID: + type: string + required: + - providerRef type: object status: description: DNSRecordStatus defines the observed state of DNSRecord diff --git a/config/policy-controller/crd/bases/kuadrant.io_managedzones.yaml b/config/policy-controller/crd/bases/kuadrant.io_managedzones.yaml index 501752f40..52c9930e6 100644 --- a/config/policy-controller/crd/bases/kuadrant.io_managedzones.yaml +++ b/config/policy-controller/crd/bases/kuadrant.io_managedzones.yaml @@ -59,16 +59,6 @@ spec: description: description: Description for this ManagedZone type: string - dnsProviderSecretRef: - properties: - name: - type: string - namespace: - type: string - required: - - name - - namespace - type: object domainName: description: Domain name 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])$ @@ -86,10 +76,27 @@ spec: required: - name type: object + providerRef: + properties: + kind: + enum: + - None + - Secret + - ManagedZone + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + - namespace + type: object required: - description - - dnsProviderSecretRef - domainName + - providerRef type: object status: description: ManagedZoneStatus defines the observed state of a Zone diff --git a/go.mod b/go.mod index ad5bc2dc4..38ed30eaa 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/jetstack/cert-manager v1.7.1 github.com/kuadrant/kuadrant-operator v0.1.1-0.20230323151616-58593d01833a + github.com/linki/instrumented_http v0.3.0 github.com/martinlindhe/base36 v1.1.1 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 diff --git a/go.sum b/go.sum index 2e6d3efd9..7fa591a7c 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/kuadrant/kuadrant-operator v0.1.1-0.20230323151616-58593d01833a h1:dK github.com/kuadrant/kuadrant-operator v0.1.1-0.20230323151616-58593d01833a/go.mod h1:VkDt2tErj42/a4GeuUqEawFXQEb5VisBlsQvD2gHrpM= github.com/kuadrant/limitador-operator v0.4.0 h1:HgJi7LuOsenCUMs2ACCfKMKsKpfHcqmmwVmqpci0hw4= github.com/kuadrant/limitador-operator v0.4.0/go.mod h1:5fQo2XwxPr7bDObut9sK5sHCnK4hwAmTsTptaYvGfuc= +github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= +github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/martinlindhe/base36 v1.1.1 h1:1F1MZ5MGghBXDZ2KJ3QfxmiydlWOGB8HCEtkap5NkVg= diff --git a/pkg/apis/v1alpha1/dnspolicy_types.go b/pkg/apis/v1alpha1/dnspolicy_types.go index c38e1c6e8..0969e1c04 100644 --- a/pkg/apis/v1alpha1/dnspolicy_types.go +++ b/pkg/apis/v1alpha1/dnspolicy_types.go @@ -49,6 +49,9 @@ type DNSPolicySpec struct { // +kubebuilder:validation:Enum=simple;loadbalanced // +kubebuilder:default=loadbalanced RoutingStrategy RoutingStrategy `json:"routingStrategy"` + + // +required + ProviderRef *ProviderRef `json:"providerRef"` } type LoadBalancingSpec struct { @@ -230,26 +233,3 @@ func init() { } const DefaultWeight Weight = 120 - -func NewDefaultDNSPolicy(gateway *gatewayv1beta1.Gateway) DNSPolicy { - gatewayTypedNamespace := gatewayv1beta1.Namespace(gateway.Namespace) - return DNSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: gateway.Name, - Namespace: gateway.Namespace, - }, - Spec: DNSPolicySpec{ - TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayv1beta1.Group(gatewayv1beta1.GroupVersion.Group), - Kind: "Gateway", - Name: gatewayv1beta1.ObjectName(gateway.Name), - Namespace: &gatewayTypedNamespace, - }, - LoadBalancing: &LoadBalancingSpec{ - Weighted: &LoadBalancingWeighted{ - DefaultWeight: DefaultWeight, - }, - }, - }, - } -} diff --git a/pkg/apis/v1alpha1/dnsrecord_types.go b/pkg/apis/v1alpha1/dnsrecord_types.go index 3b8fe43f2..c32e001f0 100644 --- a/pkg/apis/v1alpha1/dnsrecord_types.go +++ b/pkg/apis/v1alpha1/dnsrecord_types.go @@ -102,12 +102,16 @@ func (e *Endpoint) String() string { // DNSRecordSpec defines the desired state of DNSRecord type DNSRecordSpec struct { - // +kubebuilder:validation:Required - // +required - ManagedZoneRef *ManagedZoneReference `json:"managedZone,omitempty"` // +kubebuilder:validation:MinItems=1 // +optional Endpoints []*Endpoint `json:"endpoints,omitempty"` + + // +kubebuilder:validation:Required + // +required + ProviderRef *ProviderRef `json:"providerRef"` + + // +optional + ZoneID *string `json:"zoneID,omitempty"` } // DNSRecordStatus defines the observed state of DNSRecord diff --git a/pkg/apis/v1alpha1/managedzone_types.go b/pkg/apis/v1alpha1/managedzone_types.go index fc4c62a5d..8cdf098f1 100644 --- a/pkg/apis/v1alpha1/managedzone_types.go +++ b/pkg/apis/v1alpha1/managedzone_types.go @@ -41,13 +41,7 @@ type ManagedZoneSpec struct { // +optional ParentManagedZone *ManagedZoneReference `json:"parentManagedZone,omitempty"` // +required - SecretRef *SecretRef `json:"dnsProviderSecretRef"` -} - -type SecretRef struct { - //+required - Namespace string `json:"namespace"` - Name string `json:"name"` + ProviderRef *ProviderRef `json:"providerRef"` } // ManagedZoneStatus defines the observed state of a Zone diff --git a/pkg/apis/v1alpha1/shared_types.go b/pkg/apis/v1alpha1/shared_types.go new file mode 100644 index 000000000..044affbf2 --- /dev/null +++ b/pkg/apis/v1alpha1/shared_types.go @@ -0,0 +1,35 @@ +/* +Copyright 2023 The MultiCluster Traffic Controller Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +// +kubebuilder:validation:Enum=None;Secret;ManagedZone +type ProviderKind string + +type ProviderRef struct { + //+required + Name string `json:"name"` + //+required + Namespace string `json:"namespace"` + //+required + Kind ProviderKind `json:"kind"` +} + +const ( + ProviderKindNone = "None" + ProviderKindSecret = "Secret" + ProviderKindManagedZone = "ManagedZone" +) diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go index 2aae0a414..5f5c8e1e0 100644 --- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go @@ -322,6 +322,11 @@ func (in *DNSPolicySpec) DeepCopyInto(out *DNSPolicySpec) { *out = new(LoadBalancingSpec) (*in).DeepCopyInto(*out) } + if in.ProviderRef != nil { + in, out := &in.ProviderRef, &out.ProviderRef + *out = new(ProviderRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPolicySpec. @@ -438,11 +443,6 @@ func (in *DNSRecordRef) DeepCopy() *DNSRecordRef { // 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 - } if in.Endpoints != nil { in, out := &in.Endpoints, &out.Endpoints *out = make([]*Endpoint, len(*in)) @@ -454,6 +454,16 @@ func (in *DNSRecordSpec) DeepCopyInto(out *DNSRecordSpec) { } } } + if in.ProviderRef != nil { + in, out := &in.ProviderRef, &out.ProviderRef + *out = new(ProviderRef) + **out = **in + } + if in.ZoneID != nil { + in, out := &in.ZoneID, &out.ZoneID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSpec. @@ -792,9 +802,9 @@ func (in *ManagedZoneSpec) DeepCopyInto(out *ManagedZoneSpec) { *out = new(ManagedZoneReference) **out = **in } - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(SecretRef) + if in.ProviderRef != nil { + in, out := &in.ProviderRef, &out.ProviderRef + *out = new(ProviderRef) **out = **in } } @@ -842,6 +852,21 @@ func (in *ManagedZoneStatus) DeepCopy() *ManagedZoneStatus { 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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderRef. +func (in *ProviderRef) DeepCopy() *ProviderRef { + if in == nil { + return nil + } + out := new(ProviderRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in ProviderSpecific) DeepCopyInto(out *ProviderSpecific) { { @@ -876,21 +901,6 @@ func (in *ProviderSpecificProperty) DeepCopy() *ProviderSpecificProperty { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecretRef) DeepCopyInto(out *SecretRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. -func (in *SecretRef) DeepCopy() *SecretRef { - if in == nil { - return nil - } - out := new(SecretRef) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSPolicy) DeepCopyInto(out *TLSPolicy) { *out = *in diff --git a/pkg/controllers/dnspolicy/dns_helper.go b/pkg/controllers/dnspolicy/dns_helper.go index 20931afdf..e42dc3331 100644 --- a/pkg/controllers/dnspolicy/dns_helper.go +++ b/pkg/controllers/dnspolicy/dns_helper.go @@ -14,7 +14,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -34,17 +33,15 @@ const ( var ( ErrUnknownRoutingStrategy = fmt.Errorf("unknown routing strategy") - ErrNoManagedZoneForHost = fmt.Errorf("no managed zone for host") - ErrAlreadyAssigned = fmt.Errorf("managed host already assigned") ) type dnsHelper struct { client.Client } -func findMatchingManagedZone(originalHost, host string, zones []v1alpha1.ManagedZone) (*v1alpha1.ManagedZone, string, error) { - if len(zones) == 0 { - return nil, "", fmt.Errorf("%w : %s", ErrNoManagedZoneForHost, host) +func findMatchingZone(originalHost, host string, zones dns.ZoneList) (*dns.Zone, string, error) { + if len(zones.Items) == 0 { + return nil, "", fmt.Errorf("no zones available") } host = strings.ToLower(host) //get the TLD from this host @@ -65,18 +62,18 @@ func findMatchingManagedZone(originalHost, host string, zones []v1alpha1.Managed // we should never be trying to find a managed 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 findMatchingManagedZone(originalHost, parentDomain, zones) + return findMatchingZone(originalHost, parentDomain, zones) } - zone, ok := slice.Find(zones, func(zone v1alpha1.ManagedZone) bool { - return strings.ToLower(zone.Spec.DomainName) == host + zone, ok := slice.Find(zones.Items, func(zone *dns.Zone) bool { + return strings.ToLower(*zone.Name) == host }) if ok { - subdomain := strings.Replace(strings.ToLower(originalHost), "."+strings.ToLower(zone.Spec.DomainName), "", 1) - return &zone, subdomain, nil + subdomain := strings.Replace(strings.ToLower(originalHost), "."+strings.ToLower(*zone.Name), "", 1) + return zone, subdomain, nil } - return findMatchingManagedZone(originalHost, parentDomain, zones) + return findMatchingZone(originalHost, parentDomain, zones) } @@ -105,20 +102,20 @@ func gatewayDNSRecordLabels(gwKey client.ObjectKey) map[string]string { } } -func (dh *dnsHelper) buildDNSRecordForListener(gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy, targetListener gatewayv1beta1.Listener, managedZone *v1alpha1.ManagedZone) *v1alpha1.DNSRecord { - +func (dh *dnsHelper) buildDNSRecordForListener(gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy, targetListener gatewayv1beta1.Listener, zone *dns.Zone) *v1alpha1.DNSRecord { dnsRecord := &v1alpha1.DNSRecord{ ObjectMeta: metav1.ObjectMeta{ Name: dnsRecordName(gateway.Name, string(targetListener.Name)), - Namespace: managedZone.Namespace, + Namespace: dnsPolicy.Namespace, Labels: commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy)), }, Spec: v1alpha1.DNSRecordSpec{ - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: managedZone.Name, - }, + ProviderRef: dnsPolicy.Spec.ProviderRef, }, } + if zone != nil { + dnsRecord.Spec.ZoneID = zone.ID + } dnsRecord.Labels[LabelListenerReference] = string(targetListener.Name) return dnsRecord } @@ -377,29 +374,14 @@ func (dh *dnsHelper) removeDNSForDeletedListeners(ctx context.Context, upstreamG } -func (dh *dnsHelper) getManagedZoneForListener(ctx context.Context, ns string, listener gatewayv1beta1.Listener) (*v1alpha1.ManagedZone, error) { - var managedZones v1alpha1.ManagedZoneList - if err := dh.List(ctx, &managedZones, client.InNamespace(ns)); err != nil { - log.FromContext(ctx).Error(err, "unable to list managed zones for gateway ", "in ns", ns) - return nil, err - } - host := string(*listener.Hostname) - mz, _, err := findMatchingManagedZone(host, host, managedZones.Items) - return mz, err -} - func dnsRecordName(gatewayName, listenerName string) string { return fmt.Sprintf("%s-%s", gatewayName, listenerName) } -func (dh *dnsHelper) createDNSRecordForListener(ctx context.Context, gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy, mz *v1alpha1.ManagedZone, listener gatewayv1beta1.Listener) (*v1alpha1.DNSRecord, error) { - - log := log.FromContext(ctx) - log.Info("creating dns for gateway listener", "listener", listener.Name) - dnsRecord := dh.buildDNSRecordForListener(gateway, dnsPolicy, listener, mz) - if err := controllerutil.SetControllerReference(mz, dnsRecord, dh.Scheme()); err != nil { - return dnsRecord, err - } +func (dh *dnsHelper) createDNSRecordForListener(ctx context.Context, gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy, listener gatewayv1beta1.Listener, zone *dns.Zone) (*v1alpha1.DNSRecord, error) { + logger := log.FromContext(ctx) + logger.Info("creating dns for gateway listener", "listener", listener.Name) + dnsRecord := dh.buildDNSRecordForListener(gateway, dnsPolicy, listener, zone) err := dh.Create(ctx, dnsRecord, &client.CreateOptions{}) if err != nil && !k8serrors.IsAlreadyExists(err) { diff --git a/pkg/controllers/dnspolicy/dns_helper_test.go b/pkg/controllers/dnspolicy/dns_helper_test.go index c385aad24..112b636da 100644 --- a/pkg/controllers/dnspolicy/dns_helper_test.go +++ b/pkg/controllers/dnspolicy/dns_helper_test.go @@ -396,7 +396,7 @@ func Test_dnsHelper_findMatchingManagedZone(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - mx, subDomain, err := findMatchingManagedZone(testCase.Host, testCase.Host, testCase.Zones) + mx, subDomain, err := findMatchingZone(testCase.Host, testCase.Host, testCase.Zones) testCase.Assert(t, mx, subDomain, err) }) } diff --git a/pkg/controllers/dnspolicy/dnspolicy_dnsrecords.go b/pkg/controllers/dnspolicy/dnspolicy_dnsrecords.go index 2ddff0d97..3add4d5cb 100644 --- a/pkg/controllers/dnspolicy/dnspolicy_dnsrecords.go +++ b/pkg/controllers/dnspolicy/dnspolicy_dnsrecords.go @@ -45,6 +45,49 @@ func (r *DNSPolicyReconciler) reconcileDNSRecords(ctx context.Context, dnsPolicy return nil } +func managedZoneListToZoneList(mzl v1alpha1.ManagedZoneList) dns.ZoneList { + zoneList := dns.ZoneList{} + for _, mz := range mzl.Items { + zoneList.Items = append(zoneList.Items, &dns.Zone{ + ID: &mz.Spec.ID, + Name: &mz.Spec.DomainName, + }) + } + return zoneList +} + +func (r *DNSPolicyReconciler) getZones(ctx context.Context, gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) (dns.ZoneList, bool, error) { + logger := crlog.FromContext(ctx) + zoneList := dns.ZoneList{} + zoneAssignment := false + + switch dnsPolicy.Spec.ProviderRef.Kind { + case v1alpha1.ProviderKindSecret: + zoneAssignment = true + dnsProvider, err := r.DNSProvider(ctx, dnsPolicy.Spec.ProviderRef) + if err != nil { + return zoneList, zoneAssignment, err + } + zoneList, err = dnsProvider.ListZones() + if err != nil { + return zoneList, zoneAssignment, err + } + case v1alpha1.ProviderKindManagedZone: + zoneAssignment = true + var managedZones v1alpha1.ManagedZoneList + if err := r.Client().List(ctx, &managedZones, client.InNamespace(gateway.Namespace)); err != nil { + logger.Error(err, "unable to list managed zones for gateway ", "in ns", gateway.Namespace) + return zoneList, zoneAssignment, err + } + zoneList = managedZoneListToZoneList(managedZones) + case v1alpha1.ProviderKindNone: + fallthrough + default: + zoneAssignment = false + } + return zoneList, zoneAssignment, nil +} + func (r *DNSPolicyReconciler) reconcileGatewayDNSRecords(ctx context.Context, gateway *gatewayv1beta1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) error { log := crlog.FromContext(ctx) @@ -53,21 +96,33 @@ func (r *DNSPolicyReconciler) reconcileGatewayDNSRecords(ctx context.Context, ga return err } - clusterGatewayAddresses := getClusterGatewayAddresses(gateway) + zoneList, zoneAssignment, err := r.getZones(ctx, gateway, dnsPolicy) + if err != nil { + return err + } + log.V(1).Info("got zones", "zoneList", zoneList, "zoneAssignment", zoneAssignment) + clusterGatewayAddresses := getClusterGatewayAddresses(gateway) log.V(3).Info("checking gateway for attached routes ", "gateway", gateway.Name, "clusters", clusterGatewayAddresses) for _, listener := range gateway.Spec.Listeners { var clusterGateways []dns.ClusterGateway - var mz, err = r.dnsHelper.getManagedZoneForListener(ctx, gateway.Namespace, listener) - if err != nil { - return err - } listenerHost := *listener.Hostname if listenerHost == "" { log.Info("skipping listener no hostname assigned", listener.Name, "in ns ", gateway.Namespace) continue } + + var zone *dns.Zone + if zoneAssignment { + zone, _, err = findMatchingZone(string(listenerHost), string(listenerHost), zoneList) + if err != nil { + log.V(1).Info("skipping listener no matching zone for host", "listenerHost", listenerHost) + continue + } + log.V(1).Info("found zone for listener host", "zone", zone, "listenerHost", listenerHost) + } + for clusterName, gatewayAddresses := range clusterGatewayAddresses { // Only consider host for dns if there's at least 1 attached route to the listener for this host in *any* gateway @@ -96,7 +151,7 @@ func (r *DNSPolicyReconciler) reconcileGatewayDNSRecords(ctx context.Context, ga } return nil } - dnsRecord, err := r.dnsHelper.createDNSRecordForListener(ctx, gateway, dnsPolicy, mz, listener) + dnsRecord, err := r.dnsHelper.createDNSRecordForListener(ctx, gateway, dnsPolicy, listener, zone) if err := client.IgnoreAlreadyExists(err); err != nil { return fmt.Errorf("failed to create dns record for listener host %s : %s ", *listener.Hostname, err) } diff --git a/pkg/controllers/dnsrecord/dnsrecord_controller.go b/pkg/controllers/dnsrecord/dnsrecord_controller.go index b38812a8f..de57c7332 100644 --- a/pkg/controllers/dnsrecord/dnsrecord_controller.go +++ b/pkg/controllers/dnsrecord/dnsrecord_controller.go @@ -134,40 +134,23 @@ func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager) error { // 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) error { - 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 { - // If the Managed Zone isn't found, just continue - return client.IgnoreNotFound(err) - } - managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") - - if !managedZoneReady { - return fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) - } - - dnsProvider, err := r.DNSProvider(ctx, managedZone) + dnsProvider, err := r.DNSProvider(ctx, dnsRecord.Spec.ProviderRef) if err != nil { return err } - err = dnsProvider.Delete(dnsRecord, managedZone) + err = dnsProvider.Delete(dnsRecord) if err != nil { if strings.Contains(err.Error(), "was not found") || strings.Contains(err.Error(), "notFound") { - log.Log.Info("Record not found in managed zone, continuing", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + log.Log.Info("Record not found in zone, continuing", "dnsRecord", dnsRecord.Name, "zone", dnsRecord.Spec.ZoneID) return nil } else if strings.Contains(err.Error(), "no endpoints") { - log.Log.Info("DNS record had no endpoint, continuing", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + log.Log.Info("DNS record had no endpoint, continuing", "dnsRecord", dnsRecord.Name, "zone", dnsRecord.Spec.ZoneID) return nil } return err } - log.Log.Info("Deleted DNSRecord in manage zone", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + log.Log.Info("Deleted DNSRecord in zone", "dnsRecord", dnsRecord.Name, "zone", dnsRecord.Spec.ZoneID) return nil } @@ -175,37 +158,20 @@ func (r *DNSRecordReconciler) deleteRecord(ctx context.Context, dnsRecord *v1alp // 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) error { - - 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 { - return err - } - managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") - - if !managedZoneReady { - return fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) - } - if dnsRecord.Generation == dnsRecord.Status.ObservedGeneration { - log.Log.V(3).Info("Skipping managed zone to which the DNS dnsRecord is already published", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + log.Log.V(3).Info("Skipping zone to which the DNS dnsRecord is already published", "dnsRecord", dnsRecord.Name, "zone", dnsRecord.Spec.ZoneID) return nil } - dnsProvider, err := r.DNSProvider(ctx, managedZone) + dnsProvider, err := r.DNSProvider(ctx, dnsRecord.Spec.ProviderRef) if err != nil { return err } - err = dnsProvider.Ensure(dnsRecord, managedZone) + err = dnsProvider.Ensure(dnsRecord) if err != nil { return err } - log.Log.Info("Published DNSRecord to manage zone", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + log.Log.Info("Published DNSRecord to zone", "dnsRecord", dnsRecord.Name, "zone", dnsRecord.Spec.ZoneID) return nil } diff --git a/pkg/controllers/managedzone/managedzone_controller.go b/pkg/controllers/managedzone/managedzone_controller.go index bed66f846..f993f5e46 100644 --- a/pkg/controllers/managedzone/managedzone_controller.go +++ b/pkg/controllers/managedzone/managedzone_controller.go @@ -163,7 +163,7 @@ func (r *ManagedZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *ManagedZoneReconciler) publishManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { - dnsProvider, err := r.DNSProvider(ctx, managedZone) + dnsProvider, err := r.DNSProvider(ctx, managedZone.Spec.ProviderRef) if err != nil { return err } @@ -185,7 +185,7 @@ func (r *ManagedZoneReconciler) deleteManagedZone(ctx context.Context, managedZo return nil } - dnsProvider, err := r.DNSProvider(ctx, managedZone) + dnsProvider, err := r.DNSProvider(ctx, managedZone.Spec.ProviderRef) if err != nil { var reason, message string status := metav1.ConditionFalse @@ -261,9 +261,7 @@ func (r *ManagedZoneReconciler) createParentZoneNSRecord(ctx context.Context, ma Namespace: parentZone.Namespace, }, Spec: v1alpha1.DNSRecordSpec{ - ManagedZoneRef: &v1alpha1.ManagedZoneReference{ - Name: parentZone.Name, - }, + ProviderRef: managedZone.Spec.ProviderRef, Endpoints: []*v1alpha1.Endpoint{ { DNSName: recordName, diff --git a/pkg/dns/aws/client.go b/pkg/dns/aws/client.go deleted file mode 100644 index f7fee74ea..000000000 --- a/pkg/dns/aws/client.go +++ /dev/null @@ -1,141 +0,0 @@ -/* -Copyright 2022 The MultiCluster Traffic Controller Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws - -import ( - "strconv" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/route53" -) - -type InstrumentedRoute53 struct { - route53 *route53.Route53 -} - -func observe(operation string, f func() error) { - start := time.Now() - route53RequestCount.WithLabelValues(operation).Inc() - defer route53RequestCount.WithLabelValues(operation).Dec() - err := f() - duration := time.Since(start).Seconds() - code := returnCodeLabelDefault - if err != nil { - route53RequestErrors.WithLabelValues(operation, code).Inc() - if awsErr, ok := err.(awserr.Error); ok { - if reqErr, ok := err.(awserr.RequestFailure); ok { - // A service error occurred - code = strconv.Itoa(reqErr.StatusCode()) - } else { - // Generic AWS Error with Code, Message, and original error (if any) - code = awsErr.Code() - } - } - } - route53RequestDuration.WithLabelValues(operation, code).Observe(duration) - route53RequestTotal.WithLabelValues(operation, code).Inc() -} - -func (c *InstrumentedRoute53) ListHostedZones(input *route53.ListHostedZonesInput) (output *route53.ListHostedZonesOutput, err error) { - observe("ListHostedZones", func() error { - output, err = c.route53.ListHostedZones(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (output *route53.ChangeResourceRecordSetsOutput, err error) { - observe("ChangeResourceRecordSets", func() error { - output, err = c.route53.ChangeResourceRecordSets(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) CreateHealthCheck(input *route53.CreateHealthCheckInput) (output *route53.CreateHealthCheckOutput, err error) { - observe("CreateHealthCheck", func() error { - output, err = c.route53.CreateHealthCheck(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) GetHostedZone(input *route53.GetHostedZoneInput) (output *route53.GetHostedZoneOutput, err error) { - observe("GetHostedZone", func() error { - output, err = c.route53.GetHostedZone(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) UpdateHostedZoneComment(input *route53.UpdateHostedZoneCommentInput) (output *route53.UpdateHostedZoneCommentOutput, err error) { - observe("UpdateHostedZoneComment", func() error { - output, err = c.route53.UpdateHostedZoneComment(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) CreateHostedZone(input *route53.CreateHostedZoneInput) (output *route53.CreateHostedZoneOutput, err error) { - observe("CreateHostedZone", func() error { - output, err = c.route53.CreateHostedZone(input) - return err - }) - return -} -func (c *InstrumentedRoute53) DeleteHostedZone(input *route53.DeleteHostedZoneInput) (output *route53.DeleteHostedZoneOutput, err error) { - observe("DeleteHostedZone", func() error { - output, err = c.route53.DeleteHostedZone(input) - return err - }) - return -} - -func (c *InstrumentedRoute53) GetHealthCheckWithContext(ctx aws.Context, input *route53.GetHealthCheckInput, opts ...request.Option) (output *route53.GetHealthCheckOutput, err error) { - observe("GetHealthCheckWithContext", func() error { - output, err = c.route53.GetHealthCheckWithContext(ctx, input, opts...) - return err - }) - return -} - -func (c *InstrumentedRoute53) UpdateHealthCheckWithContext(ctx aws.Context, input *route53.UpdateHealthCheckInput, opts ...request.Option) (output *route53.UpdateHealthCheckOutput, err error) { - observe("UpdateHealthCheckWithContext", func() error { - output, err = c.route53.UpdateHealthCheckWithContext(ctx, input, opts...) - return err - }) - return -} - -func (c *InstrumentedRoute53) DeleteHealthCheckWithContext(ctx aws.Context, input *route53.DeleteHealthCheckInput, opts ...request.Option) (output *route53.DeleteHealthCheckOutput, err error) { - observe("DeleteHealthCheckWithContext", func() error { - output, err = c.route53.DeleteHealthCheckWithContext(ctx, input, opts...) - return err - }) - return -} - -func (c *InstrumentedRoute53) ChangeTagsForResourceWithContext(ctx aws.Context, input *route53.ChangeTagsForResourceInput, opts ...request.Option) (output *route53.ChangeTagsForResourceOutput, err error) { - observe("ChangeTagsForResourceWithContext", func() error { - output, err = c.route53.ChangeTagsForResourceWithContext(ctx, input, opts...) - return err - }) - return -} diff --git a/pkg/dns/aws/dns.go b/pkg/dns/aws/dns.go index 06cdb9fcc..71de37220 100644 --- a/pkg/dns/aws/dns.go +++ b/pkg/dns/aws/dns.go @@ -18,7 +18,9 @@ package aws import ( "fmt" + "net" "strconv" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -26,6 +28,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" "github.com/go-logr/logr" + "github.com/linki/instrumented_http" v1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/util/errors" @@ -45,37 +48,51 @@ const ( ) type Route53DNSProvider struct { - client *InstrumentedRoute53 - logger logr.Logger - + client *route53.Route53 + logger logr.Logger + zoneIDFilter dns.ZoneIDFilter healthCheckReconciler dns.HealthCheckReconciler } var _ dns.Provider = &Route53DNSProvider{} func NewProviderFromSecret(s *v1.Secret) (*Route53DNSProvider, error) { - - config := aws.NewConfig() - sessionOpts := session.Options{ - Config: *config, - } if string(s.Data["AWS_ACCESS_KEY_ID"]) == "" || string(s.Data["AWS_SECRET_ACCESS_KEY"]) == "" { 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.SharedConfigState = session.SharedConfigDisable - sess, err := session.NewSessionWithOptions(sessionOpts) - if err != nil { - return nil, fmt.Errorf("unable to create aws session: %s", err) + config := aws.NewConfig().WithMaxRetries(3) + config.WithHTTPClient( + instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{ + PathProcessor: func(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] + }, + }), + ) + config.WithCredentials( + credentials.NewStaticCredentials(string(s.Data["AWS_ACCESS_KEY_ID"]), string(s.Data["AWS_SECRET_ACCESS_KEY"]), ""), + ) + + if string(s.Data["AWS_REGION"]) != "" { + config.WithRegion(string(s.Data["AWS_REGION"])) } - if string(s.Data["REGION"]) != "" { - sess.Config.WithRegion(string(s.Data["REGION"])) + + sess, err := session.NewSessionWithOptions(session.Options{ + Config: *config, + SharedConfigState: session.SharedConfigDisable, + }) + if err != nil { + return nil, fmt.Errorf("failed to instantiate AWS session: %w", err) } + //ToDo load zoneIDFilter from secret data['CONFIG'] + zoneIDFilter := []string{} + p := &Route53DNSProvider{ - client: &InstrumentedRoute53{route53.New(sess, config)}, - logger: log.Log.WithName("aws-route53").WithValues("region", config.Region), + client: route53.New(sess), + logger: log.Log.WithName("aws-route53").WithValues("region", config.Region), + zoneIDFilter: dns.NewZoneIDFilter(zoneIDFilter), } if err := validateServiceEndpoints(p); err != nil { @@ -92,12 +109,28 @@ const ( deleteAction action = "DELETE" ) -func (p *Route53DNSProvider) Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { - return p.change(record, managedZone, upsertAction) +func (p *Route53DNSProvider) Ensure(record *v1alpha1.DNSRecord) error { + return p.change(record, upsertAction) } -func (p *Route53DNSProvider) Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { - return p.change(record, managedZone, deleteAction) +func (p *Route53DNSProvider) Delete(record *v1alpha1.DNSRecord) error { + return p.change(record, deleteAction) +} + +func (p *Route53DNSProvider) ListZones() (dns.ZoneList, error) { + var zoneList dns.ZoneList + zones, err := p.zones() + if err != nil { + return zoneList, err + } + for _, zone := range zones { + dnsName := removeTrailingDot(*zone.Name) + zoneList.Items = append(zoneList.Items, &dns.Zone{ + ID: zone.Id, + Name: &dnsName, + }) + } + return zoneList, nil } func (p *Route53DNSProvider) EnsureManagedZone(zone *v1alpha1.ManagedZone) (dns.ManagedZoneOutput, error) { @@ -172,7 +205,7 @@ func (p *Route53DNSProvider) HealthCheckReconciler() dns.HealthCheckReconciler { if p.healthCheckReconciler == nil { p.healthCheckReconciler = dns.NewCachedHealthCheckReconciler( p, - NewRoute53HealthCheckReconciler(p.client.route53), + NewRoute53HealthCheckReconciler(*p.client), ) } @@ -186,31 +219,62 @@ func (*Route53DNSProvider) ProviderSpecific() dns.ProviderSpecificLabels { } } -func (p *Route53DNSProvider) change(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone, action action) error { +// Zones returns the list of hosted zones. +func (p *Route53DNSProvider) zones() (map[string]*route53.HostedZone, error) { + zones := make(map[string]*route53.HostedZone) + + var tagErr error + f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { + for _, zone := range resp.HostedZones { + if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) { + continue + } + zones[aws.StringValue(zone.Id)] = zone + } + + return true + } + + err := p.client.ListHostedZonesPages(&route53.ListHostedZonesInput{}, f) + if err != nil { + return nil, fmt.Errorf("failed to list hosted zones: %w", err) + } + if tagErr != nil { + return nil, fmt.Errorf("failed to list zones tags: %w", err) + } + + for _, zone := range zones { + log.Log.V(1).Info("Considering zone", "zone.Id", aws.StringValue(zone.Id), "zone.Name", aws.StringValue(zone.Name)) + } + + return zones, nil +} + +func (p *Route53DNSProvider) change(record *v1alpha1.DNSRecord, action action) error { // Configure records. if len(record.Spec.Endpoints) == 0 { return nil } - err := p.updateRecord(record, managedZone.Status.ID, string(action)) + err := p.updateRecord(record, string(action)) if err != nil { - return fmt.Errorf("failed to update record in route53 hosted zone %s: %v", managedZone.Status.ID, err) + return fmt.Errorf("failed to update record in route53 hosted zone %s: %w", *record.Spec.ZoneID, err) } switch action { case upsertAction: - p.logger.Info("Upserted DNS record", "record", record.Spec, "hostedZoneID", managedZone.Status.ID) + p.logger.Info("Upserted DNS record", "record", record.Spec, "hostedZoneID", record.Spec.ZoneID) case deleteAction: - p.logger.Info("Deleted DNS record", "record", record.Spec, "hostedZoneID", managedZone.Status.ID) + p.logger.Info("Deleted DNS record", "record", record.Spec, "hostedZoneID", record.Spec.ZoneID) } return nil } -func (p *Route53DNSProvider) updateRecord(record *v1alpha1.DNSRecord, zoneID, action string) error { +func (p *Route53DNSProvider) updateRecord(record *v1alpha1.DNSRecord, action string) error { if len(record.Spec.Endpoints) == 0 { return fmt.Errorf("no endpoints") } - input := route53.ChangeResourceRecordSetsInput{HostedZoneId: aws.String(zoneID)} + input := route53.ChangeResourceRecordSetsInput{HostedZoneId: aws.String(*record.Spec.ZoneID)} expectedEndpointsMap := make(map[string]struct{}) var changes []*route53.Change @@ -245,9 +309,9 @@ func (p *Route53DNSProvider) updateRecord(record *v1alpha1.DNSRecord, zoneID, ac } resp, err := p.client.ChangeResourceRecordSets(&input) if err != nil { - return fmt.Errorf("couldn't update DNS record %s in zone %s: %v", record.Name, zoneID, err) + return fmt.Errorf("couldn't update DNS record %s in zone %s: %v", record.Name, *record.Spec.ZoneID, err) } - p.logger.Info("Updated DNS record", "record", record, "zone", zoneID, "response", resp) + p.logger.Info("Updated DNS record", "record", record, "zone", *record.Spec.ZoneID, "response", resp) return nil } @@ -339,3 +403,12 @@ func validateServiceEndpoints(provider *Route53DNSProvider) error { } return kerrors.NewAggregate(errs) } + +// removeTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. +func removeTrailingDot(hostname string) string { + if net.ParseIP(hostname) != nil { + return hostname + } + + return strings.TrimSuffix(hostname, ".") +} diff --git a/pkg/dns/aws/health.go b/pkg/dns/aws/health.go index 1d726cf97..bec0cf48e 100644 --- a/pkg/dns/aws/health.go +++ b/pkg/dns/aws/health.go @@ -6,7 +6,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53" - "github.com/aws/aws-sdk-go/service/route53/route53iface" "github.com/rs/xid" "sigs.k8s.io/controller-runtime/pkg/log" @@ -28,12 +27,12 @@ var ( ) type Route53HealthCheckReconciler struct { - client route53iface.Route53API + client route53.Route53 } var _ dns.HealthCheckReconciler = &Route53HealthCheckReconciler{} -func NewRoute53HealthCheckReconciler(client route53iface.Route53API) *Route53HealthCheckReconciler { +func NewRoute53HealthCheckReconciler(client route53.Route53) *Route53HealthCheckReconciler { return &Route53HealthCheckReconciler{ client: client, } diff --git a/pkg/dns/aws/metrics.go b/pkg/dns/aws/metrics.go deleted file mode 100644 index b46058dbc..000000000 --- a/pkg/dns/aws/metrics.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright 2022 The MultiCluster Traffic Controller Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws - -import ( - "reflect" - - "github.com/prometheus/client_golang/prometheus" - - "sigs.k8s.io/controller-runtime/pkg/metrics" -) - -const ( - operationLabel = "operation" - returnCodeLabel = "code" - // The default return code - returnCodeLabelDefault = "" -) - -var ( - // route53RequestCount is a prometheus metric which holds the number of - // concurrent inflight requests to Route53. - route53RequestCount = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "mgc_aws_route53_inflight_request_count", - Help: "MGC AWS Route53 inflight request count", - }, - []string{operationLabel}, - ) - - // route53RequestTotal is a prometheus counter metrics which holds the total - // number of requests to Route53. - route53RequestTotal = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "mgc_aws_route53_request_total", - Help: "MGC AWS Route53 total number of requests", - }, - []string{operationLabel, returnCodeLabel}, - ) - - // route53RequestErrors is a prometheus counter metrics which holds the total - // number of failed requests to Route53. - route53RequestErrors = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "mgc_aws_route53_request_errors_total", - Help: "MGC AWS Route53 total number of errors", - }, - []string{operationLabel, returnCodeLabel}, - ) - - // route53RequestDuration is a prometheus metric which records the duration - // of the requests to Route53. - route53RequestDuration = prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "mgc_aws_route53_request_duration_seconds", - Help: "MGC AWS Route53 request duration", - Buckets: []float64{ - 0.005, 0.01, 0.025, 0.05, 0.1, - 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, - 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, - 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, - 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 60, - }, - }, - []string{operationLabel, returnCodeLabel}, - ) -) - -var operationLabelValues []string - -func init() { - // Register metrics into the global prometheus registry - metrics.Registry.MustRegister( - route53RequestCount, - route53RequestTotal, - route53RequestErrors, - route53RequestDuration, - ) - - monitoredRoute53 := reflect.PtrTo(reflect.TypeOf(InstrumentedRoute53{})) - for i := 0; i < monitoredRoute53.NumMethod(); i++ { - operationLabelValues = append(operationLabelValues, monitoredRoute53.Method(i).Name) - } - - // Initialize metrics - for _, operation := range operationLabelValues { - route53RequestCount.WithLabelValues(operation).Set(0) - route53RequestTotal.WithLabelValues(operation, returnCodeLabelDefault).Add(0) - route53RequestErrors.WithLabelValues(operation, returnCodeLabelDefault).Add(0) - } -} diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index c86ebaff3..2010c06c3 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -31,16 +31,19 @@ const ( ProviderSpecificGeoCode = "geo-code" ) -type DNSProviderFactory func(ctx context.Context, managedZone *v1alpha1.ManagedZone) (Provider, error) +type DNSProviderFactory func(ctx context.Context, providerRef *v1alpha1.ProviderRef) (Provider, error) // Provider knows how to manage DNS zones only as pertains to routing. type Provider interface { // Ensure will create or update record. - Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error + Ensure(record *v1alpha1.DNSRecord) error // Delete will delete record. - Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error + Delete(record *v1alpha1.DNSRecord) error + + // List all zones + ListZones() (ZoneList, error) // Ensure will create or update a managed zone, returns an array of NameServers for that zone. EnsureManagedZone(managedZone *v1alpha1.ManagedZone) (ManagedZoneOutput, error) @@ -65,20 +68,32 @@ type ManagedZoneOutput struct { RecordCount int64 } +type Zone struct { + ID *string + Name *string +} + +type ZoneList struct { + Items []*Zone +} + var _ Provider = &FakeProvider{} type FakeProvider struct{} -func (*FakeProvider) Ensure(dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { +func (*FakeProvider) Ensure(_ *v1alpha1.DNSRecord) error { return nil } -func (*FakeProvider) Delete(dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { +func (*FakeProvider) Delete(_ *v1alpha1.DNSRecord) error { return nil } -func (*FakeProvider) EnsureManagedZone(managedZone *v1alpha1.ManagedZone) (ManagedZoneOutput, error) { +func (*FakeProvider) ListZones() (ZoneList, error) { + return ZoneList{}, nil +} +func (*FakeProvider) EnsureManagedZone(_ *v1alpha1.ManagedZone) (ManagedZoneOutput, error) { return ManagedZoneOutput{}, nil } -func (*FakeProvider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { return nil } +func (*FakeProvider) DeleteManagedZone(_ *v1alpha1.ManagedZone) error { return nil } func (*FakeProvider) HealthCheckReconciler() HealthCheckReconciler { return &FakeHealthCheckReconciler{} @@ -92,7 +107,7 @@ func (*FakeProvider) ProviderSpecific() ProviderSpecificLabels { // SanitizeError removes request specific data from error messages in order to make them consistent across multiple similar requests to the provider. e.g AWS SDK Request ids `request id: 051c860b-9b30-4c19-be1a-1280c3e9fdc4` func SanitizeError(err error) error { - regexp := regexp.MustCompile(`request id: [^\s]+`) - sanitizedErr := regexp.ReplaceAllString(err.Error(), "") + re := regexp.MustCompile(`request id: [^\s]+`) + sanitizedErr := re.ReplaceAllString(err.Error(), "") return errors.New(sanitizedErr) } diff --git a/pkg/dns/dnsprovider/dnsProvider.go b/pkg/dns/dnsprovider/dnsProvider.go index 34628d16a..6db496611 100644 --- a/pkg/dns/dnsprovider/dnsProvider.go +++ b/pkg/dns/dnsprovider/dnsProvider.go @@ -15,7 +15,10 @@ import ( "github.com/Kuadrant/multicluster-gateway-controller/pkg/dns/google" ) -var errUnsupportedProvider = fmt.Errorf("provider type given is not supported") +var ( + ErrUnsupportedProviderKind = fmt.Errorf("unsupported provider kind") + ErrUnsupportedProviderType = fmt.Errorf("unsupported provider type") +) type providerFactory struct { client.Client @@ -29,11 +32,28 @@ func NewProvider(c client.Client) *providerFactory { } // depending on the provider type specified in the form of a custom secret type https://kubernetes.io/docs/concepts/configuration/secret/#secret-types in the dnsprovider secret it returns a dnsprovider. -func (p *providerFactory) DNSProviderFactory(ctx context.Context, managedZone *v1alpha1.ManagedZone) (dns.Provider, error) { +func (p *providerFactory) DNSProviderFactory(ctx context.Context, providerRef *v1alpha1.ProviderRef) (dns.Provider, error) { + return p.provider(ctx, providerRef) +} + +func (p *providerFactory) provider(ctx context.Context, providerRef *v1alpha1.ProviderRef) (dns.Provider, error) { + switch providerRef.Kind { + case v1alpha1.ProviderKindSecret: + return p.providerFromSecret(ctx, providerRef.Name, providerRef.Namespace) + case v1alpha1.ProviderKindManagedZone: + return p.providerFromManagedZone(ctx, providerRef.Name, providerRef.Namespace) + case v1alpha1.ProviderKindNone: + fallthrough + default: + return nil, fmt.Errorf("%w : %s", ErrUnsupportedProviderKind, providerRef.Kind) + } +} + +func (p *providerFactory) providerFromSecret(ctx context.Context, name, namespace string) (dns.Provider, error) { providerSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: managedZone.Spec.SecretRef.Name, - Namespace: managedZone.Spec.SecretRef.Namespace, + Name: name, + Namespace: namespace, }} if err := p.Client.Get(ctx, client.ObjectKeyFromObject(providerSecret), providerSecret); err != nil { @@ -46,7 +66,7 @@ func (p *providerFactory) DNSProviderFactory(ctx context.Context, managedZone *v if err != nil { return nil, fmt.Errorf("unable to create AWS dns provider from secret: %v", err) } - log.Log.V(1).Info("Route53 provider created", "managed zone:", managedZone.Name) + log.Log.V(1).Info("Route53 provider created from secret", "name", name, "namespace", namespace) return dnsProvider, nil case "kuadrant.io/gcp": @@ -54,12 +74,27 @@ func (p *providerFactory) DNSProviderFactory(ctx context.Context, managedZone *v if err != nil { return nil, fmt.Errorf("unable to create GCP dns provider from secret: %v", err) } - log.Log.V(1).Info("Google provider created", "managed zone:", managedZone.Name) + log.Log.V(1).Info("Google provider created from secret", "name", name, "namespace", namespace) return dnsProvider, nil default: - return nil, errUnsupportedProvider + return nil, fmt.Errorf("%w : %s", ErrUnsupportedProviderType, providerSecret.Type) } +} +func (p *providerFactory) providerFromManagedZone(ctx context.Context, name, namespace string) (dns.Provider, error) { + mz := &v1alpha1.ManagedZone{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }} + if err := p.Client.Get(ctx, client.ObjectKeyFromObject(mz), mz); err != nil { + return nil, err + } + //Avoid ending up in a loop, a managed zone should not reference another managed zone + if mz.Spec.ProviderRef.Kind == v1alpha1.ProviderKindManagedZone { + return nil, fmt.Errorf("%w : managed zone cannot have a providerRef with kind %s", ErrUnsupportedProviderKind, v1alpha1.ProviderKindManagedZone) + } + return p.provider(ctx, mz.Spec.ProviderRef) } diff --git a/pkg/dns/google/google.go b/pkg/dns/google/google.go index a269d29c4..f1dbf81c4 100644 --- a/pkg/dns/google/google.go +++ b/pkg/dns/google/google.go @@ -177,6 +177,47 @@ func NewProviderFromSecret(ctx context.Context, s *v1.Secret) (*GoogleDNSProvide // ManagedZones +// ToDo Implement me !!! +func (p *GoogleDNSProvider) ListZones() (dns.ZoneList, error) { + var zoneList dns.ZoneList + + return zoneList, nil +} + +// +//// Zones returns the list of hosted zones. +//func (p *GoogleDNSProvider) zones() (map[string]*dnsv1.ManagedZone, error) { +// zones := make(map[string]*dnsv1.ManagedZone) +// +// f := func(resp *dnsv1.ManagedZonesListResponse) error { +// for _, zone := range resp.ManagedZones { +// if p.domainFilter.Match(zone.DnsName) && p.zoneTypeFilter.Match(zone.Visibility) && (p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) || p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Name))) { +// zones[zone.Name] = zone +// log.Debugf("Matched %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility) +// } else { +// log.Debugf("Filtered %s (zone: %s) (visibility: %s)", zone.DnsName, zone.Name, zone.Visibility) +// } +// } +// +// return nil +// } +// +// log.Debugf("Matching zones against domain filters: %v", p.domainFilter) +// if err := p.managedZonesClient.List(p.project).Pages(ctx, f); err != nil { +// return nil, err +// } +// +// if len(zones) == 0 { +// log.Warnf("No zones in the project, %s, match domain filters: %v", p.project, p.domainFilter) +// } +// +// for _, zone := range zones { +// log.Debugf("Considering zone: %s (domain: %s)", zone.Name, zone.DnsName) +// } +// +// return zones, nil +//} + func (g *GoogleDNSProvider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { return g.managedZonesClient.Delete(g.project, managedZone.Status.ID).Do() } @@ -242,12 +283,12 @@ func (g *GoogleDNSProvider) toManagedZoneOutput(mz *dnsv1.ManagedZone) (dns.Mana //DNSRecords -func (g *GoogleDNSProvider) Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { - return g.updateRecord(record, managedZone.Status.ID, upsertAction) +func (g *GoogleDNSProvider) Ensure(record *v1alpha1.DNSRecord) error { + return g.updateRecord(record, upsertAction) } -func (g *GoogleDNSProvider) Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { - return g.updateRecord(record, managedZone.Status.ID, deleteAction) +func (g *GoogleDNSProvider) Delete(record *v1alpha1.DNSRecord) error { + return g.updateRecord(record, deleteAction) } func (g *GoogleDNSProvider) HealthCheckReconciler() dns.HealthCheckReconciler { @@ -259,12 +300,15 @@ func (g *GoogleDNSProvider) ProviderSpecific() dns.ProviderSpecificLabels { return dns.ProviderSpecificLabels{} } -func (g *GoogleDNSProvider) updateRecord(dnsRecord *v1alpha1.DNSRecord, zoneID string, action action) error { +func (g *GoogleDNSProvider) updateRecord(dnsRecord *v1alpha1.DNSRecord, action action) error { // When updating records the Google DNS API expects you to delete any existing record and add the new one as part of // the same change request. The record to be deleted must match exactly what currently exists in the provider or the // change request will fail. To make sure we can always remove the records, we first get all records that exist in // the zone and build up the deleting list from `dnsRecord.Status` but use the most recent version of it retrieved // from the provider in the change request. + + zoneID := *dnsRecord.Spec.ZoneID + currentRecords, err := g.getResourceRecordSets(g.ctx, zoneID) if err != nil { return err diff --git a/pkg/dns/zone_id_filter.go b/pkg/dns/zone_id_filter.go new file mode 100644 index 000000000..209b23e99 --- /dev/null +++ b/pkg/dns/zone_id_filter.go @@ -0,0 +1,37 @@ +package dns + +import "strings" + +// ZoneIDFilter holds a list of zone ids to filter by +type ZoneIDFilter struct { + ZoneIDs []string +} + +// NewZoneIDFilter returns a new ZoneIDFilter given a list of zone ids +func NewZoneIDFilter(zoneIDs []string) ZoneIDFilter { + return ZoneIDFilter{zoneIDs} +} + +// Match checks whether a zone matches one of the provided zone ids +func (f ZoneIDFilter) Match(zoneID string) bool { + // An empty filter includes all zones. + if len(f.ZoneIDs) == 0 { + return true + } + + for _, id := range f.ZoneIDs { + if strings.HasSuffix(zoneID, id) { + return true + } + } + + return false +} + +// IsConfigured returns true if DomainFilter is configured, false otherwise +func (f ZoneIDFilter) IsConfigured() bool { + if len(f.ZoneIDs) == 1 { + return f.ZoneIDs[0] != "" + } + return len(f.ZoneIDs) > 0 +}