open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/common/common_status_update.go (about) 1 // Copyright Contributors to the Open Cluster Management project 2 3 package common 4 5 import ( 6 "context" 7 "fmt" 8 "reflect" 9 "sort" 10 11 k8serrors "k8s.io/apimachinery/pkg/api/errors" 12 "k8s.io/apimachinery/pkg/types" 13 clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" 14 appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1" 15 "sigs.k8s.io/controller-runtime/pkg/client" 16 17 policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1" 18 policiesv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1" 19 ) 20 21 // RootStatusUpdate updates the root policy status with bound decisions, placements, and cluster status. 22 func RootStatusUpdate(ctx context.Context, c client.Client, rootPolicy *policiesv1.Policy) (DecisionSet, error) { 23 placements, decisions, err := GetClusterDecisions(ctx, c, rootPolicy) 24 if err != nil { 25 log.Info("Failed to get any placement decisions. Giving up on the request.") 26 27 return nil, err 28 } 29 30 cpcs, cpcsErr := CalculatePerClusterStatus(ctx, c, rootPolicy, decisions) 31 if cpcsErr != nil { 32 // If there is a new replicated policy, then its lookup is expected to fail - it hasn't been created yet. 33 log.Error(cpcsErr, "Failed to get at least one replicated policy, but that may be expected. Ignoring.") 34 } 35 36 err = c.Get(ctx, 37 types.NamespacedName{ 38 Namespace: rootPolicy.Namespace, 39 Name: rootPolicy.Name, 40 }, rootPolicy) 41 if err != nil { 42 log.Error(err, "Failed to refresh the cached policy. Will use existing policy.") 43 } 44 45 complianceState := CalculateRootCompliance(cpcs) 46 47 if reflect.DeepEqual(rootPolicy.Status.Status, cpcs) && 48 rootPolicy.Status.ComplianceState == complianceState && 49 reflect.DeepEqual(rootPolicy.Status.Placement, placements) { 50 return decisions, nil 51 } 52 53 log.Info("Updating the root policy status", "RootPolicyName", rootPolicy.Name, "Namespace", rootPolicy.Namespace) 54 rootPolicy.Status.Status = cpcs 55 rootPolicy.Status.ComplianceState = complianceState 56 rootPolicy.Status.Placement = placements 57 58 err = c.Status().Update(ctx, rootPolicy) 59 if err != nil { 60 return nil, err 61 } 62 63 return decisions, nil 64 } 65 66 // GetPolicyPlacementDecisions retrieves the placement decisions for a input PlacementBinding when 67 // the policy is bound within it. It can return an error if the PlacementBinding is invalid, or if 68 // a required lookup fails. 69 func GetPolicyPlacementDecisions(ctx context.Context, c client.Client, 70 instance *policiesv1.Policy, pb *policiesv1.PlacementBinding, 71 ) (clusterDecisions []string, placements []*policiesv1.Placement, err error) { 72 policySubjectFound := false 73 policySetSubjects := make(map[string]struct{}) // a set, to prevent duplicates 74 75 for _, subject := range pb.Subjects { 76 if subject.APIGroup != policiesv1.SchemeGroupVersion.Group { 77 continue 78 } 79 80 switch subject.Kind { 81 case policiesv1.Kind: 82 if !policySubjectFound && subject.Name == instance.GetName() { 83 policySubjectFound = true 84 85 placements = append(placements, &policiesv1.Placement{ 86 PlacementBinding: pb.GetName(), 87 }) 88 } 89 case policiesv1.PolicySetKind: 90 if _, exists := policySetSubjects[subject.Name]; !exists { 91 policySetSubjects[subject.Name] = struct{}{} 92 93 if IsPolicyInPolicySet(ctx, c, instance.GetName(), subject.Name, pb.GetNamespace()) { 94 placements = append(placements, &policiesv1.Placement{ 95 PlacementBinding: pb.GetName(), 96 PolicySet: subject.Name, 97 }) 98 } 99 } 100 } 101 } 102 103 if len(placements) == 0 { 104 // None of the subjects in the PlacementBinding were relevant to this Policy. 105 return nil, nil, nil 106 } 107 108 // If the PlacementRef is invalid, log and return. (This is not recoverable.) 109 if !HasValidPlacementRef(pb) { 110 log.Info(fmt.Sprintf("Placement binding %s/%s placementRef is not valid. Ignoring.", pb.Namespace, pb.Name)) 111 112 return nil, nil, nil 113 } 114 115 // If the placementRef exists, then it needs to be added to the placement item 116 refNN := types.NamespacedName{ 117 Namespace: pb.GetNamespace(), 118 Name: pb.PlacementRef.Name, 119 } 120 121 switch pb.PlacementRef.Kind { 122 case "PlacementRule": 123 plr := &appsv1.PlacementRule{} 124 if err := c.Get(ctx, refNN, plr); err != nil && !k8serrors.IsNotFound(err) { 125 return nil, nil, fmt.Errorf("failed to check for PlacementRule '%v': %w", pb.PlacementRef.Name, err) 126 } 127 128 for i := range placements { 129 placements[i].PlacementRule = plr.Name // will be empty if the PlacementRule was not found 130 } 131 case "Placement": 132 pl := &clusterv1beta1.Placement{} 133 if err := c.Get(ctx, refNN, pl); err != nil && !k8serrors.IsNotFound(err) { 134 return nil, nil, fmt.Errorf("failed to check for Placement '%v': %w", pb.PlacementRef.Name, err) 135 } 136 137 for i := range placements { 138 placements[i].Placement = pl.Name // will be empty if the Placement was not found 139 } 140 } 141 142 // If there are no placements, then the PlacementBinding is not for this Policy. 143 if len(placements) == 0 { 144 return nil, nil, nil 145 } 146 147 // If the policy is disabled, don't return any decisions, so that the policy isn't put on any clusters 148 if instance.Spec.Disabled { 149 return nil, placements, nil 150 } 151 152 clusterDecisions, err = GetDecisions(ctx, c, pb) 153 154 return clusterDecisions, placements, err 155 } 156 157 type DecisionSet map[string]bool 158 159 // GetClusterDecisions identifies all managed clusters which should have a replicated policy using the root policy 160 // This returns unique decisions and placements that are NOT under Restricted subset. 161 // Also this function returns placements that are under restricted subset. 162 // But these placements include decisions which are under non-restricted subset. 163 // In other words, this function returns placements which include at least one decision under non-restricted subset. 164 func GetClusterDecisions( 165 ctx context.Context, 166 c client.Client, 167 rootPolicy *policiesv1.Policy, 168 ) ( 169 []*policiesv1.Placement, DecisionSet, error, 170 ) { 171 log := log.WithValues("policyName", rootPolicy.GetName(), "policyNamespace", rootPolicy.GetNamespace()) 172 decisions := make(map[string]bool) 173 174 pbList := &policiesv1.PlacementBindingList{} 175 176 err := c.List(ctx, pbList, &client.ListOptions{Namespace: rootPolicy.GetNamespace()}) 177 if err != nil { 178 log.Error(err, "Could not list the placement bindings") 179 180 return nil, decisions, err 181 } 182 183 placements := []*policiesv1.Placement{} 184 185 // Gather all placements and decisions when it is NOT policiesv1.Restricted 186 for i, pb := range pbList.Items { 187 if pb.SubFilter == policiesv1.Restricted { 188 continue 189 } 190 191 plcDecisions, plcPlacements, err := GetPolicyPlacementDecisions(ctx, c, rootPolicy, &pbList.Items[i]) 192 if err != nil { 193 return nil, nil, err 194 } 195 196 if len(plcDecisions) == 0 { 197 log.Info("No placement decisions to process for this policy from this non-restricted binding", 198 "policyName", rootPolicy.GetName(), "bindingName", pb.GetName()) 199 } 200 201 // Decisions are all unique 202 for _, clusterName := range plcDecisions { 203 decisions[clusterName] = true 204 } 205 206 placements = append(placements, plcPlacements...) 207 } 208 209 // Gather placements which have at least one decision that is included in NON-Restricted 210 for i, pb := range pbList.Items { 211 if pb.SubFilter != policiesv1.Restricted { 212 continue 213 } 214 215 foundInDecisions := false 216 217 plcDecisions, plcPlacements, err := GetPolicyPlacementDecisions(ctx, c, rootPolicy, &pbList.Items[i]) 218 if err != nil { 219 return nil, nil, err 220 } 221 222 if len(plcDecisions) == 0 { 223 log.Info("No placement decisions to process for this policy from this restricted binding", 224 "policyName", rootPolicy.GetName(), "bindingName", pb.GetName()) 225 } 226 227 // Decisions are all unique 228 for _, clusterName := range plcDecisions { 229 if _, ok := decisions[clusterName]; ok { 230 foundInDecisions = true 231 } 232 233 decisions[clusterName] = true 234 } 235 236 if foundInDecisions { 237 placements = append(placements, plcPlacements...) 238 } 239 } 240 241 log.V(2).Info("Sorting placements", "RootPolicyName", rootPolicy.Name, "Namespace", rootPolicy.Namespace) 242 sort.SliceStable(placements, func(i, j int) bool { 243 pi := placements[i].PlacementBinding + " " + placements[i].Placement + " " + 244 placements[i].PlacementRule + " " + placements[i].PolicySet 245 pj := placements[j].PlacementBinding + " " + placements[j].Placement + " " + 246 placements[j].PlacementRule + " " + placements[j].PolicySet 247 248 return pi < pj 249 }) 250 251 return placements, decisions, nil 252 } 253 254 // CalculatePerClusterStatus lists up all policies replicated from the input policy, and stores 255 // their compliance states in the result list. The result is sorted by cluster name. An error 256 // will be returned if lookup of a replicated policy fails, but all lookups will still be attempted. 257 func CalculatePerClusterStatus( 258 ctx context.Context, 259 c client.Client, 260 rootPolicy *policiesv1.Policy, 261 decisions DecisionSet, 262 ) ([]*policiesv1.CompliancePerClusterStatus, error) { 263 if rootPolicy.Spec.Disabled { 264 return nil, nil 265 } 266 267 status := make([]*policiesv1.CompliancePerClusterStatus, 0, len(decisions)) 268 var lookupErr error // save until end, to attempt all lookups 269 270 // Update the status based on the processed decisions 271 for clusterName := range decisions { 272 replicatedPolicy := &policiesv1.Policy{} 273 key := types.NamespacedName{ 274 Namespace: clusterName, Name: rootPolicy.Namespace + "." + rootPolicy.Name, 275 } 276 277 err := c.Get(ctx, key, replicatedPolicy) 278 if err != nil { 279 if k8serrors.IsNotFound(err) { 280 status = append(status, &policiesv1.CompliancePerClusterStatus{ 281 ClusterName: clusterName, 282 ClusterNamespace: clusterName, 283 }) 284 285 continue 286 } 287 288 lookupErr = err 289 } 290 291 status = append(status, &policiesv1.CompliancePerClusterStatus{ 292 ComplianceState: replicatedPolicy.Status.ComplianceState, 293 ClusterName: clusterName, 294 ClusterNamespace: clusterName, 295 }) 296 } 297 298 sort.Slice(status, func(i, j int) bool { 299 return status[i].ClusterName < status[j].ClusterName 300 }) 301 302 return status, lookupErr 303 } 304 305 func IsPolicyInPolicySet(ctx context.Context, c client.Client, policyName, policySetName, namespace string) bool { 306 log := log.WithValues("policyName", policyName, "policySetName", policySetName, "policyNamespace", namespace) 307 308 policySet := policiesv1beta1.PolicySet{} 309 setNN := types.NamespacedName{ 310 Name: policySetName, 311 Namespace: namespace, 312 } 313 314 if err := c.Get(ctx, setNN, &policySet); err != nil { 315 log.Error(err, "Failed to get the policyset") 316 317 return false 318 } 319 320 for _, plc := range policySet.Spec.Policies { 321 if string(plc) == policyName { 322 return true 323 } 324 } 325 326 return false 327 } 328 329 // CalculateRootCompliance uses the input per-cluster statuses to determine what a root policy's 330 // ComplianceState should be. General precedence is: NonCompliant > Pending > Unknown > Compliant. 331 func CalculateRootCompliance(clusters []*policiesv1.CompliancePerClusterStatus) policiesv1.ComplianceState { 332 if len(clusters) == 0 { 333 // No clusters == no status 334 return "" 335 } 336 337 unknownFound := false 338 pendingFound := false 339 340 for _, status := range clusters { 341 switch status.ComplianceState { 342 case policiesv1.NonCompliant: 343 // NonCompliant has the highest priority, so we can skip checking the others 344 return policiesv1.NonCompliant 345 case policiesv1.Pending: 346 pendingFound = true 347 case policiesv1.Compliant: 348 continue 349 default: 350 unknownFound = true 351 } 352 } 353 354 if pendingFound { 355 return policiesv1.Pending 356 } 357 358 if unknownFound { 359 return "" 360 } 361 362 // Returns compliant if, and only if, *all* cluster statuses are Compliant 363 return policiesv1.Compliant 364 }