diff --git a/cloud/services/domains.go b/cloud/services/domains.go index cfffbbee0..05a8a26b2 100644 --- a/cloud/services/domains.go +++ b/cloud/services/domains.go @@ -52,7 +52,7 @@ func EnsureDNSEntries(ctx context.Context, cscope *scope.ClusterScope, operation } if cscope.LinodeCluster.Spec.Network.DNSProvider == "akamai" { - if err := deleteStaleAkamaiEntries(ctx, cscope); err != nil { + if err := deleteStaleAkamaiEntries(ctx, cscope, dnsEntries); err != nil { return err } for _, dnsEntry := range dnsEntries { @@ -69,29 +69,6 @@ func EnsureDNSEntries(ctx context.Context, cscope *scope.ClusterScope, operation return nil } -func getMachineIPs(cscope *scope.ClusterScope) (ipv4IPs, ipv6IPs []string, err error) { - for _, eachMachine := range cscope.LinodeMachines.Items { - if !eachMachine.Status.Ready { - continue - } - for _, IPs := range eachMachine.Status.Addresses { - if IPs.Type != v1beta2.MachineExternalIP { - continue - } - addr, err := netip.ParseAddr(IPs.Address) - if err != nil { - return nil, nil, fmt.Errorf("not a valid IP %w", err) - } - if addr.Is4() { - ipv4IPs = append(ipv4IPs, IPs.Address) - } else { - ipv6IPs = append(ipv6IPs, IPs.Address) - } - } - } - return ipv4IPs, ipv6IPs, nil -} - func resetAkamaiRecord(ctx context.Context, cscope *scope.ClusterScope, recordResponse *dns.GetRecordResponse, machineIPList []string, rootDomain string) error { freshEntries := make([]string, 0) for _, ip := range recordResponse.Target { @@ -121,12 +98,8 @@ func resetAkamaiRecord(ctx context.Context, cscope *scope.ClusterScope, recordRe }) } -func deleteStaleAkamaiEntries(ctx context.Context, cscope *scope.ClusterScope) error { - ipv4IPs, ipv6IPs, err := getMachineIPs(cscope) - if err != nil { - return err - } - +func deleteStaleAkamaiEntries(ctx context.Context, cscope *scope.ClusterScope, dnsEntries []DNSOptions) error { + ipv4IPs, ipv6IPs := getDNSMachineIPs(dnsEntries) rootDomain := cscope.LinodeCluster.Spec.Network.DNSRootDomain fqdn := getSubDomain(cscope) + "." + rootDomain @@ -165,12 +138,20 @@ func deleteStaleAkamaiEntries(ctx context.Context, cscope *scope.ClusterScope) e return nil } -func deleteStaleLinodeEntries(ctx context.Context, cscope *scope.ClusterScope, domainRecords []linodego.DomainRecord, domainID int) error { - ipv4IPs, ipv6IPs, err := getMachineIPs(cscope) - if err != nil { - return err +func getDNSMachineIPs(dnsEntries []DNSOptions) (ipv4IPs, ipv6IPs []string) { + for _, entry := range dnsEntries { + if entry.DNSRecordType == linodego.RecordTypeA { + ipv4IPs = append(ipv4IPs, entry.Target) + } + if entry.DNSRecordType == linodego.RecordTypeAAAA { + ipv6IPs = append(ipv6IPs, entry.Target) + } } + return ipv4IPs, ipv6IPs +} +func deleteStaleLinodeEntries(ctx context.Context, cscope *scope.ClusterScope, domainRecords []linodego.DomainRecord, domainID int, dnsEntries []DNSOptions) error { + ipv4IPs, ipv6IPs := getDNSMachineIPs(dnsEntries) if len(domainRecords) > 0 { for _, record := range domainRecords { if record.Type == linodego.RecordTypeTXT { @@ -207,7 +188,7 @@ func EnsureLinodeDNSEntries(ctx context.Context, cscope *scope.ClusterScope, ope return err } - if err := deleteStaleLinodeEntries(ctx, cscope, domainRecords, domainID); err != nil { + if err := deleteStaleLinodeEntries(ctx, cscope, domainRecords, domainID, dnsEntries); err != nil { return err } diff --git a/cloud/services/domains_test.go b/cloud/services/domains_test.go index e1656989a..e7bf7aeb3 100644 --- a/cloud/services/domains_test.go +++ b/cloud/services/domains_test.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "strings" "testing" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v12/pkg/dns" @@ -1121,6 +1122,136 @@ func TestAddIPToDNS(t *testing.T) { } } +func TestEnsureLinodeDNSDeletesRecordForDeletingCAPIMachine(t *testing.T) { + t.Parallel() + + clusterScope := &scope.ClusterScope{ + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + UID: "test-uid", + }, + }, + LinodeCluster: &infrav1alpha2.LinodeCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + UID: "test-uid", + }, + Spec: infrav1alpha2.LinodeClusterSpec{ + Network: infrav1alpha2.NetworkSpec{ + LoadBalancerType: "dns", + DNSRootDomain: "lkedevs.net", + DNSUniqueIdentifier: "test-hash", + }, + }, + }, + LinodeMachines: infrav1alpha2.LinodeMachineList{ + Items: []infrav1alpha2.LinodeMachine{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "deleting-machine", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Machine", + Name: "deleting-machine", + UID: "deleting-machine-uid", + }}, + }, + Status: infrav1alpha2.LinodeMachineStatus{ + Addresses: []clusterv1.MachineAddress{{ + Type: clusterv1.MachineExternalIP, + Address: "10.10.10.10", + }}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "active-machine", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Machine", + Name: "active-machine", + UID: "active-machine-uid", + }}, + }, + Status: infrav1alpha2.LinodeMachineStatus{ + Addresses: []clusterv1.MachineAddress{{ + Type: clusterv1.MachineExternalIP, + Address: "10.20.20.20", + }}, + }, + }, + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDNSClient := mock.NewMockLinodeClient(ctrl) + clusterScope.LinodeDomainsClient = mockDNSClient + mockDNSClient.EXPECT().ListDomains(gomock.Any(), gomock.Any()).Return([]linodego.Domain{{ + ID: 1, + Domain: "lkedevs.net", + }}, nil) + mockDNSClient.EXPECT().ListDomainRecords(gomock.Any(), 1, gomock.Any()).DoAndReturn( + func(_ context.Context, _ int, opts *linodego.ListOptions) ([]linodego.DomainRecord, error) { + if strings.Contains(opts.Filter, `"name":"test-cluster-test-hash"`) && !strings.Contains(opts.Filter, `"target"`) { + return []linodego.DomainRecord{ + { + ID: 100, + Type: linodego.RecordTypeA, + Name: "test-cluster-test-hash", + Target: "10.10.10.10", + }, + { + ID: 101, + Type: linodego.RecordTypeA, + Name: "test-cluster-test-hash", + Target: "10.20.20.20", + }, + { + ID: 102, + Type: linodego.RecordTypeTXT, + Name: "test-cluster-test-hash", + Target: "test-cluster", + }, + }, nil + } + return []linodego.DomainRecord{{ID: 101}}, nil + }).AnyTimes() + mockDNSClient.EXPECT().DeleteDomainRecord(gomock.Any(), 1, 100).Return(nil) + + mockK8sClient := mock.NewMockK8sClient(ctrl) + clusterScope.Client = mockK8sClient + mockK8sClient.EXPECT().Scheme().Return(nil).AnyTimes() + mockK8sClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { + machine, ok := obj.(*clusterv1.Machine) + if !ok { + return nil + } + + machine.Name = key.Name + machine.Namespace = key.Namespace + switch key.Name { + case "deleting-machine": + deletionTime := metav1.Now() + machine.DeletionTimestamp = &deletionTime + machine.UID = "deleting-machine-uid" + case "active-machine": + machine.UID = "active-machine-uid" + machine.Status.Conditions = []metav1.Condition{{ + Type: clusterv1.ReadyCondition, + Status: metav1.ConditionTrue, + }} + } + return nil + }).AnyTimes() + + require.NoError(t, EnsureDNSEntries(t.Context(), clusterScope, "create")) +} + func TestDeleteIPFromDNS(t *testing.T) { t.Parallel() tests := []struct { diff --git a/internal/controller/linodecluster_controller.go b/internal/controller/linodecluster_controller.go index b8473eebb..0926d3218 100644 --- a/internal/controller/linodecluster_controller.go +++ b/internal/controller/linodecluster_controller.go @@ -38,7 +38,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" "github.com/linode/cluster-api-provider-linode/cloud/scope" @@ -533,6 +535,22 @@ func (r *LinodeClusterReconciler) SetupWithManager(mgr ctrl.Manager, options crc ), builder.WithPredicates(predicates.ClusterPausedTransitionsOrInfrastructureProvisioned(mgr.GetScheme(), mgr.GetLogger())), ). + Watches( + &clusterv1.Machine{}, + handler.EnqueueRequestsFromMapFunc(machineToLinodeCluster(r.TracedClient(), mgr.GetLogger())), + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return false }, + UpdateFunc: func(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + _, isControlPlane := e.ObjectNew.GetLabels()[clusterv1.MachineControlPlaneLabel] + return isControlPlane + }, + DeleteFunc: func(event.DeleteEvent) bool { return false }, + GenericFunc: func(event.GenericEvent) bool { return false }, + }), + ). Watches( &infrav1alpha2.LinodeMachine{}, handler.EnqueueRequestsFromMapFunc(linodeMachineToLinodeCluster(r.TracedClient(), mgr.GetLogger())), diff --git a/internal/controller/linodecluster_controller_helpers.go b/internal/controller/linodecluster_controller_helpers.go index 1313adee6..6bff5b954 100644 --- a/internal/controller/linodecluster_controller_helpers.go +++ b/internal/controller/linodecluster_controller_helpers.go @@ -143,6 +143,43 @@ func buildPortCombosForIP(ip string, apiServerLBPort int, additionalPorts []infr return results } +func machineToLinodeCluster(tracedClient client.Client, logger logr.Logger) handler.MapFunc { + logger = logger.WithName("LinodeClusterReconciler").WithName("MachineToLinodeCluster") + + return func(ctx context.Context, o client.Object) []ctrl.Request { + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout) + defer cancel() + + machine, ok := o.(*clusterv1.Machine) + if !ok { + logger.Info("Failed to cast object to Machine") + return nil + } + + linodeCluster := infrav1alpha2.LinodeCluster{} + if err := tracedClient.Get( + ctx, + types.NamespacedName{ + Name: machine.Labels[clusterv1.ClusterNameLabel], + Namespace: machine.Namespace, + }, + &linodeCluster); err != nil { + logger.Info("Failed to get LinodeCluster") + return nil + } + + result := make([]ctrl.Request, 0, 1) + result = append(result, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: linodeCluster.Namespace, + Name: linodeCluster.Name, + }, + }) + + return result + } +} + func linodeMachineToLinodeCluster(tracedClient client.Client, logger logr.Logger) handler.MapFunc { logger = logger.WithName("LinodeClusterReconciler").WithName("linodeMachineToLinodeCluster")