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

     1  // Copyright (c) 2022 Red Hat, Inc.
     2  // Copyright Contributors to the Open Cluster Management project
     3  
     4  package encryptionkeys
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/rand"
    10  	"errors"
    11  	"fmt"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	. "github.com/onsi/ginkgo/v2"
    17  	. "github.com/onsi/gomega"
    18  	corev1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    21  	"k8s.io/apimachinery/pkg/types"
    22  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    23  	ctrl "sigs.k8s.io/controller-runtime"
    24  	"sigs.k8s.io/controller-runtime/pkg/client"
    25  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    26  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    27  
    28  	v1 "open-cluster-management.io/governance-policy-propagator/api/v1"
    29  	"open-cluster-management.io/governance-policy-propagator/controllers/common"
    30  	"open-cluster-management.io/governance-policy-propagator/controllers/propagator"
    31  )
    32  
    33  const (
    34  	keySize     = 256
    35  	clusterName = "local-cluster"
    36  	day         = time.Hour * 24
    37  )
    38  
    39  type erroringFakeClient struct {
    40  	client.Client
    41  	GetError    bool
    42  	ListError   bool
    43  	PatchError  bool
    44  	UpdateError bool
    45  }
    46  
    47  func (c *erroringFakeClient) Get(
    48  	ctx context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption,
    49  ) error {
    50  	if c.GetError {
    51  		return errors.New("some get error")
    52  	}
    53  
    54  	return c.Client.Get(ctx, key, obj)
    55  }
    56  
    57  func (c *erroringFakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
    58  	if c.ListError {
    59  		return errors.New("some list error")
    60  	}
    61  
    62  	return c.Client.List(ctx, list, opts...)
    63  }
    64  
    65  func (c *erroringFakeClient) Patch(
    66  	ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption,
    67  ) error {
    68  	if c.PatchError {
    69  		return errors.New("some patch error")
    70  	}
    71  
    72  	return c.Client.Patch(ctx, obj, patch, opts...)
    73  }
    74  
    75  func (c *erroringFakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
    76  	if c.UpdateError {
    77  		return errors.New("some update error")
    78  	}
    79  
    80  	return c.Client.Update(ctx, obj, opts...)
    81  }
    82  
    83  func generateSecret() *corev1.Secret {
    84  	key := make([]byte, keySize/8)
    85  	_, err := rand.Read(key)
    86  	Expect(err).ToNot(HaveOccurred())
    87  
    88  	encryptionSecret := &corev1.Secret{
    89  		ObjectMeta: metav1.ObjectMeta{
    90  			Name:      propagator.EncryptionKeySecret,
    91  			Namespace: clusterName,
    92  		},
    93  		Data: map[string][]byte{"key": key},
    94  	}
    95  
    96  	prevKey := make([]byte, keySize/8)
    97  	_, err = rand.Read(prevKey)
    98  	Expect(err).ToNot(HaveOccurred())
    99  
   100  	encryptionSecret.Data["previousKey"] = prevKey
   101  
   102  	return encryptionSecret
   103  }
   104  
   105  func generatePolicies() []client.Object {
   106  	return []client.Object{
   107  		client.Object(
   108  			&v1.Policy{
   109  				ObjectMeta: metav1.ObjectMeta{
   110  					Name:        "default.policy-one",
   111  					Namespace:   clusterName,
   112  					Annotations: map[string]string{propagator.IVAnnotation: "7cznVUq5SXEE4RMZNkGOrQ=="},
   113  					Labels:      map[string]string{common.RootPolicyLabel: "default.policy-one"},
   114  				},
   115  			},
   116  		),
   117  		client.Object(
   118  			&v1.Policy{
   119  				ObjectMeta: metav1.ObjectMeta{
   120  					Name:        "policy-one",
   121  					Namespace:   "default",
   122  					Annotations: map[string]string{},
   123  				},
   124  			},
   125  		),
   126  		client.Object(
   127  			&v1.Policy{
   128  				ObjectMeta: metav1.ObjectMeta{
   129  					Name:        "default.policy-two",
   130  					Namespace:   clusterName,
   131  					Annotations: map[string]string{},
   132  					Labels:      map[string]string{common.RootPolicyLabel: "default.policy-two"},
   133  				},
   134  			},
   135  		),
   136  		client.Object(
   137  			&v1.Policy{
   138  				ObjectMeta: metav1.ObjectMeta{
   139  					Name:        "policy-two",
   140  					Namespace:   "default",
   141  					Annotations: map[string]string{},
   142  				},
   143  			},
   144  		),
   145  		client.Object(
   146  			&v1.Policy{
   147  				ObjectMeta: metav1.ObjectMeta{
   148  					Name:        "default.policy-three",
   149  					Namespace:   "some-other-cluster",
   150  					Annotations: map[string]string{propagator.IVAnnotation: "7cznVUq5SXEE4RMZNkGOrQ=="},
   151  					Labels:      map[string]string{common.RootPolicyLabel: "default.policy-three"},
   152  				},
   153  			},
   154  		),
   155  		client.Object(
   156  			&v1.Policy{
   157  				ObjectMeta: metav1.ObjectMeta{
   158  					Name:        "policy-three",
   159  					Namespace:   "default",
   160  					Annotations: map[string]string{},
   161  				},
   162  			},
   163  		),
   164  		client.Object(
   165  			&v1.Policy{
   166  				ObjectMeta: metav1.ObjectMeta{
   167  					Name:        "default.policy-four",
   168  					Namespace:   clusterName,
   169  					Annotations: map[string]string{propagator.IVAnnotation: "7cznVUq5SXEE4RMZNkGOrQ=="},
   170  					Labels:      map[string]string{common.RootPolicyLabel: "default.policy-four"},
   171  				},
   172  			},
   173  		),
   174  		client.Object(
   175  			&v1.Policy{
   176  				ObjectMeta: metav1.ObjectMeta{
   177  					Name:        "policy-four",
   178  					Namespace:   "default",
   179  					Annotations: map[string]string{},
   180  				},
   181  			},
   182  		),
   183  	}
   184  }
   185  
   186  func getRequeueAfterDays(result reconcile.Result) int {
   187  	return int(result.RequeueAfter.Round(day) / (day))
   188  }
   189  
   190  func getReconciler(encryptionSecret *corev1.Secret) *EncryptionKeysReconciler {
   191  	policies := generatePolicies()
   192  
   193  	scheme := k8sruntime.NewScheme()
   194  	err := clientgoscheme.AddToScheme(scheme)
   195  	Expect(err).ToNot(HaveOccurred())
   196  	err = v1.AddToScheme(scheme)
   197  	Expect(err).ToNot(HaveOccurred())
   198  
   199  	builder := fake.NewClientBuilder().WithObjects(policies...).WithScheme(scheme)
   200  
   201  	if encryptionSecret != nil {
   202  		builder = builder.WithObjects(encryptionSecret)
   203  	}
   204  
   205  	client := builder.Build()
   206  
   207  	return &EncryptionKeysReconciler{
   208  		Client:          client,
   209  		KeyRotationDays: 30,
   210  		Scheme:          scheme,
   211  	}
   212  }
   213  
   214  func assertTriggerUpdate(r *EncryptionKeysReconciler) {
   215  	policyList := v1.PolicyList{}
   216  	err := r.List(context.TODO(), &policyList)
   217  	Expect(err).ToNot(HaveOccurred())
   218  
   219  	for _, policy := range policyList.Items {
   220  		annotation := policy.Annotations[propagator.TriggerUpdateAnnotation]
   221  
   222  		if policy.ObjectMeta.Name == "policy-one" || policy.ObjectMeta.Name == "policy-four" {
   223  			expectedPrefix := fmt.Sprintf("rotate-key-%s-", clusterName)
   224  			Expect(strings.HasPrefix(annotation, expectedPrefix)).To(BeTrue())
   225  		} else {
   226  			Expect(annotation).To(Equal(""))
   227  		}
   228  	}
   229  }
   230  
   231  func assertNoTriggerUpdate(r *EncryptionKeysReconciler) {
   232  	policyList := v1.PolicyList{}
   233  	err := r.List(context.TODO(), &policyList)
   234  	Expect(err).ToNot(HaveOccurred())
   235  
   236  	for _, policy := range policyList.Items {
   237  		annotation := policy.Annotations[propagator.TriggerUpdateAnnotation]
   238  		Expect(annotation).To(Equal(""))
   239  	}
   240  }
   241  
   242  func TestReconcileRotateKey(t *testing.T) {
   243  	t.Parallel()
   244  	RegisterFailHandler(Fail)
   245  
   246  	tests := []struct{ Annotation string }{{""}, {"2020-04-15T01:02:03Z"}, {"not-a-timestamp"}}
   247  
   248  	for _, test := range tests {
   249  		test := test
   250  
   251  		t.Run(
   252  			fmt.Sprintf(`annotation="%s"`, test.Annotation),
   253  			func(t *testing.T) {
   254  				t.Parallel()
   255  
   256  				encryptionSecret := generateSecret()
   257  				if test.Annotation != "" {
   258  					annotations := map[string]string{propagator.LastRotatedAnnotation: test.Annotation}
   259  					encryptionSecret.SetAnnotations(annotations)
   260  				}
   261  				originalKey := encryptionSecret.Data["key"]
   262  
   263  				r := getReconciler(encryptionSecret)
   264  
   265  				secretID := types.NamespacedName{
   266  					Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   267  				}
   268  				request := ctrl.Request{NamespacedName: secretID}
   269  				result, err := r.Reconcile(context.TODO(), request)
   270  
   271  				Expect(err).ToNot(HaveOccurred())
   272  				Expect(result.Requeue).To(BeFalse())
   273  				Expect(getRequeueAfterDays(result)).To(Equal(30))
   274  
   275  				err = r.Get(context.TODO(), secretID, encryptionSecret)
   276  				Expect(err).ToNot(HaveOccurred())
   277  				Expect(bytes.Equal(encryptionSecret.Data["key"], originalKey)).To(BeFalse())
   278  				Expect(bytes.Equal(encryptionSecret.Data["previousKey"], originalKey)).To(BeTrue())
   279  
   280  				assertTriggerUpdate(r)
   281  			},
   282  		)
   283  	}
   284  }
   285  
   286  func TestReconcileNoRotation(t *testing.T) {
   287  	t.Parallel()
   288  	RegisterFailHandler(Fail)
   289  
   290  	encryptionSecret := generateSecret()
   291  	now := time.Now().UTC().Format(time.RFC3339)
   292  
   293  	encryptionSecret.SetAnnotations(map[string]string{propagator.LastRotatedAnnotation: now})
   294  
   295  	originalKey := encryptionSecret.Data["key"]
   296  
   297  	r := getReconciler(encryptionSecret)
   298  
   299  	secretID := types.NamespacedName{
   300  		Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   301  	}
   302  	request := ctrl.Request{NamespacedName: secretID}
   303  	result, err := r.Reconcile(context.TODO(), request)
   304  
   305  	Expect(err).ToNot(HaveOccurred())
   306  	Expect(getRequeueAfterDays(result)).To(Equal(30))
   307  
   308  	err = r.Get(context.TODO(), secretID, encryptionSecret)
   309  	Expect(err).ToNot(HaveOccurred())
   310  	Expect(bytes.Equal(encryptionSecret.Data["key"], originalKey)).To(BeTrue())
   311  	Expect(bytes.Equal(encryptionSecret.Data["previousKey"], originalKey)).To(BeFalse())
   312  
   313  	assertNoTriggerUpdate(r)
   314  }
   315  
   316  func TestReconcileNotFound(t *testing.T) {
   317  	t.Parallel()
   318  	RegisterFailHandler(Fail)
   319  
   320  	r := getReconciler(nil)
   321  
   322  	secretID := types.NamespacedName{
   323  		Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   324  	}
   325  	request := ctrl.Request{NamespacedName: secretID}
   326  	result, err := r.Reconcile(context.TODO(), request)
   327  
   328  	Expect(err).ToNot(HaveOccurred())
   329  	Expect(result.RequeueAfter).To(Equal(time.Duration(0)))
   330  
   331  	policyList := v1.PolicyList{}
   332  	err = r.List(context.TODO(), &policyList)
   333  	Expect(err).ToNot(HaveOccurred())
   334  
   335  	for _, policy := range policyList.Items {
   336  		annotation := policy.Annotations[propagator.TriggerUpdateAnnotation]
   337  		Expect(annotation).To(Equal(""))
   338  	}
   339  }
   340  
   341  func TestReconcileManualRotation(t *testing.T) {
   342  	t.Parallel()
   343  	RegisterFailHandler(Fail)
   344  
   345  	encryptionSecret := generateSecret()
   346  	annotations := map[string]string{
   347  		"policy.open-cluster-management.io/disable-rotation": "true",
   348  		propagator.LastRotatedAnnotation:                     "2020-04-15T01:02:03Z",
   349  	}
   350  	encryptionSecret.SetAnnotations(annotations)
   351  	originalKey := encryptionSecret.Data["key"]
   352  	originalPrevKey := encryptionSecret.Data["previousKey"]
   353  
   354  	r := getReconciler(encryptionSecret)
   355  
   356  	secretID := types.NamespacedName{
   357  		Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   358  	}
   359  	request := ctrl.Request{NamespacedName: secretID}
   360  	result, err := r.Reconcile(context.TODO(), request)
   361  
   362  	Expect(err).ToNot(HaveOccurred())
   363  	Expect(result.Requeue).To(BeFalse())
   364  	Expect(result.RequeueAfter).To(Equal(time.Duration(0)))
   365  
   366  	err = r.Get(context.TODO(), secretID, encryptionSecret)
   367  	Expect(err).ToNot(HaveOccurred())
   368  	Expect(bytes.Equal(encryptionSecret.Data["key"], originalKey)).To(BeTrue())
   369  	Expect(bytes.Equal(encryptionSecret.Data["previousKey"], originalPrevKey)).To(BeTrue())
   370  
   371  	assertTriggerUpdate(r)
   372  }
   373  
   374  func TestReconcileInvalidKey(t *testing.T) {
   375  	t.Parallel()
   376  	RegisterFailHandler(Fail)
   377  
   378  	encryptionSecret := generateSecret()
   379  	encryptionSecret.Data["key"] = []byte("not-a-key")
   380  	originalKey := encryptionSecret.Data["key"]
   381  
   382  	now := time.Now().UTC().Format(time.RFC3339)
   383  
   384  	encryptionSecret.SetAnnotations(map[string]string{propagator.LastRotatedAnnotation: now})
   385  
   386  	r := getReconciler(encryptionSecret)
   387  
   388  	secretID := types.NamespacedName{
   389  		Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   390  	}
   391  	request := ctrl.Request{NamespacedName: secretID}
   392  	result, err := r.Reconcile(context.TODO(), request)
   393  
   394  	Expect(err).ToNot(HaveOccurred())
   395  	Expect(result.Requeue).To(BeFalse())
   396  	Expect(getRequeueAfterDays(result)).To(Equal(30))
   397  
   398  	err = r.Get(context.TODO(), secretID, encryptionSecret)
   399  	Expect(err).ToNot(HaveOccurred())
   400  	Expect(bytes.Equal(encryptionSecret.Data["key"], originalKey)).To(BeFalse())
   401  	Expect(encryptionSecret.Data["previousKey"]).To(BeEmpty())
   402  
   403  	assertTriggerUpdate(r)
   404  }
   405  
   406  func TestReconcileInvalidPreviousKey(t *testing.T) {
   407  	t.Parallel()
   408  	RegisterFailHandler(Fail)
   409  
   410  	encryptionSecret := generateSecret()
   411  	encryptionSecret.Data["previousKey"] = []byte("not-a-key")
   412  	originalKey := encryptionSecret.Data["key"]
   413  
   414  	now := time.Now().UTC().Format(time.RFC3339)
   415  
   416  	encryptionSecret.SetAnnotations(map[string]string{propagator.LastRotatedAnnotation: now})
   417  
   418  	r := getReconciler(encryptionSecret)
   419  
   420  	secretID := types.NamespacedName{
   421  		Namespace: clusterName, Name: propagator.EncryptionKeySecret,
   422  	}
   423  	request := ctrl.Request{NamespacedName: secretID}
   424  	result, err := r.Reconcile(context.TODO(), request)
   425  
   426  	Expect(err).ToNot(HaveOccurred())
   427  	Expect(result.Requeue).To(BeFalse())
   428  	Expect(getRequeueAfterDays(result)).To(Equal(30))
   429  
   430  	err = r.Get(context.TODO(), secretID, encryptionSecret)
   431  	Expect(err).ToNot(HaveOccurred())
   432  	Expect(bytes.Equal(encryptionSecret.Data["key"], originalKey)).To(BeTrue())
   433  	Expect(encryptionSecret.Data["previousKey"]).To(BeEmpty())
   434  
   435  	assertNoTriggerUpdate(r)
   436  }
   437  
   438  func TestReconcileSecretNotFiltered(t *testing.T) {
   439  	t.Parallel()
   440  	RegisterFailHandler(Fail)
   441  
   442  	randomSecret := &corev1.Secret{
   443  		ObjectMeta: metav1.ObjectMeta{
   444  			Name:      "random-secret",
   445  			Namespace: clusterName,
   446  		},
   447  	}
   448  
   449  	r := getReconciler(randomSecret)
   450  
   451  	secretID := types.NamespacedName{Namespace: clusterName, Name: "random-secret"}
   452  	request := ctrl.Request{NamespacedName: secretID}
   453  	result, err := r.Reconcile(context.TODO(), request)
   454  
   455  	Expect(err).ToNot(HaveOccurred())
   456  	Expect(result.Requeue).To(BeFalse())
   457  	Expect(result.RequeueAfter).To(Equal(time.Duration(0)))
   458  
   459  	assertNoTriggerUpdate(r)
   460  }
   461  
   462  func TestReconcileAPIFails(t *testing.T) {
   463  	t.Parallel()
   464  	RegisterFailHandler(Fail)
   465  
   466  	tests := []struct {
   467  		ExpectedRotation bool
   468  		GetError         bool
   469  		ListError        bool
   470  		Name             string
   471  		PatchError       bool
   472  		UpdateError      bool
   473  	}{
   474  		{ExpectedRotation: false, GetError: true, Name: "get-secret"},
   475  		{ExpectedRotation: false, UpdateError: true, Name: "update-secret"},
   476  		{ExpectedRotation: true, ListError: true, Name: "list-policy"},
   477  		{ExpectedRotation: true, PatchError: true, Name: "patch-policy"},
   478  	}
   479  
   480  	for _, test := range tests {
   481  		test := test
   482  		t.Run(
   483  			test.Name,
   484  			func(t *testing.T) {
   485  				t.Parallel()
   486  				encryptionSecret := generateSecret()
   487  				originalKey := encryptionSecret.Data["key"]
   488  				r := getReconciler(encryptionSecret)
   489  				erroringClient := erroringFakeClient{
   490  					Client:      r.Client,
   491  					GetError:    test.GetError,
   492  					ListError:   test.ListError,
   493  					PatchError:  test.PatchError,
   494  					UpdateError: test.UpdateError,
   495  				}
   496  				r.Client = &erroringClient
   497  
   498  				secretID := types.NamespacedName{Namespace: clusterName, Name: propagator.EncryptionKeySecret}
   499  				request := ctrl.Request{NamespacedName: secretID}
   500  				result, err := r.Reconcile(context.TODO(), request)
   501  
   502  				if !test.ExpectedRotation {
   503  					Expect(err).Should(HaveOccurred())
   504  					Expect(result.RequeueAfter).Should(Equal(time.Duration(0)))
   505  				} else {
   506  					Expect(err).ShouldNot(HaveOccurred())
   507  					Expect(result.RequeueAfter).ShouldNot(Equal(time.Duration(0)))
   508  				}
   509  
   510  				Expect(result.Requeue).To(BeFalse())
   511  
   512  				// Revert back the fake client to verify the secret and that no policy updates were triggered
   513  				r.Client = erroringClient.Client
   514  
   515  				err = r.Get(context.TODO(), client.ObjectKeyFromObject(encryptionSecret), encryptionSecret)
   516  				Expect(err).ShouldNot(HaveOccurred())
   517  
   518  				if test.ExpectedRotation {
   519  					Expect(bytes.Equal(originalKey, encryptionSecret.Data["key"])).Should(BeFalse())
   520  				} else {
   521  					Expect(bytes.Equal(originalKey, encryptionSecret.Data["key"])).Should(BeTrue())
   522  				}
   523  
   524  				// Revert back the fake client to verify no policy updates were triggered
   525  				r.Client = erroringClient.Client
   526  				assertNoTriggerUpdate(r)
   527  			},
   528  		)
   529  	}
   530  }