Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pkg/apis/work/v1/applier/workapplier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,38 @@ func TestWorkApplierWithTypedClient(t *testing.T) {
}
assertActions(t, fakeWorkClient.Actions(), "patch")

assertExternalMetadataChangeDetected := func(description string, tamper func(*workapiv1.ManifestWork)) {
obj, exists, getErr := workInformerFactory.Work().V1().ManifestWorks().Informer().GetStore().GetByKey("test/test")
if getErr != nil || !exists {
t.Fatalf("%s: failed to get work from store: err=%v exists=%v", description, getErr, exists)
}

tamperedWork := obj.(*workapiv1.ManifestWork).DeepCopy()
tamper(tamperedWork)
if err := workInformerFactory.Work().V1().ManifestWorks().Informer().GetStore().Update(tamperedWork); err != nil {
t.Errorf("%s: failed to update work with err %v", description, err)
}

fakeWorkClient.ClearActions()
_, err = workApplier.Apply(context.TODO(), newWork)
if err != nil {
t.Errorf("%s: failed to apply work with err %v", description, err)
}
assertActions(t, fakeWorkClient.Actions(), "patch")
}
Comment thread
mkolesnik marked this conversation as resolved.

assertExternalMetadataChangeDetected("external label change", func(w *workapiv1.ManifestWork) {
w.Labels = map[string]string{"tampered": "true"}
})
assertExternalMetadataChangeDetected("external annotation change", func(w *workapiv1.ManifestWork) {
w.Annotations = map[string]string{"tampered": "true"}
})
assertExternalMetadataChangeDetected("external owner reference change", func(w *workapiv1.ManifestWork) {
w.OwnerReferences = []metav1.OwnerReference{
{APIVersion: "v1", Kind: "ConfigMap", Name: "rogue-owner", UID: "rogue"},
}
})

fakeWorkClient.ClearActions()
err = workApplier.Delete(context.TODO(), newWork.Namespace, newWork.Name)
if err != nil {
Expand Down
25 changes: 20 additions & 5 deletions pkg/apis/work/v1/applier/workcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"sync"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"

workapiv1 "open-cluster-management.io/api/work/v1"
Expand All @@ -20,6 +21,7 @@ type workKey struct {
type cachedResource struct {
resourceHash string
generation int64
metadataHash string
}

type workCache struct {
Expand Down Expand Up @@ -48,6 +50,7 @@ func (w *workCache) updateCache(required, existing *workapiv1.ManifestWork) {
value := cachedResource{
resourceHash: hashOfResourceStruct(required),
generation: existing.Generation,
metadataHash: hashOfMetadata(required),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think resourceHash has included metadata section already. Are you trying to detect if label/annotation of the exisiting mw is changed? If so, I would consider an annotatil/label merge when apply rather than replace.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick review!

You're right that resourceHash hashes the desired work, so it catches when we change our labels.
The problem is it doesn't catch when someone else changes labels on the existing work (see #223 which I reported).
When that happens, our desired hash stays the same and generation doesn't bump, so the cache skips and the external change is never corrected.

Re merge vs replace, that's a good point and worth discussing separately.
This PR just makes the cache consistent with what the applier already does when the cache misses.

}

w.cache[key] = value
Expand Down Expand Up @@ -79,12 +82,11 @@ func (w *workCache) safeToSkipApply(required, existing *workapiv1.ManifestWork)
generation := existing.Generation
w.mutex.RLock()
defer w.mutex.RUnlock()
var generationMatch, hashMatch bool
metadataHash := hashOfMetadata(existing)

if cached, exists := w.cache[cacheKey]; exists {
generationMatch = cached.generation == generation
hashMatch = cached.resourceHash == resourceHash
if generationMatch && hashMatch {
klog.V(4).Infof("found matching generation & manifest hash")
if cached.generation == generation && cached.resourceHash == resourceHash && cached.metadataHash == metadataHash {
klog.V(4).Infof("found matching generation, manifest hash & metadata hash")
return true
}
}
Expand All @@ -101,3 +103,16 @@ func hashOfResourceStruct(o interface{}) string {
rval := fmt.Sprintf("%x", h.Sum(nil))
return rval
}

func hashOfMetadata(work *workapiv1.ManifestWork) string {
meta := struct {
Labels map[string]string
Annotations map[string]string
OwnerReferences []metav1.OwnerReference
}{
Labels: work.Labels,
Annotations: work.Annotations,
OwnerReferences: work.OwnerReferences,
}
return hashOfResourceStruct(meta)
}
48 changes: 48 additions & 0 deletions pkg/apis/work/v1/applier/workcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,51 @@ func TestCache(t *testing.T) {
t.Errorf("should update work if related cache is not found")
}
}

func TestCacheDetectsExternalMetadataChange(t *testing.T) {
cases := []struct {
name string
required metav1.ObjectMeta
tamper func(*workapiv1.ManifestWork)
}{
{
name: "labels",
required: metav1.ObjectMeta{Name: "test", Namespace: "cluster1", Labels: map[string]string{"managed-by": "addon"}},
tamper: func(w *workapiv1.ManifestWork) { w.Labels["managed-by"] = "tampered" },
},
{
name: "annotations",
required: metav1.ObjectMeta{Name: "test", Namespace: "cluster1", Annotations: map[string]string{"config-hash": "abc123"}},
tamper: func(w *workapiv1.ManifestWork) { w.Annotations["config-hash"] = "tampered" },
},
{
name: "owner references",
required: metav1.ObjectMeta{Name: "test", Namespace: "cluster1", OwnerReferences: []metav1.OwnerReference{
{APIVersion: "v1", Kind: "ConfigMap", Name: "owner", UID: "abc"},
}},
tamper: func(w *workapiv1.ManifestWork) { w.OwnerReferences[0].Name = "tampered" },
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
cache := newWorkCache()

requiredWork := &workapiv1.ManifestWork{ObjectMeta: c.required}
existingWork := requiredWork.DeepCopy()
existingWork.Generation = 1

cache.updateCache(requiredWork, existingWork)

if !cache.safeToSkipApply(requiredWork, existingWork) {
t.Errorf("should skip apply when nothing changed")
}

c.tamper(existingWork)

if cache.safeToSkipApply(requiredWork, existingWork) {
t.Errorf("should not skip apply when %s are externally modified", c.name)
}
})
}
}
Loading