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  }