open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/policyset/policyset_controller.go (about) 1 // Copyright (c) 2022 Red Hat, Inc. 2 // Copyright Contributors to the Open Cluster Management project 3 4 package controllers 5 6 import ( 7 "context" 8 "fmt" 9 "strings" 10 11 "k8s.io/apimachinery/pkg/api/equality" 12 "k8s.io/apimachinery/pkg/api/errors" 13 "k8s.io/apimachinery/pkg/runtime" 14 "k8s.io/apimachinery/pkg/types" 15 "k8s.io/client-go/tools/record" 16 clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" 17 appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1" 18 ctrl "sigs.k8s.io/controller-runtime" 19 "sigs.k8s.io/controller-runtime/pkg/builder" 20 "sigs.k8s.io/controller-runtime/pkg/client" 21 "sigs.k8s.io/controller-runtime/pkg/handler" 22 "sigs.k8s.io/controller-runtime/pkg/reconcile" 23 24 policyv1 "open-cluster-management.io/governance-policy-propagator/api/v1" 25 policyv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1" 26 "open-cluster-management.io/governance-policy-propagator/controllers/common" 27 ) 28 29 const ControllerName string = "policy-set" 30 31 var log = ctrl.Log.WithName(ControllerName) 32 33 // PolicySetReconciler reconciles a PolicySet object 34 type PolicySetReconciler struct { 35 client.Client 36 Scheme *runtime.Scheme 37 Recorder record.EventRecorder 38 } 39 40 // blank assignment to verify that PolicySetReconciler implements reconcile.Reconciler 41 var _ reconcile.Reconciler = &PolicySetReconciler{} 42 43 //+kubebuilder:rbac:groups=policy.open-cluster-management.io,resources=policysets,verbs=get;list;watch;create;update;patch;delete 44 //+kubebuilder:rbac:groups=policy.open-cluster-management.io,resources=policysets/status,verbs=get;update;patch 45 //+kubebuilder:rbac:groups=policy.open-cluster-management.io,resources=policysets/finalizers,verbs=update 46 47 func (r *PolicySetReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { 48 log := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 49 log.Info("Reconciling policy sets...") 50 // Fetch the PolicySet instance 51 instance := &policyv1beta1.PolicySet{} 52 53 err := r.Get(ctx, request.NamespacedName, instance) 54 if err != nil { 55 if errors.IsNotFound(err) { 56 // Request object not found, could have been deleted after reconcile request. 57 // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 58 // Return and don't requeue 59 log.Info("Policy set not found, so it may have been deleted.") 60 61 return reconcile.Result{}, nil 62 } 63 // Error reading the object - requeue the request. 64 log.Error(err, "Failed to retrieve policy set") 65 66 return reconcile.Result{}, err 67 } 68 69 log.V(1).Info("Policy set was found, processing it") 70 71 originalInstance := instance.DeepCopy() 72 setNeedsUpdate := r.processPolicySet(ctx, instance) 73 74 if setNeedsUpdate { 75 log.Info("Status update needed") 76 77 err := r.Status().Patch(ctx, instance, client.MergeFrom(originalInstance)) 78 if err != nil { 79 log.Error(err, "Failed to update policy set status") 80 81 return reconcile.Result{}, err 82 } 83 } 84 85 log.Info("Policy set successfully processed, reconcile complete.") 86 87 r.Recorder.Event( 88 instance, 89 "Normal", 90 fmt.Sprintf("policySet: %s", instance.GetName()), 91 fmt.Sprintf("Status successfully updated for policySet %s in namespace %s", instance.GetName(), 92 instance.GetNamespace()), 93 ) 94 95 return reconcile.Result{}, nil 96 } 97 98 // processPolicySet compares the status of a policyset to its desired state and determines whether an update is needed 99 func (r *PolicySetReconciler) processPolicySet(ctx context.Context, plcSet *policyv1beta1.PolicySet) bool { 100 log.V(1).Info("Processing policy sets") 101 102 needsUpdate := false 103 104 // compile results and compliance state from policy statuses 105 compliancesFound := []string{} 106 deletedPolicies := []string{} 107 unknownPolicies := []string{} 108 disabledPolicies := []string{} 109 pendingPolicies := []string{} 110 aggregatedCompliance := policyv1.Compliant 111 placementsByBinding := map[string]policyv1beta1.PolicySetStatusPlacement{} 112 113 // if there are no policies in the policyset, status should be empty 114 if len(plcSet.Spec.Policies) == 0 { 115 builtStatus := policyv1beta1.PolicySetStatus{} 116 117 if !equality.Semantic.DeepEqual(plcSet.Status, builtStatus) { 118 plcSet.Status = *builtStatus.DeepCopy() 119 120 return true 121 } 122 123 return false 124 } 125 126 for i := range plcSet.Spec.Policies { 127 childPlcName := plcSet.Spec.Policies[i] 128 childNamespacedName := types.NamespacedName{ 129 Name: string(childPlcName), 130 Namespace: plcSet.Namespace, 131 } 132 133 childPlc := &policyv1.Policy{} 134 135 err := r.Client.Get(ctx, childNamespacedName, childPlc) 136 if err != nil { 137 // policy does not exist, log error message and generate event 138 var errMessage string 139 if errors.IsNotFound(err) { 140 errMessage = string(childPlcName) + " not found" 141 } else { 142 split := strings.Split(err.Error(), "Policy.policy.open-cluster-management.io ") 143 if len(split) < 2 { 144 errMessage = err.Error() 145 } else { 146 errMessage = split[1] 147 } 148 } 149 150 log.V(2).Info(errMessage) 151 152 r.Recorder.Event(plcSet, "Warning", "PolicyNotFound", 153 fmt.Sprintf( 154 "Policy %s is in PolicySet %s but could not be found in namespace %s", 155 childPlcName, 156 plcSet.GetName(), 157 plcSet.GetNamespace(), 158 ), 159 ) 160 161 deletedPolicies = append(deletedPolicies, string(childPlcName)) 162 } else { 163 // aggregate placements 164 for _, placement := range childPlc.Status.Placement { 165 if placement.PolicySet == plcSet.GetName() { 166 placementsByBinding[placement.PlacementBinding] = plcPlacementToSetPlacement(*placement) 167 } 168 } 169 170 if childPlc.Spec.Disabled { 171 // policy is disabled, do not process compliance 172 disabledPolicies = append(disabledPolicies, string(childPlcName)) 173 174 continue 175 } 176 177 // create list of all relevant clusters 178 clusters := []string{} 179 for pbName := range placementsByBinding { 180 pbNamespacedName := types.NamespacedName{ 181 Name: pbName, 182 Namespace: plcSet.Namespace, 183 } 184 185 pb := &policyv1.PlacementBinding{} 186 187 err := r.Client.Get(ctx, pbNamespacedName, pb) 188 if err != nil { 189 if errors.IsNotFound(err) { 190 log.V(1).Info("The placement binding was not found", "placementBinding", pbName) 191 } else { 192 log.Error(err, "Failed to get the placement binding", "placementBinding", pbName) 193 } 194 195 continue 196 } 197 198 var clusterDecisions []string 199 clusterDecisions, err = common.GetDecisions(ctx, r.Client, pb) 200 if err != nil { 201 log.Error( 202 err, "Failed to get placement decisions for the placement binding", "placementBinding", pbName, 203 ) 204 205 continue 206 } 207 208 clusters = append(clusters, clusterDecisions...) 209 } 210 211 // aggregate compliance state 212 plcComplianceState := complianceInRelevantClusters(childPlc.Status.Status, clusters) 213 if plcComplianceState == "" { 214 unknownPolicies = append(unknownPolicies, string(childPlcName)) 215 } else { 216 if plcComplianceState == policyv1.Pending { 217 pendingPolicies = append(pendingPolicies, string(childPlcName)) 218 if aggregatedCompliance != policyv1.NonCompliant { 219 aggregatedCompliance = policyv1.Pending 220 } 221 } else { 222 compliancesFound = append(compliancesFound, string(childPlcName)) 223 if plcComplianceState == policyv1.NonCompliant { 224 aggregatedCompliance = policyv1.NonCompliant 225 } 226 } 227 } 228 } 229 } 230 231 generatedPlacements := []policyv1beta1.PolicySetStatusPlacement{} 232 for _, pcmt := range placementsByBinding { 233 generatedPlacements = append(generatedPlacements, pcmt) 234 } 235 236 builtStatus := policyv1beta1.PolicySetStatus{ 237 Placement: generatedPlacements, 238 StatusMessage: getStatusMessage(disabledPolicies, unknownPolicies, deletedPolicies, pendingPolicies), 239 } 240 if showCompliance(compliancesFound, unknownPolicies, pendingPolicies) { 241 builtStatus.Compliant = string(aggregatedCompliance) 242 } 243 244 if !equality.Semantic.DeepEqual(plcSet.Status, builtStatus) { 245 plcSet.Status = *builtStatus.DeepCopy() 246 needsUpdate = true 247 } 248 249 return needsUpdate 250 } 251 252 // getStatusMessage returns a message listing disabled, deleted and policies with no status 253 func getStatusMessage( 254 disabledPolicies []string, 255 unknownPolicies []string, 256 deletedPolicies []string, 257 pendingPolicies []string, 258 ) string { 259 statusMessage := "" 260 separator := "" 261 allReporting := true 262 263 if len(pendingPolicies) > 0 { 264 allReporting = false 265 statusMessage += fmt.Sprintf("Policies awaiting pending dependencies: %s", 266 strings.Join(pendingPolicies, ", ")) 267 separator = "; " 268 } 269 270 if len(disabledPolicies) > 0 { 271 allReporting = false 272 statusMessage += fmt.Sprintf(separator+"Disabled policies: %s", strings.Join(disabledPolicies, ", ")) 273 separator = "; " 274 } 275 276 if len(unknownPolicies) > 0 { 277 allReporting = false 278 statusMessage += fmt.Sprintf(separator+"No status provided while awaiting policy status: %s", 279 strings.Join(unknownPolicies, ", ")) 280 separator = "; " 281 } 282 283 if len(deletedPolicies) > 0 { 284 allReporting = false 285 statusMessage += fmt.Sprintf(separator+"Deleted policies: %s", strings.Join(deletedPolicies, ", ")) 286 } 287 288 if allReporting { 289 return "All policies are reporting status" 290 } 291 292 return statusMessage 293 } 294 295 // showCompliance only if there are policies with compliance and none are still awaiting status 296 func showCompliance(compliancesFound []string, unknown []string, pending []string) bool { 297 if len(unknown) > 0 { 298 return false 299 } 300 301 if len(compliancesFound)+len(pending) > 0 { 302 return true 303 } 304 305 return false 306 } 307 308 // SetupWithManager sets up the controller with the Manager. 309 func (r *PolicySetReconciler) SetupWithManager(mgr ctrl.Manager) error { 310 return ctrl.NewControllerManagedBy(mgr). 311 Named(ControllerName). 312 For( 313 &policyv1beta1.PolicySet{}, 314 builder.WithPredicates(policySetPredicateFuncs)). 315 Watches( 316 &policyv1.Policy{}, 317 handler.EnqueueRequestsFromMapFunc(policyMapper(mgr.GetClient())), 318 builder.WithPredicates(policyPredicateFuncs)). 319 Watches( 320 &policyv1.PlacementBinding{}, 321 handler.EnqueueRequestsFromMapFunc(placementBindingMapper(mgr.GetClient())), 322 builder.WithPredicates(pbPredicateFuncs)). 323 Watches( 324 &appsv1.PlacementRule{}, 325 handler.EnqueueRequestsFromMapFunc(placementRuleMapper(mgr.GetClient()))). 326 Watches( 327 &clusterv1beta1.PlacementDecision{}, 328 handler.EnqueueRequestsFromMapFunc(placementDecisionMapper(mgr.GetClient()))). 329 Complete(r) 330 } 331 332 // Helper function to filter out compliance statuses that are not in scope 333 func complianceInRelevantClusters( 334 status []*policyv1.CompliancePerClusterStatus, 335 relevantClusters []string, 336 ) policyv1.ComplianceState { 337 complianceFound := false 338 compliance := policyv1.Compliant 339 340 for i := range status { 341 if clusterInList(relevantClusters, status[i].ClusterName) { 342 if status[i].ComplianceState == policyv1.NonCompliant { 343 compliance = policyv1.NonCompliant 344 complianceFound = true 345 } else if status[i].ComplianceState == policyv1.Pending { 346 complianceFound = true 347 if compliance != policyv1.NonCompliant { 348 compliance = policyv1.Pending 349 } 350 } else if status[i].ComplianceState != "" { 351 complianceFound = true 352 } 353 } 354 } 355 356 if complianceFound { 357 return compliance 358 } 359 360 return "" 361 } 362 363 // helper function to check whether a cluster is in a list of clusters 364 func clusterInList(list []string, cluster string) bool { 365 for _, item := range list { 366 if item == cluster { 367 return true 368 } 369 } 370 371 return false 372 } 373 374 // Helper function to convert policy placement to policyset placement 375 func plcPlacementToSetPlacement(plcPlacement policyv1.Placement) policyv1beta1.PolicySetStatusPlacement { 376 return policyv1beta1.PolicySetStatusPlacement{ 377 PlacementBinding: plcPlacement.PlacementBinding, 378 Placement: plcPlacement.Placement, 379 PlacementRule: plcPlacement.PlacementRule, 380 } 381 }