sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/azureasomanagedcluster_controller.go (about) 1 /* 2 Copyright 2024 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package controllers 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1" 27 "sigs.k8s.io/cluster-api-provider-azure/pkg/mutators" 28 "sigs.k8s.io/cluster-api-provider-azure/util/tele" 29 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 30 "sigs.k8s.io/cluster-api/controllers/external" 31 "sigs.k8s.io/cluster-api/util" 32 "sigs.k8s.io/cluster-api/util/annotations" 33 "sigs.k8s.io/cluster-api/util/patch" 34 "sigs.k8s.io/cluster-api/util/predicates" 35 ctrl "sigs.k8s.io/controller-runtime" 36 "sigs.k8s.io/controller-runtime/pkg/builder" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 "sigs.k8s.io/controller-runtime/pkg/controller" 39 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 40 "sigs.k8s.io/controller-runtime/pkg/event" 41 "sigs.k8s.io/controller-runtime/pkg/handler" 42 "sigs.k8s.io/controller-runtime/pkg/predicate" 43 "sigs.k8s.io/controller-runtime/pkg/reconcile" 44 ) 45 46 var errInvalidControlPlaneKind = errors.New("AzureASOManagedCluster cannot be used without AzureASOManagedControlPlane") 47 48 // AzureASOManagedClusterReconciler reconciles a AzureASOManagedCluster object. 49 type AzureASOManagedClusterReconciler struct { 50 client.Client 51 WatchFilterValue string 52 53 newResourceReconciler func(*infrav1alpha.AzureASOManagedCluster, []*unstructured.Unstructured) resourceReconciler 54 } 55 56 type resourceReconciler interface { 57 // Reconcile reconciles resources defined by this object and updates this object's status to reflect the 58 // state of the specified resources. 59 Reconcile(context.Context) error 60 61 // Pause stops ASO from continuously reconciling the specified resources. 62 Pause(context.Context) error 63 64 // Delete begins deleting the specified resources and updates the object's status to reflect the state of 65 // the specified resources. 66 Delete(context.Context) error 67 } 68 69 // SetupWithManager sets up the controller with the Manager. 70 func (r *AzureASOManagedClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { 71 ctx, log, done := tele.StartSpanWithLogger(ctx, 72 "controllers.AzureASOManagedClusterReconciler.SetupWithManager", 73 tele.KVP("controller", infrav1alpha.AzureASOManagedClusterKind), 74 ) 75 defer done() 76 77 c, err := ctrl.NewControllerManagedBy(mgr). 78 WithOptions(options). 79 For(&infrav1alpha.AzureASOManagedCluster{}). 80 WithEventFilter(predicates.ResourceHasFilterLabel(log, r.WatchFilterValue)). 81 WithEventFilter(predicates.ResourceIsNotExternallyManaged(log)). 82 // Watch clusters for pause/unpause notifications 83 Watches( 84 &clusterv1.Cluster{}, 85 handler.EnqueueRequestsFromMapFunc( 86 util.ClusterToInfrastructureMapFunc(ctx, infrav1alpha.GroupVersion.WithKind(infrav1alpha.AzureASOManagedClusterKind), mgr.GetClient(), &infrav1alpha.AzureASOManagedCluster{}), 87 ), 88 builder.WithPredicates( 89 predicates.ResourceHasFilterLabel(log, r.WatchFilterValue), 90 ClusterUpdatePauseChange(log), 91 ), 92 ). 93 Watches( 94 &infrav1alpha.AzureASOManagedControlPlane{}, 95 handler.EnqueueRequestsFromMapFunc(asoManagedControlPlaneToManagedClusterMap(r.Client)), 96 builder.WithPredicates( 97 predicates.ResourceHasFilterLabel(log, r.WatchFilterValue), 98 predicate.Funcs{ 99 CreateFunc: func(ev event.CreateEvent) bool { 100 controlPlane := ev.Object.(*infrav1alpha.AzureASOManagedControlPlane) 101 return !controlPlane.Status.ControlPlaneEndpoint.IsZero() 102 }, 103 UpdateFunc: func(ev event.UpdateEvent) bool { 104 oldControlPlane := ev.ObjectOld.(*infrav1alpha.AzureASOManagedControlPlane) 105 newControlPlane := ev.ObjectNew.(*infrav1alpha.AzureASOManagedControlPlane) 106 return oldControlPlane.Status.ControlPlaneEndpoint != 107 newControlPlane.Status.ControlPlaneEndpoint 108 }, 109 }, 110 ), 111 ). 112 Build(r) 113 if err != nil { 114 return err 115 } 116 117 externalTracker := &external.ObjectTracker{ 118 Cache: mgr.GetCache(), 119 Controller: c, 120 } 121 122 r.newResourceReconciler = func(asoManagedCluster *infrav1alpha.AzureASOManagedCluster, resources []*unstructured.Unstructured) resourceReconciler { 123 return &ResourceReconciler{ 124 Client: r.Client, 125 resources: resources, 126 owner: asoManagedCluster, 127 watcher: externalTracker, 128 } 129 } 130 131 return nil 132 } 133 134 func asoManagedControlPlaneToManagedClusterMap(c client.Client) handler.MapFunc { 135 return func(ctx context.Context, o client.Object) []reconcile.Request { 136 asoManagedControlPlane := o.(*infrav1alpha.AzureASOManagedControlPlane) 137 138 cluster, err := util.GetOwnerCluster(ctx, c, asoManagedControlPlane.ObjectMeta) 139 if err != nil { 140 return nil 141 } 142 143 if cluster == nil || 144 cluster.Spec.InfrastructureRef == nil || 145 cluster.Spec.InfrastructureRef.APIVersion != infrav1alpha.GroupVersion.Identifier() || 146 cluster.Spec.InfrastructureRef.Kind != infrav1alpha.AzureASOManagedClusterKind { 147 return nil 148 } 149 150 return []reconcile.Request{ 151 { 152 NamespacedName: client.ObjectKey{ 153 Namespace: cluster.Spec.InfrastructureRef.Namespace, 154 Name: cluster.Spec.InfrastructureRef.Name, 155 }, 156 }, 157 } 158 } 159 } 160 161 //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters,verbs=get;list;watch;create;update;patch;delete 162 //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters/status,verbs=get;update;patch 163 //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters/finalizers,verbs=update 164 165 // Reconcile reconciles an AzureASOManagedCluster. 166 func (r *AzureASOManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, resultErr error) { 167 ctx, _, done := tele.StartSpanWithLogger(ctx, 168 "controllers.AzureASOManagedClusterReconciler.Reconcile", 169 tele.KVP("namespace", req.Namespace), 170 tele.KVP("name", req.Name), 171 tele.KVP("kind", infrav1alpha.AzureASOManagedClusterKind), 172 ) 173 defer done() 174 175 asoManagedCluster := &infrav1alpha.AzureASOManagedCluster{} 176 err := r.Get(ctx, req.NamespacedName, asoManagedCluster) 177 if err != nil { 178 return ctrl.Result{}, client.IgnoreNotFound(err) 179 } 180 181 patchHelper, err := patch.NewHelper(asoManagedCluster, r.Client) 182 if err != nil { 183 return ctrl.Result{}, fmt.Errorf("failed to create patch helper: %w", err) 184 } 185 defer func() { 186 err := patchHelper.Patch(ctx, asoManagedCluster) 187 if err != nil && resultErr == nil { 188 resultErr = err 189 result = ctrl.Result{} 190 } 191 }() 192 193 asoManagedCluster.Status.Ready = false 194 195 cluster, err := util.GetOwnerCluster(ctx, r.Client, asoManagedCluster.ObjectMeta) 196 if err != nil { 197 return ctrl.Result{}, err 198 } 199 200 if cluster != nil && cluster.Spec.Paused || 201 annotations.HasPaused(asoManagedCluster) { 202 return r.reconcilePaused(ctx, asoManagedCluster) 203 } 204 205 if !asoManagedCluster.GetDeletionTimestamp().IsZero() { 206 return r.reconcileDelete(ctx, asoManagedCluster) 207 } 208 209 return r.reconcileNormal(ctx, asoManagedCluster, cluster) 210 } 211 212 func (r *AzureASOManagedClusterReconciler) reconcileNormal(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster, cluster *clusterv1.Cluster) (ctrl.Result, error) { 213 ctx, log, done := tele.StartSpanWithLogger(ctx, 214 "controllers.AzureASOManagedClusterReconciler.reconcileNormal", 215 ) 216 defer done() 217 log.V(4).Info("reconciling normally") 218 219 if cluster == nil { 220 log.V(4).Info("Cluster Controller has not yet set OwnerRef") 221 return ctrl.Result{}, nil 222 } 223 if cluster.Spec.ControlPlaneRef == nil || 224 cluster.Spec.ControlPlaneRef.APIVersion != infrav1alpha.GroupVersion.Identifier() || 225 cluster.Spec.ControlPlaneRef.Kind != infrav1alpha.AzureASOManagedControlPlaneKind { 226 return ctrl.Result{}, reconcile.TerminalError(errInvalidControlPlaneKind) 227 } 228 229 needsPatch := controllerutil.AddFinalizer(asoManagedCluster, clusterv1.ClusterFinalizer) 230 needsPatch = AddBlockMoveAnnotation(asoManagedCluster) || needsPatch 231 if needsPatch { 232 return ctrl.Result{Requeue: true}, nil 233 } 234 235 resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources) 236 if err != nil { 237 return ctrl.Result{}, err 238 } 239 resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources) 240 err = resourceReconciler.Reconcile(ctx) 241 if err != nil { 242 return ctrl.Result{}, fmt.Errorf("failed to reconcile resources: %w", err) 243 } 244 for _, status := range asoManagedCluster.Status.Resources { 245 if !status.Ready { 246 return ctrl.Result{}, nil 247 } 248 } 249 250 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 251 ObjectMeta: metav1.ObjectMeta{ 252 Namespace: cluster.Spec.ControlPlaneRef.Namespace, 253 Name: cluster.Spec.ControlPlaneRef.Name, 254 }, 255 } 256 err = r.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane) 257 if client.IgnoreNotFound(err) != nil { 258 return ctrl.Result{}, fmt.Errorf("failed to get AzureASOManagedControlPlane %s/%s: %w", asoManagedControlPlane.Namespace, asoManagedControlPlane.Name, err) 259 } 260 asoManagedCluster.Spec.ControlPlaneEndpoint = asoManagedControlPlane.Status.ControlPlaneEndpoint 261 262 asoManagedCluster.Status.Ready = !asoManagedCluster.Spec.ControlPlaneEndpoint.IsZero() 263 264 return ctrl.Result{}, nil 265 } 266 267 //nolint:unparam // an empty ctrl.Result is always returned here, leaving it as-is to avoid churn in refactoring later if that changes. 268 func (r *AzureASOManagedClusterReconciler) reconcilePaused(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster) (ctrl.Result, error) { 269 ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.AzureASOManagedClusterReconciler.reconcilePaused") 270 defer done() 271 log.V(4).Info("reconciling pause") 272 273 resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources) 274 if err != nil { 275 return ctrl.Result{}, err 276 } 277 resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources) 278 err = resourceReconciler.Pause(ctx) 279 if err != nil { 280 return ctrl.Result{}, fmt.Errorf("failed to pause resources: %w", err) 281 } 282 283 RemoveBlockMoveAnnotation(asoManagedCluster) 284 285 return ctrl.Result{}, nil 286 } 287 288 //nolint:unparam // an empty ctrl.Result is always returned here, leaving it as-is to avoid churn in refactoring later if that changes. 289 func (r *AzureASOManagedClusterReconciler) reconcileDelete(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster) (ctrl.Result, error) { 290 ctx, log, done := tele.StartSpanWithLogger(ctx, 291 "controllers.AzureASOManagedClusterReconciler.reconcileDelete", 292 ) 293 defer done() 294 log.V(4).Info("reconciling delete") 295 296 resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources) 297 if err != nil { 298 return ctrl.Result{}, err 299 } 300 resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources) 301 err = resourceReconciler.Delete(ctx) 302 if err != nil { 303 return ctrl.Result{}, fmt.Errorf("failed to reconcile resources: %w", err) 304 } 305 if len(asoManagedCluster.Status.Resources) > 0 { 306 return ctrl.Result{}, nil 307 } 308 309 controllerutil.RemoveFinalizer(asoManagedCluster, clusterv1.ClusterFinalizer) 310 return ctrl.Result{}, nil 311 }