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 +}