Skip to content

Commit db12f27

Browse files
committed
certrotation: don't hotloop on resources managed by several controllers
Ensure that multiple controllers managing single resource don't overwrite each other metadata changes. This is archived by checking ownership references - if current controller is set as first owner other metadata changes are being ignored. This ensures that multiple cert rotation controllers can handle single CA bundle configmap - but only one of them will be updating configmap metadata - whichever is started first
1 parent 079ddac commit db12f27

File tree

3 files changed

+149
-3
lines changed

3 files changed

+149
-3
lines changed

pkg/operator/certrotation/cabundle.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,19 @@ func (c CABundleConfigMap) EnsureConfigMapCABundle(ctx context.Context, signingC
5858
)}
5959
}
6060

61+
isPrimaryOwner := false
6162
needsMetadataUpdate := false
6263
if c.Owner != nil {
6364
needsMetadataUpdate = ensureOwnerReference(&caBundleConfigMap.ObjectMeta, c.Owner)
65+
if caBundleConfigMap.ObjectMeta.OwnerReferences[0] == *c.Owner {
66+
isPrimaryOwner = true
67+
}
68+
}
69+
// Don't update metadata if its not primary CM owner
70+
if isPrimaryOwner {
71+
needsMetadataUpdate = c.AdditionalAnnotations.EnsureTLSMetadataUpdate(&caBundleConfigMap.ObjectMeta) || needsMetadataUpdate
6472
}
65-
needsMetadataUpdate = c.AdditionalAnnotations.EnsureTLSMetadataUpdate(&caBundleConfigMap.ObjectMeta) || needsMetadataUpdate
73+
6674
if needsMetadataUpdate && len(caBundleConfigMap.ResourceVersion) > 0 {
6775
actualCABundleConfigMap, _, err := resourceapply.ApplyConfigMap(ctx, c.Client, c.EventRecorder, caBundleConfigMap)
6876
if err != nil {

pkg/operator/certrotation/cabundle_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/davecgh/go-spew/spew"
17+
"github.com/google/go-cmp/cmp"
1718

1819
"github.com/openshift/library-go/pkg/crypto"
1920
"github.com/openshift/library-go/pkg/operator/events"
@@ -388,3 +389,132 @@ func signCertificate(template *x509.Certificate, requestKey gcrypto.PublicKey, i
388389
}
389390
return certs[0], nil
390391
}
392+
393+
func TestEnsureConfigMapCABundleWontHotloop(t *testing.T) {
394+
t.Run("TestEnsureConfigMapCABundleHotloop", func(t *testing.T) {
395+
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
396+
397+
startingObj := &corev1.ConfigMap{
398+
ObjectMeta: metav1.ObjectMeta{
399+
Namespace: "ns", Name: "trust-bundle",
400+
ResourceVersion: "10",
401+
},
402+
Data: map[string]string{},
403+
}
404+
indexer.Add(startingObj)
405+
client := kubefake.NewSimpleClientset(startingObj)
406+
407+
c1 := &CABundleConfigMap{
408+
Namespace: "ns",
409+
Name: "trust-bundle",
410+
411+
Client: client.CoreV1(),
412+
Lister: corev1listers.NewConfigMapLister(indexer),
413+
EventRecorder: events.NewInMemoryRecorder("test"),
414+
AdditionalAnnotations: AdditionalAnnotations{JiraComponent: "test_1"},
415+
Owner: &metav1.OwnerReference{Name: "operator_1"},
416+
}
417+
418+
newCA, err := newTestCACertificate(pkix.Name{CommonName: "signer-tests"}, int64(1), metav1.Duration{Duration: time.Hour * 24 * 60}, time.Now)
419+
if err != nil {
420+
t.Fatal(err)
421+
}
422+
_, err = c1.EnsureConfigMapCABundle(context.TODO(), newCA)
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
427+
actions := client.Actions()
428+
if len(actions) != 4 {
429+
t.Fatal(spew.Sdump(actions))
430+
}
431+
432+
if !actions[0].Matches("get", "configmaps") {
433+
t.Error(actions[0])
434+
}
435+
if !actions[1].Matches("update", "configmaps") {
436+
t.Error(actions[1])
437+
}
438+
if !actions[2].Matches("get", "configmaps") {
439+
t.Error(actions[2])
440+
}
441+
if !actions[3].Matches("update", "configmaps") {
442+
t.Error(actions[3])
443+
}
444+
445+
actual_1 := actions[3].(clienttesting.CreateAction).GetObject().(*corev1.ConfigMap)
446+
if certType, _ := CertificateTypeFromObject(actual_1); certType != CertificateTypeCABundle {
447+
t.Errorf("expected certificate type 'ca-bundle', got: %v", certType)
448+
}
449+
if len(actual_1.Data["ca-bundle.crt"]) == 0 {
450+
t.Error(actual_1.Data)
451+
}
452+
if len(actual_1.OwnerReferences) != 1 {
453+
t.Errorf("expected to have exactly one owner reference")
454+
}
455+
if actual_1.OwnerReferences[0].Name != "operator_1" {
456+
t.Errorf("expected owner reference to be 'operator_1', got %v", actual_1.OwnerReferences[0].Name)
457+
}
458+
if got, exists := actual_1.Annotations["openshift.io/owning-component"]; !exists || got != "test_1" {
459+
t.Errorf("expected owner annotation to be 'test_1', got: %#v", actual_1.Annotations)
460+
}
461+
462+
// Run another cycle and make sure updates are no longer issued
463+
err = indexer.Update(actual_1)
464+
if err != nil {
465+
t.Fatal(err)
466+
}
467+
client.ClearActions()
468+
469+
c2 := &CABundleConfigMap{
470+
Namespace: "ns",
471+
Name: "trust-bundle",
472+
473+
Client: client.CoreV1(),
474+
Lister: corev1listers.NewConfigMapLister(indexer),
475+
EventRecorder: events.NewInMemoryRecorder("test"),
476+
AdditionalAnnotations: AdditionalAnnotations{JiraComponent: "test_2"},
477+
Owner: &metav1.OwnerReference{Name: "operator_2"},
478+
}
479+
_, err = c2.EnsureConfigMapCABundle(context.TODO(), newCA)
480+
if err != nil {
481+
t.Fatal(err)
482+
}
483+
actions = client.Actions()
484+
if len(actions) != 2 {
485+
t.Fatal(spew.Sdump(actions))
486+
}
487+
if !actions[0].Matches("get", "configmaps") {
488+
t.Error(actions[0])
489+
}
490+
if !actions[1].Matches("update", "configmaps") {
491+
t.Error(actions[1])
492+
}
493+
494+
actual_2 := actions[1].(clienttesting.CreateAction).GetObject().(*corev1.ConfigMap)
495+
if certType, _ := CertificateTypeFromObject(actual_1); certType != CertificateTypeCABundle {
496+
t.Errorf("expected certificate type 'ca-bundle', got: %v", certType)
497+
}
498+
if len(actual_2.Data["ca-bundle.crt"]) == 0 {
499+
t.Error(actual_2.Data)
500+
}
501+
if len(actual_2.OwnerReferences) != 2 {
502+
t.Errorf("expected to have exactly two owner references")
503+
}
504+
if actual_2.OwnerReferences[0].Name != "operator_1" {
505+
t.Errorf("expected first owner reference to be 'operator_1', got %v", actual_2.OwnerReferences[0].Name)
506+
}
507+
if actual_2.OwnerReferences[1].Name != "operator_2" {
508+
t.Errorf("expected second owner reference to be 'operator_2', got %v", actual_2.OwnerReferences[0].Name)
509+
}
510+
if got, exists := actual_2.Annotations["openshift.io/owning-component"]; !exists || got != "test_1" {
511+
t.Errorf("unexpected owner annotation: %#v", actual_2.Annotations)
512+
}
513+
514+
// Ensure that the second controller didn't cause the contents to change
515+
diff := cmp.Diff(actual_1.Data["ca-bundle.crt"], actual_2.Data["ca-bundle.crt"])
516+
if len(diff) != 0 {
517+
t.Errorf("second controller caused content change: %v", diff)
518+
}
519+
})
520+
}

pkg/operator/certrotation/metadata.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ import (
77

88
func ensureMetadataUpdate(secret *corev1.Secret, owner *metav1.OwnerReference, additionalAnnotations AdditionalAnnotations) bool {
99
needsMetadataUpdate := false
10+
isPrimaryOwner := false
1011
// no ownerReference set
1112
if owner != nil {
1213
needsMetadataUpdate = ensureOwnerReference(&secret.ObjectMeta, owner)
14+
if secret.ObjectMeta.OwnerReferences[0] == *owner {
15+
isPrimaryOwner = true
16+
}
17+
}
18+
// skip metadata updates if current controller is not the primary owner
19+
if isPrimaryOwner {
20+
// ownership annotations not set
21+
return additionalAnnotations.EnsureTLSMetadataUpdate(&secret.ObjectMeta) || needsMetadataUpdate
1322
}
14-
// ownership annotations not set
15-
return additionalAnnotations.EnsureTLSMetadataUpdate(&secret.ObjectMeta) || needsMetadataUpdate
23+
return needsMetadataUpdate
1624
}
1725

1826
func ensureSecretTLSTypeSet(secret *corev1.Secret) bool {

0 commit comments

Comments
 (0)