Skip to content

Commit adb68bc

Browse files
authored
fix(clusterCache): don't miss finding live obj if obj is cluster-scoped and namespacedResources is in transition (#597)
* sync.Reconcile: guard against incomplete discovery When Reconcile performs its logic to compare the desired state (target objects) against the actual state (live objects), it looks up each live object based on a key comprised of data from the target object: API group, API kind, namespace, and name. While group, kind, and name will always be accurate, there is a chance that the value for namespace is not. If a cluster-scoped target object has a namespace (because it incorrectly has a namespace from its source) or the namespace parameter passed into the Reconcile method has a non-empty value (indicating a default value to use on namespace-scoped objects that don't have it set in the source), AND the resInfo ResourceInfoProvider has incomplete or missing API discovery data, the call to IsNamespacedOrUnknown will return true when the information is unknown. This leads to the key being incorrect - it will have a value for namespace when it shouldn't. As a result, indexing into liveObjByKey will fail. This failure results in the reconciliation containing incorrect data: there will be a nil entry appended to targetObjs when there shouldn't be. Signed-off-by: Andy Goldstein <[email protected]> * Address code review comments Signed-off-by: Andy Goldstein <[email protected]> --------- Signed-off-by: Andy Goldstein <[email protected]>
1 parent a0c23b4 commit adb68bc

File tree

2 files changed

+77
-7
lines changed

2 files changed

+77
-7
lines changed

pkg/sync/reconcile.go

+25-7
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,36 @@ func Reconcile(targetObjs []*unstructured.Unstructured, liveObjByKey map[kube.Re
7676
managedLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
7777
for i, obj := range targetObjs {
7878
gvk := obj.GroupVersionKind()
79+
7980
ns := text.FirstNonEmpty(obj.GetNamespace(), namespace)
80-
if namespaced := kubeutil.IsNamespacedOrUnknown(resInfo, obj.GroupVersionKind().GroupKind()); !namespaced {
81-
ns = ""
81+
82+
namespaced, err := resInfo.IsNamespaced(gvk.GroupKind())
83+
unknownScope := err != nil
84+
85+
var keysToCheck []kubeutil.ResourceKey
86+
// If we get an error, we don't know whether the resource is namespaced. So we need to check for both in the
87+
// live objects. If we don't check for both, then we risk missing the object and deleting it.
88+
if namespaced || unknownScope {
89+
keysToCheck = append(keysToCheck, kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName()))
8290
}
83-
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
84-
if liveObj, ok := liveObjByKey[key]; ok {
85-
managedLiveObj[i] = liveObj
86-
delete(liveObjByKey, key)
87-
} else {
91+
if !namespaced || unknownScope {
92+
keysToCheck = append(keysToCheck, kubeutil.NewResourceKey(gvk.Group, gvk.Kind, "", obj.GetName()))
93+
}
94+
95+
found := false
96+
for _, key := range keysToCheck {
97+
if liveObj, ok := liveObjByKey[key]; ok {
98+
managedLiveObj[i] = liveObj
99+
delete(liveObjByKey, key)
100+
found = true
101+
break
102+
}
103+
}
104+
if !found {
88105
managedLiveObj[i] = nil
89106
}
90107
}
108+
91109
for _, obj := range liveObjByKey {
92110
targetObjs = append(targetObjs, nil)
93111
managedLiveObj = append(managedLiveObj, obj)

pkg/sync/reconcile_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package sync
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
11+
"github.com/argoproj/gitops-engine/pkg/utils/kube"
12+
)
13+
14+
type unknownResourceInfoProvider struct{}
15+
16+
func (e *unknownResourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
17+
return false, errors.New("unknown")
18+
}
19+
20+
func TestReconcileWithUnknownDiscoveryDataForClusterScopedResources(t *testing.T) {
21+
targetObjs := []*unstructured.Unstructured{
22+
{
23+
Object: map[string]interface{}{
24+
"apiVersion": "v1",
25+
"kind": "Namespace",
26+
"metadata": map[string]interface{}{
27+
"name": "my-namespace",
28+
},
29+
},
30+
},
31+
}
32+
33+
liveNS := &unstructured.Unstructured{
34+
Object: map[string]interface{}{
35+
"apiVersion": "v1",
36+
"kind": "Namespace",
37+
"metadata": map[string]interface{}{
38+
"name": "my-namespace",
39+
"uid": "c99ff56d-1921-495d-8512-d66cdfcb5740",
40+
},
41+
},
42+
}
43+
liveObjByKey := map[kube.ResourceKey]*unstructured.Unstructured{
44+
kube.NewResourceKey("", "Namespace", "", "my-namespace"): liveNS,
45+
}
46+
47+
result := Reconcile(targetObjs, liveObjByKey, "some-namespace", &unknownResourceInfoProvider{})
48+
require.Len(t, result.Target, 1)
49+
require.Equal(t, result.Target[0], targetObjs[0])
50+
require.Len(t, result.Live, 1)
51+
require.Equal(t, result.Live[0], liveNS)
52+
}

0 commit comments

Comments
 (0)