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

     1  package propagator
     2  
     3  import (
     4  	"context"
     5  	"reflect"
     6  	"strings"
     7  	"testing"
     8  
     9  	k8sdepwatches "github.com/stolostron/kubernetes-dependency-watches/client"
    10  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    12  	"sigs.k8s.io/controller-runtime/pkg/client"
    13  	fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    14  
    15  	policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
    16  	policiesv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1"
    17  )
    18  
    19  func fakeBasicPolicy(name, namespace string) *policiesv1.Policy {
    20  	return &policiesv1.Policy{
    21  		TypeMeta: v1.TypeMeta{
    22  			Kind:       policiesv1.Kind,
    23  			APIVersion: policiesv1.GroupVersion.String(),
    24  		},
    25  		ObjectMeta: v1.ObjectMeta{
    26  			Name:      name,
    27  			Namespace: namespace,
    28  		},
    29  		Spec: policiesv1.PolicySpec{
    30  			Dependencies: []policiesv1.PolicyDependency{},
    31  			PolicyTemplates: []*policiesv1.PolicyTemplate{{
    32  				ExtraDependencies: []policiesv1.PolicyDependency{},
    33  			}},
    34  		},
    35  	}
    36  }
    37  
    38  func fakePolicyWithDeps(name, namespace string, deps, extraDeps []policiesv1.PolicyDependency) *policiesv1.Policy {
    39  	pol := fakeBasicPolicy(name, namespace)
    40  	pol.Spec.Dependencies = deps
    41  	pol.Spec.PolicyTemplates[0].ExtraDependencies = extraDeps
    42  
    43  	return pol
    44  }
    45  
    46  func fakePolicySet(name, namespace string, policies ...string) *policiesv1beta1.PolicySet {
    47  	pols := []policiesv1beta1.NonEmptyString{}
    48  	for _, p := range policies {
    49  		pols = append(pols, policiesv1beta1.NonEmptyString(p))
    50  	}
    51  
    52  	return &policiesv1beta1.PolicySet{
    53  		TypeMeta: v1.TypeMeta{
    54  			Kind:       policiesv1.PolicySetKind,
    55  			APIVersion: policiesv1beta1.GroupVersion.String(),
    56  		},
    57  		ObjectMeta: v1.ObjectMeta{
    58  			Name:      name,
    59  			Namespace: namespace,
    60  		},
    61  		Spec: policiesv1beta1.PolicySetSpec{
    62  			Policies: pols,
    63  		},
    64  	}
    65  }
    66  
    67  func fakeDependencyFromObj(obj client.Object, compliance string) policiesv1.PolicyDependency {
    68  	return policiesv1.PolicyDependency{
    69  		TypeMeta: v1.TypeMeta{
    70  			Kind:       obj.GetObjectKind().GroupVersionKind().Kind,
    71  			APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
    72  		},
    73  		Name:       obj.GetName(),
    74  		Namespace:  obj.GetNamespace(),
    75  		Compliance: policiesv1.ComplianceState(compliance),
    76  	}
    77  }
    78  
    79  func TestEquivalentReplicatedPolicies(t *testing.T) {
    80  	basePolicy := fakeBasicPolicy("base-policy", "default")
    81  
    82  	withAnnotation := basePolicy.DeepCopy()
    83  	withAnnotation.SetAnnotations(map[string]string{
    84  		"test-annotation.io/foo": "bar",
    85  	})
    86  
    87  	withLabel := basePolicy.DeepCopy()
    88  	withLabel.SetLabels(map[string]string{
    89  		"test-label.io/fizz": "buzz",
    90  	})
    91  
    92  	thwimpDep := fakeDependencyFromObj(fakeBasicPolicy("thwimp", "thwump"), "NonCompliant")
    93  	withDependency := fakePolicyWithDeps("base-policy", "default", []policiesv1.PolicyDependency{thwimpDep}, nil)
    94  
    95  	withStatus := basePolicy.DeepCopy()
    96  	withStatus.Status = policiesv1.PolicyStatus{
    97  		ComplianceState: policiesv1.Pending,
    98  	}
    99  
   100  	tests := map[string]struct {
   101  		comparePlc *policiesv1.Policy
   102  		expected   bool
   103  	}{
   104  		"reflexive":             {basePolicy, true},
   105  		"different annotations": {withAnnotation, false},
   106  		"different labels":      {withLabel, false},
   107  		"different specs":       {withDependency, false},
   108  		"different status":      {withStatus, true},
   109  	}
   110  
   111  	for name, test := range tests {
   112  		t.Run(name, func(t *testing.T) {
   113  			if equivalentReplicatedPolicies(basePolicy, test.comparePlc) != test.expected {
   114  				if test.expected {
   115  					t.Fatalf("Expected policies to be equivalent: %+v, %+v", basePolicy, test.comparePlc)
   116  				} else {
   117  					t.Fatalf("Expected policies not to be equivalent: %+v, %+v", basePolicy, test.comparePlc)
   118  				}
   119  			}
   120  		})
   121  	}
   122  }
   123  
   124  func TestCanonicalizeDependencies(t *testing.T) {
   125  	depPol := func(name, namespace, compliance string) policiesv1.PolicyDependency {
   126  		return fakeDependencyFromObj(fakeBasicPolicy(name, namespace), compliance)
   127  	}
   128  
   129  	unusualDependency := policiesv1.PolicyDependency{
   130  		TypeMeta: v1.TypeMeta{
   131  			Kind:       "WeirdPolicy",
   132  			APIVersion: "something.com/v4beta7",
   133  		},
   134  		Name:       "foo",
   135  		Namespace:  "bar",
   136  		Compliance: "Auditing",
   137  	}
   138  
   139  	fooSet := fakePolicySet("foo", "default", "one", "two")
   140  	barSet := fakePolicySet("bar", "other-ns", "three", "four", "five")
   141  	fakeScheme := k8sruntime.NewScheme()
   142  
   143  	if err := policiesv1beta1.AddToScheme(fakeScheme); err != nil {
   144  		t.Fatalf("Unexpected error building scheme: %v", err)
   145  	}
   146  
   147  	fakeClient := fakeClient.NewClientBuilder().
   148  		WithScheme(fakeScheme).
   149  		WithObjects(fooSet, barSet).
   150  		Build()
   151  
   152  	fakeReconciler := Propagator{Client: fakeClient}
   153  
   154  	tests := map[string]struct {
   155  		input []policiesv1.PolicyDependency
   156  		want  []policiesv1.PolicyDependency
   157  	}{
   158  		"empty": {
   159  			input: []policiesv1.PolicyDependency{},
   160  			want:  []policiesv1.PolicyDependency{},
   161  		},
   162  		"single compliant set": {
   163  			input: []policiesv1.PolicyDependency{
   164  				fakeDependencyFromObj(fooSet, "Compliant"),
   165  			},
   166  			want: []policiesv1.PolicyDependency{
   167  				depPol("default.one", "", "Compliant"),
   168  				depPol("default.two", "", "Compliant"),
   169  			},
   170  		},
   171  		"two sets, mixed compliance": {
   172  			input: []policiesv1.PolicyDependency{
   173  				fakeDependencyFromObj(fooSet, "Compliant"),
   174  				fakeDependencyFromObj(barSet, "NonCompliant"),
   175  			},
   176  			want: []policiesv1.PolicyDependency{
   177  				depPol("default.one", "", "Compliant"),
   178  				depPol("default.two", "", "Compliant"),
   179  				depPol("other-ns.three", "", "NonCompliant"),
   180  				depPol("other-ns.four", "", "NonCompliant"),
   181  				depPol("other-ns.five", "", "NonCompliant"),
   182  			},
   183  		},
   184  		"policies with namespaces": {
   185  			input: []policiesv1.PolicyDependency{
   186  				depPol("red", "colors", "Compliant"),
   187  				depPol("blue", "colors", "NonCompliant"),
   188  			},
   189  			want: []policiesv1.PolicyDependency{
   190  				depPol("colors.red", "", "Compliant"),
   191  				depPol("colors.blue", "", "NonCompliant"),
   192  			},
   193  		},
   194  		"policies without namespaces": {
   195  			input: []policiesv1.PolicyDependency{
   196  				depPol("magikarp", "", "Compliant"),
   197  				depPol("gyarados", "", "Compliant"),
   198  			},
   199  			want: []policiesv1.PolicyDependency{
   200  				depPol("sentinel.magikarp", "", "Compliant"),
   201  				depPol("sentinel.gyarados", "", "Compliant"),
   202  			},
   203  		},
   204  		"policies already canonicalized": {
   205  			input: []policiesv1.PolicyDependency{
   206  				depPol("legendary.lugia", "", "Terminating"),
   207  				depPol("legendary.ho-oh", "", "Terminating"),
   208  			},
   209  			want: []policiesv1.PolicyDependency{
   210  				depPol("legendary.lugia", "", "Terminating"),
   211  				depPol("legendary.ho-oh", "", "Terminating"),
   212  			},
   213  		},
   214  		"unusual dependency should be unchanged": {
   215  			input: []policiesv1.PolicyDependency{unusualDependency},
   216  			want:  []policiesv1.PolicyDependency{unusualDependency},
   217  		},
   218  	}
   219  
   220  	for name, test := range tests {
   221  		t.Run(name, func(t *testing.T) {
   222  			got, err := fakeReconciler.canonicalizeDependencies(context.TODO(), test.input, "sentinel")
   223  			if err != nil {
   224  				t.Fatal("Got unexpected error")
   225  			}
   226  
   227  			if !reflect.DeepEqual(test.want, got) {
   228  				t.Fatalf("expected: %v, got: %v", test.want, got)
   229  			}
   230  		})
   231  	}
   232  }
   233  
   234  func TestGetPolicySetDependencies(t *testing.T) {
   235  	makeDepPlcSet := func(name, namespace, compliance string) policiesv1.PolicyDependency {
   236  		return fakeDependencyFromObj(fakePolicySet(name, namespace), compliance)
   237  	}
   238  
   239  	makeDepPolicy := func(name, namespace, compliance string) policiesv1.PolicyDependency {
   240  		return fakeDependencyFromObj(fakeBasicPolicy(name, namespace), compliance)
   241  	}
   242  
   243  	idsFromDeps := func(deps ...policiesv1.PolicyDependency) map[k8sdepwatches.ObjectIdentifier]bool {
   244  		ids := make(map[k8sdepwatches.ObjectIdentifier]bool)
   245  
   246  		for _, d := range deps {
   247  			ids[k8sdepwatches.ObjectIdentifier{
   248  				Group:     strings.Split(d.APIVersion, "/")[0],
   249  				Version:   strings.Split(d.APIVersion, "/")[1],
   250  				Kind:      d.Kind,
   251  				Namespace: d.Namespace,
   252  				Name:      d.Name,
   253  			}] = true
   254  		}
   255  
   256  		return ids
   257  	}
   258  
   259  	fooSet := makeDepPlcSet("foo", "default", "Compliant")
   260  	barSet := makeDepPlcSet("bar", "default", "NonCompliant")
   261  	myPolicy := makeDepPolicy("mypolicy", "mynamespace", "Compliant")
   262  	fooSetEmptyNamespace := makeDepPlcSet("foo", "", "Compliant")
   263  	fooSetInferredNamespace := makeDepPlcSet("foo", "testns", "Compliant")
   264  
   265  	tests := map[string]struct {
   266  		inputDeps      []policiesv1.PolicyDependency
   267  		inputExtraDeps []policiesv1.PolicyDependency
   268  		want           map[k8sdepwatches.ObjectIdentifier]bool
   269  	}{
   270  		"no dependencies": {
   271  			want: idsFromDeps(),
   272  		},
   273  		"one top set dependency": {
   274  			inputDeps: []policiesv1.PolicyDependency{fooSet},
   275  			want:      idsFromDeps(fooSet),
   276  		},
   277  		"one extra set dependency": {
   278  			inputExtraDeps: []policiesv1.PolicyDependency{fooSet},
   279  			want:           idsFromDeps(fooSet),
   280  		},
   281  		"set dependencies at both levels": {
   282  			inputDeps:      []policiesv1.PolicyDependency{fooSet},
   283  			inputExtraDeps: []policiesv1.PolicyDependency{barSet},
   284  			want:           idsFromDeps(fooSet, barSet),
   285  		},
   286  		"one policy should be ignored": {
   287  			inputDeps: []policiesv1.PolicyDependency{myPolicy},
   288  			want:      idsFromDeps(),
   289  		},
   290  		"policies should be ignored when mixed with sets": {
   291  			inputDeps: []policiesv1.PolicyDependency{fooSet, myPolicy},
   292  			want:      idsFromDeps(fooSet),
   293  		},
   294  		"namespace should be inferred from policy when not provided": {
   295  			inputDeps: []policiesv1.PolicyDependency{fooSetEmptyNamespace},
   296  			want:      idsFromDeps(fooSetInferredNamespace),
   297  		},
   298  		"extraDependency namespace should be inferred from policy when not provided": {
   299  			inputExtraDeps: []policiesv1.PolicyDependency{fooSetEmptyNamespace},
   300  			want:           idsFromDeps(fooSetInferredNamespace),
   301  		},
   302  	}
   303  
   304  	for name, test := range tests {
   305  		t.Run(name, func(t *testing.T) {
   306  			testpol := fakePolicyWithDeps("testpolicy", "testns", test.inputDeps, test.inputExtraDeps)
   307  
   308  			got := getPolicySetDependencies(testpol)
   309  			if !reflect.DeepEqual(got, test.want) {
   310  				t.Fatalf("expected: %v, got: %v", test.want, got)
   311  			}
   312  		})
   313  	}
   314  }