open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/propagator/replication.go (about)

     1  package propagator
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  
     7  	k8sdepwatches "github.com/stolostron/kubernetes-dependency-watches/client"
     8  	"k8s.io/apimachinery/pkg/api/equality"
     9  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    10  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/types"
    12  
    13  	policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
    14  	policiesv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1"
    15  	"open-cluster-management.io/governance-policy-propagator/controllers/common"
    16  )
    17  
    18  const argoCDCompareOptionsAnnotation = "argocd.argoproj.io/compare-options"
    19  
    20  // equivalentReplicatedPolicies compares replicated policies. Returns true if they match.
    21  func equivalentReplicatedPolicies(plc1 *policiesv1.Policy, plc2 *policiesv1.Policy) bool {
    22  	// Compare annotations
    23  	if !equality.Semantic.DeepEqual(plc1.GetAnnotations(), plc2.GetAnnotations()) {
    24  		return false
    25  	}
    26  
    27  	// Compare labels
    28  	if !equality.Semantic.DeepEqual(plc1.GetLabels(), plc2.GetLabels()) {
    29  		return false
    30  	}
    31  
    32  	// Compare the specs
    33  	return equality.Semantic.DeepEqual(plc1.Spec, plc2.Spec)
    34  }
    35  
    36  // buildReplicatedPolicy constructs a replicated policy based on a root policy and a placementDecision.
    37  // In particular, it adds labels that the policy framework uses, and ensures that policy dependencies
    38  // are in a consistent format suited for use on managed clusters.
    39  // It can return an error if it needed to canonicalize a dependency, but a PolicySet lookup failed.
    40  func (r *ReplicatedPolicyReconciler) buildReplicatedPolicy(ctx context.Context,
    41  	root *policiesv1.Policy, clusterDec clusterDecision,
    42  ) (*policiesv1.Policy, error) {
    43  	clusterName := clusterDec.Cluster
    44  	replicatedName := common.FullNameForPolicy(root)
    45  
    46  	// Create the copy this way to avoid copying the root policy's status which can be quite large in large
    47  	// environments.
    48  	replicated := &policiesv1.Policy{}
    49  	replicated.TypeMeta = root.TypeMeta
    50  	root.ObjectMeta.DeepCopyInto(&replicated.ObjectMeta)
    51  	root.Spec.DeepCopyInto(&replicated.Spec)
    52  	replicated.SetName(replicatedName)
    53  	replicated.SetNamespace(clusterName)
    54  	replicated.SetResourceVersion("")
    55  	replicated.SetFinalizers(nil)
    56  	replicated.SetOwnerReferences(nil)
    57  
    58  	labels := root.GetLabels()
    59  	if labels == nil {
    60  		labels = map[string]string{}
    61  	}
    62  
    63  	if root.Spec.CopyPolicyMetadata != nil && !*root.Spec.CopyPolicyMetadata {
    64  		originalLabels := replicated.GetLabels()
    65  
    66  		for label := range originalLabels {
    67  			if !strings.HasPrefix(label, policiesv1.GroupVersion.Group+"/") {
    68  				delete(labels, label)
    69  			}
    70  		}
    71  	}
    72  
    73  	// Extra labels on replicated policies
    74  	labels[common.ClusterNameLabel] = clusterName
    75  	labels[common.ClusterNamespaceLabel] = clusterName
    76  	labels[common.RootPolicyLabel] = replicatedName
    77  
    78  	replicated.SetLabels(labels)
    79  
    80  	annotations := replicated.GetAnnotations()
    81  	if annotations == nil {
    82  		annotations = map[string]string{}
    83  	}
    84  
    85  	if root.Spec.CopyPolicyMetadata != nil && !*root.Spec.CopyPolicyMetadata {
    86  		originalAnnotations := replicated.GetAnnotations()
    87  
    88  		for annotation := range originalAnnotations {
    89  			if !strings.HasPrefix(annotation, policiesv1.GroupVersion.Group+"/") {
    90  				delete(annotations, annotation)
    91  			}
    92  		}
    93  	}
    94  
    95  	// Always set IgnoreExtraneous to avoid ArgoCD managing the replicated policy.
    96  	annotations[argoCDCompareOptionsAnnotation] = "IgnoreExtraneous"
    97  
    98  	replicated.SetAnnotations(annotations)
    99  
   100  	// Override the replicated policy remediationAction when it's selected to be enforced
   101  	if !strings.EqualFold(string(replicated.Spec.RemediationAction), string(policiesv1.Enforce)) {
   102  		if clusterDec.PolicyOverrides.RemediationAction != "" {
   103  			replicated.Spec.RemediationAction = policiesv1.RemediationAction(
   104  				clusterDec.PolicyOverrides.RemediationAction)
   105  		}
   106  	}
   107  
   108  	var err error
   109  
   110  	replicated.Spec.Dependencies, err = r.canonicalizeDependencies(ctx, replicated.Spec.Dependencies, root.Namespace)
   111  	if err != nil {
   112  		return replicated, err
   113  	}
   114  
   115  	for i, template := range replicated.Spec.PolicyTemplates {
   116  		replicated.Spec.PolicyTemplates[i].ExtraDependencies, err = r.canonicalizeDependencies(
   117  			ctx, template.ExtraDependencies, root.Namespace)
   118  		if err != nil {
   119  			return replicated, err
   120  		}
   121  	}
   122  
   123  	return replicated, nil
   124  }
   125  
   126  // depIsPolicySet returns true if the given PolicyDependency is a PolicySet
   127  func depIsPolicySet(dep policiesv1.PolicyDependency) bool {
   128  	return dep.Kind == policiesv1.PolicySetKind &&
   129  		dep.APIVersion == policiesv1beta1.GroupVersion.String()
   130  }
   131  
   132  // depIsPolicy returns true if the given PolicyDependency is a Policy
   133  func depIsPolicy(dep policiesv1.PolicyDependency) bool {
   134  	return dep.Kind == policiesv1.Kind &&
   135  		dep.APIVersion == policiesv1.GroupVersion.String()
   136  }
   137  
   138  // canonicalizeDependencies returns an adjusted list of the input dependencies, ensuring that
   139  // Policies are in a consistent format (omitting the namespace, and using the <namespace>.<name>
   140  // format as in replicated Policies), and that PolicySets are replaced with their constituent
   141  // Policies. If a PolicySet could not be found, that dependency will be copied as-is. It will
   142  // return an error if there is an unexpected error looking up a PolicySet to replace.
   143  func (r *Propagator) canonicalizeDependencies(
   144  	ctx context.Context, rawDeps []policiesv1.PolicyDependency, defaultNamespace string,
   145  ) ([]policiesv1.PolicyDependency, error) {
   146  	deps := make([]policiesv1.PolicyDependency, 0)
   147  
   148  	for _, dep := range rawDeps {
   149  		if depIsPolicySet(dep) {
   150  			plcset := &policiesv1beta1.PolicySet{}
   151  
   152  			if dep.Namespace == "" {
   153  				dep.Namespace = defaultNamespace
   154  			}
   155  
   156  			err := r.Get(ctx, types.NamespacedName{
   157  				Namespace: dep.Namespace,
   158  				Name:      dep.Name,
   159  			}, plcset)
   160  			if err != nil {
   161  				if k8serrors.IsNotFound(err) {
   162  					// If the PolicySet does not exist, that's ok - the propagator doesn't need to
   163  					// do anything special because there will be a useful status message created by
   164  					// the framework on the managed cluster.
   165  					deps = append(deps, dep)
   166  
   167  					continue
   168  				}
   169  
   170  				return deps, err
   171  			}
   172  
   173  			for _, plc := range plcset.Spec.Policies {
   174  				deps = append(deps, policiesv1.PolicyDependency{
   175  					TypeMeta: v1.TypeMeta{
   176  						Kind:       policiesv1.Kind,
   177  						APIVersion: policiesv1.GroupVersion.String(),
   178  					},
   179  					Name:       dep.Namespace + "." + string(plc),
   180  					Namespace:  "",
   181  					Compliance: dep.Compliance,
   182  				})
   183  			}
   184  		} else if depIsPolicy(dep) {
   185  			split := strings.Split(dep.Name, ".")
   186  			if len(split) == 2 { // assume it's already in the correct <namespace>.<name> format
   187  				deps = append(deps, dep)
   188  			} else {
   189  				if dep.Namespace == "" {
   190  					// use the namespace from the dependent policy when otherwise not provided
   191  					dep.Namespace = defaultNamespace
   192  				}
   193  
   194  				dep.Name = dep.Namespace + "." + dep.Name
   195  				dep.Namespace = ""
   196  
   197  				deps = append(deps, dep)
   198  			}
   199  		} else {
   200  			deps = append(deps, dep)
   201  		}
   202  	}
   203  
   204  	return deps, nil
   205  }
   206  
   207  // getPolicySetDependencies find all dependencies and extraDependencies in the given policy that
   208  // are PolicySets, since those objects will need to be watched.
   209  func getPolicySetDependencies(root *policiesv1.Policy) map[k8sdepwatches.ObjectIdentifier]bool {
   210  	policySetIDs := make(map[k8sdepwatches.ObjectIdentifier]bool)
   211  
   212  	for _, dep := range root.Spec.Dependencies {
   213  		if depIsPolicySet(dep) {
   214  			ns := dep.Namespace
   215  			if ns == "" {
   216  				ns = root.Namespace
   217  			}
   218  
   219  			policySetIDs[k8sdepwatches.ObjectIdentifier{
   220  				Group:     common.APIGroup,
   221  				Version:   "v1beta1",
   222  				Kind:      policiesv1.PolicySetKind,
   223  				Namespace: ns,
   224  				Name:      dep.Name,
   225  			}] = true
   226  		}
   227  	}
   228  
   229  	for _, tmpl := range root.Spec.PolicyTemplates {
   230  		for _, dep := range tmpl.ExtraDependencies {
   231  			if depIsPolicySet(dep) {
   232  				ns := dep.Namespace
   233  				if ns == "" {
   234  					ns = root.Namespace
   235  				}
   236  
   237  				policySetIDs[k8sdepwatches.ObjectIdentifier{
   238  					Group:     common.APIGroup,
   239  					Version:   "v1beta1",
   240  					Kind:      policiesv1.PolicySetKind,
   241  					Namespace: ns,
   242  					Name:      dep.Name,
   243  				}] = true
   244  			}
   245  		}
   246  	}
   247  
   248  	return policySetIDs
   249  }