Skip to content
This repository was archived by the owner on Apr 25, 2023. It is now read-only.

Commit e13973c

Browse files
authored
Merge pull request #1380 from ExpediaInc/infinite-reconciliation-loop-rawstatuscollection
Infinite reconciliation loop (rawstatuscollection)
2 parents 66abaa4 + c7f9e8c commit e13973c

File tree

3 files changed

+249
-36
lines changed

3 files changed

+249
-36
lines changed

pkg/controller/sync/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ func (s *KubeFedSyncController) syncToClusters(fedResource FederatedResource) ut
387387
}
388388

389389
collectedStatus, collectedResourceStatus := dispatcher.CollectedStatus()
390-
klog.V(4).Infof("Setting the federated status '%v'", collectedResourceStatus)
390+
klog.V(4).Infof("Setting the federated status '%v' for %s %q", collectedResourceStatus, kind, key)
391391
return s.setFederatedStatus(fedResource, status.AggregateSuccess, &collectedStatus, &collectedResourceStatus, enableRawResourceStatusCollection)
392392
}
393393

pkg/controller/sync/status/status.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,25 @@ type CollectedResourceStatus struct {
125125
// whether status should be written to the API.
126126
func SetFederatedStatus(fedObject *unstructured.Unstructured, reason AggregateReason, collectedStatus CollectedPropagationStatus, collectedResourceStatus CollectedResourceStatus, resourceStatusCollection bool) (bool, error) {
127127
resource := &GenericFederatedResource{}
128+
128129
err := util.UnstructuredToInterface(fedObject, resource)
129130
if err != nil {
130131
return false, errors.Wrapf(err, "Failed to unmarshall to generic resource")
131132
}
133+
134+
// we apply to collectedResourceStatus the same marshalling applied to GenericFederatedResource
135+
// so the resources can be actually comparable later on
136+
normalizedCollectedResourceStatus, err := normalizeStatus(collectedResourceStatus)
137+
if err != nil {
138+
return false, errors.Wrap(err, "Failed to normalize status")
139+
}
140+
132141
if resource.Status == nil {
133142
resource.Status = &GenericFederatedStatus{}
134143
}
135144

136-
changed := resource.Status.update(fedObject.GetGeneration(), reason, collectedStatus, collectedResourceStatus, resourceStatusCollection)
145+
changed := resource.Status.update(fedObject.GetGeneration(), reason, collectedStatus, *normalizedCollectedResourceStatus, resourceStatusCollection)
146+
137147
if !changed {
138148
return false, nil
139149
}
@@ -177,7 +187,7 @@ func (s *GenericFederatedStatus) update(generation int64, reason AggregateReason
177187
}
178188
}
179189

180-
clustersChanged := s.setClusters(collectedStatus.StatusMap, collectedResourceStatus.StatusMap)
190+
clustersChanged := s.setClusters(collectedStatus.StatusMap, collectedResourceStatus.StatusMap, resourceStatusCollection)
181191

182192
// Indicate that changes were propagated if either status.clusters
183193
// was changed or if existing resources were updated (which could
@@ -196,8 +206,8 @@ func (s *GenericFederatedStatus) update(generation int64, reason AggregateReason
196206
// setClusters sets the status.clusters slice from propagation and resource status
197207
// maps. Returns a boolean indication of whether the status.clusters was
198208
// modified.
199-
func (s *GenericFederatedStatus) setClusters(statusMap PropagationStatusMap, resourceStatusMap map[string]interface{}) bool {
200-
if !s.clustersDiffer(statusMap, resourceStatusMap) {
209+
func (s *GenericFederatedStatus) setClusters(statusMap PropagationStatusMap, resourceStatusMap map[string]interface{}, resourceStatusCollection bool) bool {
210+
if !s.clustersDiffer(statusMap, resourceStatusMap, resourceStatusCollection) {
201211
return false
202212
}
203213
s.Clusters = []GenericClusterStatus{}
@@ -214,9 +224,9 @@ func (s *GenericFederatedStatus) setClusters(statusMap PropagationStatusMap, res
214224

215225
// clustersDiffer checks whether `status.clusters` differs from the
216226
// given status map.
217-
func (s *GenericFederatedStatus) clustersDiffer(statusMap PropagationStatusMap, resourceStatusMap map[string]interface{}) bool {
218-
if len(s.Clusters) != len(statusMap) || len(s.Clusters) != len(resourceStatusMap) {
219-
klog.V(4).Info("Clusters differs from the size")
227+
func (s *GenericFederatedStatus) clustersDiffer(statusMap PropagationStatusMap, resourceStatusMap map[string]interface{}, resourceStatusCollection bool) bool {
228+
if len(s.Clusters) != len(statusMap) || resourceStatusCollection && len(s.Clusters) != len(resourceStatusMap) {
229+
klog.V(4).Infof("Clusters differs from the size: clusters = %v, statusMap = %v, resourceStatusMap = %v", s.Clusters, statusMap, resourceStatusMap)
220230
return true
221231
}
222232
for _, status := range s.Clusters {
@@ -278,3 +288,28 @@ func (s *GenericFederatedStatus) setPropagationCondition(reason AggregateReason,
278288

279289
return updateRequired
280290
}
291+
292+
func normalizeStatus(collectedResourceStatus CollectedResourceStatus) (*CollectedResourceStatus, error) {
293+
if len(collectedResourceStatus.StatusMap) == 0 {
294+
return &collectedResourceStatus, nil
295+
}
296+
cleanedStatus := CollectedResourceStatus{
297+
StatusMap: map[string]interface{}{},
298+
ResourcesUpdated: collectedResourceStatus.ResourcesUpdated,
299+
}
300+
301+
for key, value := range collectedResourceStatus.StatusMap {
302+
content, err := json.Marshal(value)
303+
if err != nil {
304+
return nil, errors.Wrapf(err, "Failed to marshall collected resource status for cluster %s", key)
305+
}
306+
var status interface{}
307+
err = json.Unmarshal(content, &status)
308+
if err != nil {
309+
return nil, errors.Wrapf(err, "Failed to unmarshall collected resource status as interface for cluster %s", key)
310+
}
311+
cleanedStatus.StatusMap[key] = status
312+
}
313+
314+
return &cleanedStatus, nil
315+
}

pkg/controller/sync/status/status_test.go

Lines changed: 206 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,67 +17,187 @@ limitations under the License.
1717
package status
1818

1919
import (
20+
"reflect"
2021
"testing"
2122

2223
apiv1 "k8s.io/api/core/v1"
2324
)
2425

2526
func TestGenericPropagationStatusUpdateChanged(t *testing.T) {
2627
testCases := map[string]struct {
27-
generation int64
28-
reason AggregateReason
29-
statusMap PropagationStatusMap
30-
resourceStatusMap map[string]interface{}
31-
resourcesUpdated bool
32-
expectedChanged bool
28+
generation int64
29+
reason AggregateReason
30+
statusMap PropagationStatusMap
31+
resourceStatusMap map[string]interface{}
32+
remoteStatus interface{}
33+
resourcesUpdated bool
34+
expectedChanged bool
35+
resourceStatusCollection bool
3336
}{
34-
"No change in clusters indicates unchanged": {
37+
"Cluster not propagated indicates changed with status collected enabled": {
38+
statusMap: PropagationStatusMap{
39+
"cluster1": ClusterNotReady,
40+
},
41+
resourceStatusMap: map[string]interface{}{
42+
"cluster1": map[string]interface{}{},
43+
},
44+
reason: AggregateSuccess,
45+
resourcesUpdated: false,
46+
resourceStatusCollection: true,
47+
expectedChanged: true,
48+
},
49+
"Cluster not propagated indicates changed with status collected disabled": {
50+
statusMap: PropagationStatusMap{
51+
"cluster1": ClusterNotReady,
52+
},
53+
resourceStatusMap: map[string]interface{}{
54+
"cluster1": map[string]interface{}{},
55+
},
56+
reason: AggregateSuccess,
57+
resourcesUpdated: false,
58+
resourceStatusCollection: false,
59+
expectedChanged: true,
60+
},
61+
"Cluster status not retrieved indicates changed with status collected enabled": {
62+
statusMap: PropagationStatusMap{},
63+
reason: AggregateSuccess,
64+
resourcesUpdated: false,
65+
resourceStatusCollection: true,
66+
expectedChanged: true,
67+
},
68+
"Cluster status not retrieved indicates changed with status collected disabled": {
69+
statusMap: PropagationStatusMap{},
70+
reason: AggregateSuccess,
71+
resourcesUpdated: false,
72+
resourceStatusCollection: false,
73+
expectedChanged: true,
74+
},
75+
"No collected remote status indicates changed with status collected enabled": {
76+
statusMap: PropagationStatusMap{
77+
"cluster1": ClusterPropagationOK,
78+
},
79+
reason: AggregateSuccess,
80+
resourcesUpdated: false,
81+
resourceStatusCollection: true,
82+
expectedChanged: true,
83+
},
84+
"No collected remote status indicates unchanged with status collected disabled": {
85+
statusMap: PropagationStatusMap{
86+
"cluster1": ClusterPropagationOK,
87+
},
88+
reason: AggregateSuccess,
89+
resourcesUpdated: false,
90+
resourceStatusCollection: false,
91+
expectedChanged: false,
92+
},
93+
"No change in clusters indicates unchanged with status collected enabled": {
3594
statusMap: PropagationStatusMap{
3695
"cluster1": ClusterPropagationOK,
3796
},
3897
resourceStatusMap: map[string]interface{}{
39-
"ready": false,
40-
"stage": "absent",
98+
"cluster1": map[string]interface{}{},
4199
},
42-
resourcesUpdated: false,
43-
expectedChanged: true,
100+
resourcesUpdated: false,
101+
resourceStatusCollection: true,
102+
expectedChanged: true,
44103
},
45-
"No change in clusters with update indicates changed": {
104+
"No change in clusters indicates unchanged with status collected disabled": {
46105
statusMap: PropagationStatusMap{
47106
"cluster1": ClusterPropagationOK,
48107
},
49108
resourceStatusMap: map[string]interface{}{
50-
"ready": false,
51-
"stage": "absent",
109+
"cluster1": map[string]interface{}{},
52110
},
53-
resourcesUpdated: true,
54-
expectedChanged: true,
111+
resourcesUpdated: false,
112+
resourceStatusCollection: false,
113+
expectedChanged: true,
55114
},
56-
"Change in clusters indicates changed": {
115+
"No change in clusters with update indicates changed with status collected enabled": {
57116
statusMap: PropagationStatusMap{
58117
"cluster1": ClusterPropagationOK,
59118
},
60119
resourceStatusMap: map[string]interface{}{
61-
"ready": true,
62-
"stage": "deployed",
120+
"cluster1": map[string]interface{}{},
63121
},
64-
expectedChanged: true,
122+
resourcesUpdated: true,
123+
resourceStatusCollection: true,
124+
expectedChanged: true,
65125
},
66-
"Transition indicates changed": {
67-
reason: NamespaceNotFederated,
68-
expectedChanged: true,
126+
"No change in clusters with update indicates changed with status collected disabled": {
127+
statusMap: PropagationStatusMap{
128+
"cluster1": ClusterPropagationOK,
129+
},
130+
resourceStatusMap: map[string]interface{}{
131+
"cluster1": map[string]interface{}{},
132+
},
133+
resourcesUpdated: true,
134+
resourceStatusCollection: false,
135+
expectedChanged: true,
69136
},
70-
"Changed generation indicates changed": {
71-
generation: 1,
72-
expectedChanged: true,
137+
"No change in remote status indicates unchanged with status collected enabled": {
138+
statusMap: PropagationStatusMap{
139+
"cluster1": ClusterPropagationOK,
140+
},
141+
resourceStatusMap: map[string]interface{}{
142+
"cluster1": map[string]interface{}{
143+
"status": map[string]interface{}{
144+
"status": "remoteStatus",
145+
},
146+
},
147+
},
148+
remoteStatus: map[string]interface{}{
149+
"status": map[string]interface{}{
150+
"status": "remoteStatus",
151+
},
152+
},
153+
resourcesUpdated: false,
154+
resourceStatusCollection: true,
155+
expectedChanged: false,
156+
},
157+
"Change in clusters indicates changed with status collected enabled": {
158+
statusMap: PropagationStatusMap{
159+
"cluster1": ClusterPropagationOK,
160+
"cluster2": ClusterPropagationOK,
161+
},
162+
resourceStatusCollection: true,
163+
expectedChanged: true,
164+
},
165+
"Change in clusters indicates changed with status collected disabled": {
166+
statusMap: PropagationStatusMap{
167+
"cluster1": ClusterPropagationOK,
168+
"cluster2": ClusterPropagationOK,
169+
},
170+
resourceStatusCollection: false,
171+
expectedChanged: true,
172+
},
173+
"Transition indicates changed with remote status collection enabled": {
174+
reason: NamespaceNotFederated,
175+
resourceStatusCollection: true,
176+
expectedChanged: true,
177+
},
178+
"Transition indicates changed with remote status collection disabled": {
179+
reason: NamespaceNotFederated,
180+
resourceStatusCollection: false,
181+
expectedChanged: true,
182+
},
183+
"Changed generation indicates changed with remote status collection enabled": {
184+
generation: 1,
185+
resourceStatusCollection: true,
186+
expectedChanged: true,
187+
},
188+
"Changed generation indicates changed with remote status collection disabled": {
189+
generation: 1,
190+
resourceStatusCollection: false,
191+
expectedChanged: true,
73192
},
74193
}
75194
for testName, tc := range testCases {
76195
t.Run(testName, func(t *testing.T) {
77196
fedStatus := &GenericFederatedStatus{
78197
Clusters: []GenericClusterStatus{
79198
{
80-
Name: "cluster1",
199+
Name: "cluster1",
200+
RemoteStatus: tc.remoteStatus,
81201
},
82202
},
83203
Conditions: []*GenericCondition{
@@ -95,10 +215,68 @@ func TestGenericPropagationStatusUpdateChanged(t *testing.T) {
95215
StatusMap: tc.resourceStatusMap,
96216
ResourcesUpdated: tc.resourcesUpdated,
97217
}
98-
changed := fedStatus.update(tc.generation, tc.reason, collectedStatus, collectedResourceStatus, true)
218+
changed := fedStatus.update(tc.generation, tc.reason, collectedStatus, collectedResourceStatus, tc.resourceStatusCollection)
99219
if tc.expectedChanged != changed {
100220
t.Fatalf("Expected changed to be %v, got %v", tc.expectedChanged, changed)
101221
}
102222
})
103223
}
104224
}
225+
226+
func TestNormalizeStatus(t *testing.T) {
227+
testCases := []struct {
228+
name string
229+
input CollectedResourceStatus
230+
expectedResult *CollectedResourceStatus
231+
expectedError error
232+
}{
233+
{
234+
name: "CollectedResourceStatus is not modified if StatusMap is nil",
235+
input: CollectedResourceStatus{},
236+
expectedResult: &CollectedResourceStatus{},
237+
},
238+
{
239+
name: "CollectedResourceStatus is not modified if StatusMap is empty",
240+
input: CollectedResourceStatus{
241+
StatusMap: map[string]interface{}{},
242+
},
243+
expectedResult: &CollectedResourceStatus{
244+
StatusMap: map[string]interface{}{},
245+
},
246+
},
247+
{
248+
name: "CollectedResourceStatus StatusMap is correctly normalized and numbers are converted to float64",
249+
input: CollectedResourceStatus{
250+
StatusMap: map[string]interface{}{
251+
"number": 1,
252+
"string": "value",
253+
"complexobj": map[string]int{
254+
"one": 1,
255+
},
256+
},
257+
},
258+
expectedResult: &CollectedResourceStatus{
259+
StatusMap: map[string]interface{}{
260+
"number": float64(1),
261+
"string": "value",
262+
"complexobj": map[string]interface{}{
263+
"one": float64(1),
264+
},
265+
},
266+
},
267+
},
268+
}
269+
270+
for _, tc := range testCases {
271+
t.Run(tc.name, func(t *testing.T) {
272+
actual, err := normalizeStatus(tc.input)
273+
if err != tc.expectedError {
274+
t.Fatalf("Expected error to be %v, got %v", tc.expectedError, err)
275+
}
276+
277+
if !reflect.DeepEqual(tc.expectedResult, actual) {
278+
t.Fatalf("Expected result to be %#v, got %#v", tc.expectedResult, actual)
279+
}
280+
})
281+
}
282+
}

0 commit comments

Comments
 (0)