sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/resource_reconciler.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 "github.com/Azure/azure-service-operator/v2/pkg/genruntime/conditions" 25 "github.com/go-logr/logr" 26 apierrors "k8s.io/apimachinery/pkg/api/errors" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/klog/v2" 31 infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1" 32 "sigs.k8s.io/cluster-api-provider-azure/pkg/mutators" 33 "sigs.k8s.io/cluster-api-provider-azure/util/tele" 34 "sigs.k8s.io/controller-runtime/pkg/client" 35 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 36 "sigs.k8s.io/controller-runtime/pkg/handler" 37 "sigs.k8s.io/controller-runtime/pkg/predicate" 38 "sigs.k8s.io/controller-runtime/pkg/reconcile" 39 ) 40 41 // ResourceReconciler reconciles a set of arbitrary ASO resources. 42 type ResourceReconciler struct { 43 client.Client 44 resources []*unstructured.Unstructured 45 owner resourceStatusObject 46 watcher watcher 47 } 48 49 type watcher interface { 50 Watch(log logr.Logger, obj client.Object, handler handler.EventHandler, p ...predicate.Predicate) error 51 } 52 53 type resourceStatusObject interface { 54 client.Object 55 GetResourceStatuses() []infrav1alpha.ResourceStatus 56 SetResourceStatuses([]infrav1alpha.ResourceStatus) 57 } 58 59 // Reconcile creates or updates the specified resources. 60 func (r *ResourceReconciler) Reconcile(ctx context.Context) error { 61 ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Reconcile") 62 defer done() 63 log.V(4).Info("reconciling resources") 64 65 var newResourceStatuses []infrav1alpha.ResourceStatus 66 67 for _, spec := range r.resources { 68 gvk := spec.GroupVersionKind() 69 spec.SetNamespace(r.owner.GetNamespace()) 70 71 log := log.WithValues("resource", klog.KObj(spec), "resourceVersion", gvk.GroupVersion(), "resourceKind", gvk.Kind) 72 73 if err := controllerutil.SetControllerReference(r.owner, spec, r.Scheme()); err != nil { 74 return fmt.Errorf("failed to set owner reference: %w", err) 75 } 76 77 if err := r.watcher.Watch(log, spec, handler.EnqueueRequestForOwner(r.Client.Scheme(), r.Client.RESTMapper(), r.owner)); err != nil { 78 return fmt.Errorf("failed to watch resource: %w", err) 79 } 80 81 log.V(4).Info("applying resource") 82 err := r.Patch(ctx, spec, client.Apply, client.FieldOwner("capz-manager"), client.ForceOwnership) 83 if err != nil { 84 return fmt.Errorf("failed to apply resource: %w", err) 85 } 86 87 ready, err := readyStatus(ctx, spec) 88 if err != nil { 89 return fmt.Errorf("failed to get ready status: %w", err) 90 } 91 newResourceStatuses = append(newResourceStatuses, infrav1alpha.ResourceStatus{ 92 Resource: infrav1alpha.StatusResource{ 93 Group: gvk.Group, 94 Version: gvk.Version, 95 Kind: gvk.Kind, 96 Name: spec.GetName(), 97 }, 98 Ready: ready, 99 }) 100 } 101 102 for _, oldStatus := range r.owner.GetResourceStatuses() { 103 needsDelete := true 104 for _, newStatus := range newResourceStatuses { 105 if oldStatus.Resource.Group == newStatus.Resource.Group && 106 oldStatus.Resource.Kind == newStatus.Resource.Kind && 107 oldStatus.Resource.Name == newStatus.Resource.Name { 108 needsDelete = false 109 break 110 } 111 } 112 113 if needsDelete { 114 updatedStatus, err := r.deleteResource(ctx, oldStatus.Resource) 115 if err != nil { 116 return err 117 } 118 if updatedStatus != nil { 119 newResourceStatuses = append(newResourceStatuses, *updatedStatus) 120 } 121 } 122 } 123 124 r.owner.SetResourceStatuses(newResourceStatuses) 125 126 return nil 127 } 128 129 // Pause pauses reconciliation of the specified resources. 130 func (r *ResourceReconciler) Pause(ctx context.Context) error { 131 ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Pause") 132 defer done() 133 log.V(4).Info("pausing resources") 134 135 err := mutators.Pause(ctx, r.resources) 136 if err != nil { 137 if errors.As(err, &mutators.Incompatible{}) { 138 err = reconcile.TerminalError(err) 139 } 140 return err 141 } 142 143 for _, spec := range r.resources { 144 gvk := spec.GroupVersionKind() 145 spec.SetNamespace(r.owner.GetNamespace()) 146 147 log := log.WithValues("resource", klog.KObj(spec), "resourceVersion", gvk.GroupVersion(), "resourceKind", gvk.Kind) 148 149 log.V(4).Info("pausing resource") 150 err := r.Patch(ctx, spec, client.Apply, client.FieldOwner("capz-manager")) 151 if err != nil { 152 return fmt.Errorf("failed to patch resource: %w", err) 153 } 154 } 155 156 return nil 157 } 158 159 // Delete deletes the specified resources. 160 func (r *ResourceReconciler) Delete(ctx context.Context) error { 161 ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Delete") 162 defer done() 163 log.V(4).Info("deleting resources") 164 165 var newResourceStatuses []infrav1alpha.ResourceStatus 166 167 for _, spec := range r.owner.GetResourceStatuses() { 168 newStatus, err := r.deleteResource(ctx, spec.Resource) 169 if err != nil { 170 return err 171 } 172 if newStatus != nil { 173 newResourceStatuses = append(newResourceStatuses, *newStatus) 174 } 175 } 176 177 r.owner.SetResourceStatuses(newResourceStatuses) 178 179 return nil 180 } 181 182 func (r *ResourceReconciler) deleteResource(ctx context.Context, resource infrav1alpha.StatusResource) (*infrav1alpha.ResourceStatus, error) { 183 ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.deleteResource") 184 defer done() 185 186 spec := &unstructured.Unstructured{} 187 spec.SetGroupVersionKind(schema.GroupVersionKind{Group: resource.Group, Version: resource.Version, Kind: resource.Kind}) 188 spec.SetNamespace(r.owner.GetNamespace()) 189 spec.SetName(resource.Name) 190 191 log = log.WithValues("resource", klog.KObj(spec), "resourceVersion", spec.GroupVersionKind().GroupVersion(), "resourceKind", spec.GetKind()) 192 193 log.V(4).Info("deleting resource") 194 err := r.Client.Delete(ctx, spec) 195 if apierrors.IsNotFound(err) { 196 log.V(4).Info("resource has been deleted") 197 return nil, nil 198 } 199 if err != nil { 200 return nil, fmt.Errorf("failed to delete resource: %w", err) 201 } 202 203 err = r.Client.Get(ctx, client.ObjectKeyFromObject(spec), spec) 204 if apierrors.IsNotFound(err) { 205 log.V(4).Info("resource has been deleted") 206 return nil, nil 207 } 208 if err != nil { 209 return nil, fmt.Errorf("failed to get resource: %w", err) 210 } 211 ready, err := readyStatus(ctx, spec) 212 if err != nil { 213 return nil, fmt.Errorf("failed to get ready status: %w", err) 214 } 215 216 return &infrav1alpha.ResourceStatus{ 217 Resource: resource, 218 Ready: ready, 219 }, nil 220 } 221 222 func readyStatus(ctx context.Context, u *unstructured.Unstructured) (bool, error) { 223 _, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.readyStatus") 224 defer done() 225 226 statusConditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions") 227 if err != nil { 228 return false, err 229 } 230 if !found { 231 return false, nil 232 } 233 234 for _, el := range statusConditions { 235 condition, ok := el.(map[string]interface{}) 236 if !ok { 237 continue 238 } 239 condType, found, err := unstructured.NestedString(condition, "type") 240 if !found || err != nil || condType != conditions.ConditionTypeReady { 241 continue 242 } 243 244 observedGen, _, err := unstructured.NestedInt64(condition, "observedGeneration") 245 if err != nil { 246 return false, err 247 } 248 if observedGen < u.GetGeneration() { 249 log.V(4).Info("waiting for ASO to reconcile the resource") 250 return false, nil 251 } 252 253 readyStatus, _, err := unstructured.NestedString(condition, "status") 254 if err != nil { 255 return false, err 256 } 257 return readyStatus == string(metav1.ConditionTrue), nil 258 } 259 260 // no ready condition is set 261 return false, nil 262 }