open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/common/common.go (about) 1 // Copyright (c) 2020 Red Hat, Inc. 2 // Copyright Contributors to the Open Cluster Management project 3 4 // +kubebuilder:skip 5 package common 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "strings" 13 14 k8serrors "k8s.io/apimachinery/pkg/api/errors" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/runtime" 17 "k8s.io/apimachinery/pkg/types" 18 clusterv1 "open-cluster-management.io/api/cluster/v1" 19 clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" 20 appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1" 21 ctrl "sigs.k8s.io/controller-runtime" 22 "sigs.k8s.io/controller-runtime/pkg/client" 23 "sigs.k8s.io/controller-runtime/pkg/reconcile" 24 25 policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1" 26 policiesv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1" 27 ) 28 29 const ( 30 APIGroup string = "policy.open-cluster-management.io" 31 ClusterNameLabel string = APIGroup + "/cluster-name" 32 ClusterNamespaceLabel string = APIGroup + "/cluster-namespace" 33 RootPolicyLabel string = APIGroup + "/root-policy" 34 ) 35 36 var ( 37 log = ctrl.Log.WithName("common") 38 ErrInvalidLabelValue = errors.New("unexpected format of label value") 39 ) 40 41 type GuttedObject struct { 42 metav1.TypeMeta `json:",inline"` 43 metav1.ObjectMeta `json:"metadata,omitempty"` 44 } 45 46 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 47 func (in *GuttedObject) DeepCopyInto(out *GuttedObject) { 48 *out = *in 49 out.TypeMeta = in.TypeMeta 50 in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 51 } 52 53 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuttedObject. 54 func (in *GuttedObject) DeepCopy() *GuttedObject { 55 if in == nil { 56 return nil 57 } 58 59 out := new(GuttedObject) 60 in.DeepCopyInto(out) 61 62 return out 63 } 64 65 // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 66 func (in *GuttedObject) DeepCopyObject() runtime.Object { 67 return in.DeepCopy() 68 } 69 70 // IsInClusterNamespace check if policy is in cluster namespace 71 func IsInClusterNamespace(ctx context.Context, c client.Client, ns string) (bool, error) { 72 cluster := &clusterv1.ManagedCluster{} 73 74 err := c.Get(ctx, types.NamespacedName{Name: ns}, cluster) 75 if k8serrors.IsNotFound(err) { 76 return false, nil 77 } 78 79 if err != nil { 80 return false, fmt.Errorf("failed to get the managed cluster %s: %w", ns, err) 81 } 82 83 return true, nil 84 } 85 86 func IsReplicatedPolicy(ctx context.Context, c client.Client, policy client.Object) (bool, error) { 87 rootPlcName := policy.GetLabels()[RootPolicyLabel] 88 if rootPlcName == "" { 89 return false, nil 90 } 91 92 _, _, err := ParseRootPolicyLabel(rootPlcName) 93 if err != nil { 94 return false, fmt.Errorf("invalid value set in %s: %w", RootPolicyLabel, err) 95 } 96 97 return IsInClusterNamespace(ctx, c, policy.GetNamespace()) 98 } 99 100 // IsForPolicyOrPolicySet returns true if any of the subjects of the PlacementBinding are Policies 101 // or PolicySets. 102 func IsForPolicyOrPolicySet(pb *policiesv1.PlacementBinding) bool { 103 if pb == nil { 104 return false 105 } 106 107 for _, subject := range pb.Subjects { 108 if subject.APIGroup == policiesv1.SchemeGroupVersion.Group && 109 (subject.Kind == policiesv1.Kind || subject.Kind == policiesv1.PolicySetKind) { 110 return true 111 } 112 } 113 114 return false 115 } 116 117 // IsPbForPolicySet compares group and kind with policyset group and kind for given pb 118 func IsPbForPolicySet(pb *policiesv1.PlacementBinding) bool { 119 if pb == nil { 120 return false 121 } 122 123 subjects := pb.Subjects 124 for _, subject := range subjects { 125 if subject.Kind == policiesv1.PolicySetKind && subject.APIGroup == policiesv1.SchemeGroupVersion.Group { 126 return true 127 } 128 } 129 130 return false 131 } 132 133 // GetPoliciesInPlacementBinding returns a list of the Policies that are either direct subjects of 134 // the given PlacementBinding, or are in PolicySets that are subjects of the PlacementBinding. 135 // The list items are guaranteed to be unique (for example if a policy is in multiple sets). 136 func GetPoliciesInPlacementBinding( 137 ctx context.Context, c client.Client, pb *policiesv1.PlacementBinding, 138 ) []reconcile.Request { 139 table := map[reconcile.Request]bool{} 140 141 for _, subject := range pb.Subjects { 142 if subject.APIGroup != policiesv1.SchemeGroupVersion.Group { 143 continue 144 } 145 146 switch subject.Kind { 147 case policiesv1.Kind: 148 req := reconcile.Request{NamespacedName: types.NamespacedName{ 149 Name: subject.Name, 150 Namespace: pb.GetNamespace(), 151 }} 152 153 table[req] = true 154 case policiesv1.PolicySetKind: 155 setNN := types.NamespacedName{ 156 Name: subject.Name, 157 Namespace: pb.GetNamespace(), 158 } 159 160 policySet := policiesv1beta1.PolicySet{} 161 if err := c.Get(ctx, setNN, &policySet); err != nil { 162 continue 163 } 164 165 for _, plc := range policySet.Spec.Policies { 166 req := reconcile.Request{NamespacedName: types.NamespacedName{ 167 Name: string(plc), 168 Namespace: pb.GetNamespace(), 169 }} 170 171 table[req] = true 172 } 173 } 174 } 175 176 result := make([]reconcile.Request, 0, len(table)) 177 178 for k := range table { 179 result = append(result, k) 180 } 181 182 return result 183 } 184 185 // FindNonCompliantClustersForPolicy returns cluster in noncompliant status with given policy 186 func FindNonCompliantClustersForPolicy(plc *policiesv1.Policy) []string { 187 clusterList := []string{} 188 189 for _, clusterStatus := range plc.Status.Status { 190 if clusterStatus.ComplianceState == policiesv1.NonCompliant { 191 clusterList = append(clusterList, clusterStatus.ClusterName) 192 } 193 } 194 195 return clusterList 196 } 197 198 func HasValidPlacementRef(pb *policiesv1.PlacementBinding) bool { 199 switch pb.PlacementRef.Kind { 200 case "PlacementRule": 201 return pb.PlacementRef.APIGroup == appsv1.SchemeGroupVersion.Group 202 case "Placement": 203 return pb.PlacementRef.APIGroup == clusterv1beta1.SchemeGroupVersion.Group 204 default: 205 return false 206 } 207 } 208 209 // GetDecisions returns the placement decisions from the Placement or PlacementRule referred to by 210 // the PlacementBinding 211 func GetDecisions( 212 ctx context.Context, c client.Client, pb *policiesv1.PlacementBinding, 213 ) ([]string, error) { 214 // If the PlacementRef is invalid, log and return. (This is not recoverable.) 215 if !HasValidPlacementRef(pb) { 216 log.Info(fmt.Sprintf("PlacementBinding %s/%s placementRef is not valid. Ignoring.", pb.Namespace, pb.Name)) 217 218 return nil, nil 219 } 220 221 clusterDecisions := make([]string, 0) 222 refNN := types.NamespacedName{ 223 Namespace: pb.GetNamespace(), 224 Name: pb.PlacementRef.Name, 225 } 226 227 switch pb.PlacementRef.Kind { 228 case "Placement": 229 pl := &clusterv1beta1.Placement{} 230 231 err := c.Get(ctx, refNN, pl) 232 if err != nil && !k8serrors.IsNotFound(err) { 233 return nil, fmt.Errorf("failed to get Placement '%v': %w", pb.PlacementRef.Name, err) 234 } 235 236 if k8serrors.IsNotFound(err) { 237 return nil, nil 238 } 239 240 list := &clusterv1beta1.PlacementDecisionList{} 241 lopts := &client.ListOptions{Namespace: pb.GetNamespace()} 242 243 opts := client.MatchingLabels{"cluster.open-cluster-management.io/placement": pl.GetName()} 244 opts.ApplyToList(lopts) 245 246 err = c.List(ctx, list, lopts) 247 if err != nil && !k8serrors.IsNotFound(err) { 248 return nil, fmt.Errorf("failed to list the PlacementDecisions for '%v', %w", pb.PlacementRef.Name, err) 249 } 250 251 for _, item := range list.Items { 252 for _, cluster := range item.Status.Decisions { 253 clusterDecisions = append(clusterDecisions, cluster.ClusterName) 254 } 255 } 256 257 return clusterDecisions, nil 258 case "PlacementRule": 259 plr := &appsv1.PlacementRule{} 260 if err := c.Get(ctx, refNN, plr); err != nil && !k8serrors.IsNotFound(err) { 261 return nil, fmt.Errorf("failed to get PlacementRule '%v': %w", pb.PlacementRef.Name, err) 262 } 263 264 for _, cluster := range plr.Status.Decisions { 265 clusterDecisions = append(clusterDecisions, cluster.ClusterName) 266 } 267 268 // if the PlacementRule was not found, the decisions will be empty 269 return clusterDecisions, nil 270 } 271 272 return nil, fmt.Errorf("placement binding %s/%s reference is not valid", pb.Namespace, pb.Name) 273 } 274 275 func ParseRootPolicyLabel(rootPlc string) (name, namespace string, err error) { 276 // namespaces can't have a `.` (but names can) so this always correctly pulls the namespace out 277 namespace, name, found := strings.Cut(rootPlc, ".") 278 if !found { 279 err = fmt.Errorf("required at least one `.` in value of label `%v`: %w", 280 RootPolicyLabel, ErrInvalidLabelValue) 281 282 return "", "", err 283 } 284 285 return name, namespace, nil 286 } 287 288 // LabelsForRootPolicy returns the labels for given policy 289 func LabelsForRootPolicy(plc *policiesv1.Policy) map[string]string { 290 return map[string]string{RootPolicyLabel: FullNameForPolicy(plc)} 291 } 292 293 // fullNameForPolicy returns the fully qualified name for given policy 294 // full qualified name: ${namespace}.${name} 295 func FullNameForPolicy(plc *policiesv1.Policy) string { 296 return plc.GetNamespace() + "." + plc.GetName() 297 } 298 299 // GetRepPoliciesInPlacementBinding returns a list of the replicated policies that are either direct subjects of 300 // the given PlacementBinding, or are in PolicySets that are subjects of the PlacementBinding. 301 // The list items are guaranteed to be unique (for example if a policy is in multiple sets). 302 func GetRepPoliciesInPlacementBinding( 303 ctx context.Context, c client.Client, pb *policiesv1.PlacementBinding, 304 ) []reconcile.Request { 305 decisions, err := GetDecisions(ctx, c, pb) 306 if err != nil { 307 return []reconcile.Request{} 308 } 309 // Use this for removing duplicated policies 310 rootPolicyRequest := GetPoliciesInPlacementBinding(ctx, c, pb) 311 312 result := make([]reconcile.Request, 0, len(rootPolicyRequest)*len(decisions)) 313 314 for _, rp := range rootPolicyRequest { 315 for _, clusterName := range decisions { 316 result = append(result, reconcile.Request{NamespacedName: types.NamespacedName{ 317 Name: rp.Namespace + "." + rp.Name, 318 Namespace: clusterName, 319 }}) 320 } 321 } 322 323 return result 324 } 325 326 // TypeConverter is a helper function to converter type struct a to b 327 func TypeConverter(a, b interface{}) error { 328 js, err := json.Marshal(a) 329 if err != nil { 330 return err 331 } 332 333 return json.Unmarshal(js, b) 334 } 335 336 // Select objects that are deleted or created 337 func GetAffectedObjs[T comparable](oldObjs []T, newObjs []T) []T { 338 table := make(map[T]int) 339 340 for _, oldObj := range oldObjs { 341 table[oldObj] = 1 342 } 343 344 for _, newObj := range newObjs { 345 table[newObj]++ 346 } 347 348 result := []T{} 349 350 for key, val := range table { 351 if val == 1 { 352 result = append(result, key) 353 } 354 } 355 356 return result 357 } 358 359 type PlacementRefKinds string 360 361 const ( 362 Placement PlacementRefKinds = "Placement" 363 PlacementRule PlacementRefKinds = "PlacementRule" 364 ) 365 366 // GetRootPolicyRequests find and filter placementbindings which have namespace and placementRef.name. 367 // Gather all root policies under placementbindings 368 func GetRootPolicyRequests(ctx context.Context, c client.Client, 369 namespace, placementRefName string, refKind PlacementRefKinds, 370 ) ([]reconcile.Request, error) { 371 kindGroupMap := map[PlacementRefKinds]string{ 372 Placement: clusterv1beta1.SchemeGroupVersion.Group, 373 PlacementRule: appsv1.SchemeGroupVersion.Group, 374 } 375 376 pbList := &policiesv1.PlacementBindingList{} 377 // Find pb in the same namespace of placementrule 378 lopts := &client.ListOptions{Namespace: namespace} 379 opts := client.MatchingFields{"placementRef.name": placementRefName} 380 opts.ApplyToList(lopts) 381 382 err := c.List(ctx, pbList, lopts) 383 if err != nil { 384 return nil, err 385 } 386 var rootPolicyResults []reconcile.Request 387 388 for i, pb := range pbList.Items { 389 if pb.PlacementRef.APIGroup != kindGroupMap[refKind] || 390 pb.PlacementRef.Kind != string(refKind) || pb.PlacementRef.Name != placementRefName { 391 continue 392 } 393 // GetPoliciesInPlacementBinding only pick root-policy name 394 rootPolicyResults = append(rootPolicyResults, 395 GetPoliciesInPlacementBinding(ctx, c, &pbList.Items[i])...) 396 } 397 398 return rootPolicyResults, nil 399 }