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  }