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  }