2
2
3
3
import java .util .AbstractMap ;
4
4
import java .util .ArrayList ;
5
- import java .util .Arrays ;
6
5
import java .util .Collections ;
7
6
import java .util .HashMap ;
8
7
import java .util .LinkedHashMap ;
9
8
import java .util .List ;
10
9
import java .util .Map ;
11
10
import java .util .Map .Entry ;
11
+ import java .util .Objects ;
12
12
import java .util .Optional ;
13
13
import java .util .Set ;
14
- import java .util .SortedMap ;
15
14
import java .util .TreeMap ;
16
15
17
16
import org .slf4j .Logger ;
31
30
32
31
/**
33
32
* Matches the actual state on the server vs the desired state. Based on the managedFields of SSA.
34
- *
35
33
* <p>
36
- * The basis of algorithm is to extract the fields managed we convert resources to Map/List
34
+ * The basis of the algorithm is to extract the managed fields by converting resources to a Map/List
37
35
* composition. The actual resource (from the server) is pruned, all the fields which are not
38
- * mentioned in managedFields of the target manager is removed. Some irrelevant fields are also
39
- * removed from desired. And the two resulted Maps are compared for equality. The implementation is
40
- * a bit nasty since have to deal with some specific cases of managedFields format.
41
- * </p>
36
+ * mentioned in managedFields of the target manager are removed. Some irrelevant fields are also
37
+ * removed from the desired resource. Finally, the two resulting maps are compared for equality.
38
+ * <p>
39
+ * The implementation is a bit nasty since we have to deal with some specific cases of managedFields
40
+ * formats.
42
41
*
43
42
* @param <R> matched resource type
44
43
*/
48
47
// see also: https://kubernetes.slack.com/archives/C0123CNN8F3/p1686141087220719
49
48
public class SSABasedGenericKubernetesResourceMatcher <R extends HasMetadata > {
50
49
51
- @ SuppressWarnings ("rawtypes" )
52
- private static final SSABasedGenericKubernetesResourceMatcher INSTANCE =
53
- new SSABasedGenericKubernetesResourceMatcher <>();
54
50
public static final String APPLY_OPERATION = "Apply" ;
55
51
public static final String DOT_KEY = "." ;
56
52
53
+ @ SuppressWarnings ("rawtypes" )
54
+ private static final SSABasedGenericKubernetesResourceMatcher INSTANCE =
55
+ new SSABasedGenericKubernetesResourceMatcher <>();
57
56
private static final List <String > IGNORED_METADATA =
58
- Arrays .asList ("creationTimestamp" , "deletionTimestamp" ,
59
- "generation" , "selfLink" , "uid" );
57
+ List .of ("creationTimestamp" , "deletionTimestamp" , "generation" , "selfLink" , "uid" );
60
58
61
59
@ SuppressWarnings ("unchecked" )
62
60
public static <L extends HasMetadata > SSABasedGenericKubernetesResourceMatcher <L > getInstance () {
@@ -92,31 +90,26 @@ public boolean matches(R actual, R desired, Context<?> context) {
92
90
var objectMapper = context .getClient ().getKubernetesSerialization ();
93
91
94
92
var actualMap = objectMapper .convertValue (actual , Map .class );
95
-
96
93
sanitizeState (actual , desired , actualMap );
97
-
98
- var desiredMap = objectMapper .convertValue (desired , Map .class );
99
- if (LoggingUtils .isNotSensitiveResource (desired )) {
100
- log .trace ("Original actual: \n {} \n original desired: \n {} " , actual , desiredMap );
101
- }
102
-
103
94
var prunedActual = new HashMap <String , Object >(actualMap .size ());
104
95
keepOnlyManagedFields (prunedActual , actualMap ,
105
96
managedFieldsEntry .getFieldsV1 ().getAdditionalProperties (),
106
97
objectMapper );
107
98
99
+ var desiredMap = objectMapper .convertValue (desired , Map .class );
100
+ if (LoggingUtils .isNotSensitiveResource (desired )) {
101
+ log .trace ("Original actual:\n {}\n original desired:\n {}" , actual , desiredMap );
102
+ }
108
103
removeIrrelevantValues (desiredMap );
109
104
110
105
var matches = prunedActual .equals (desiredMap );
111
-
112
106
if (!matches && log .isDebugEnabled () && LoggingUtils .isNotSensitiveResource (desired )) {
113
107
var diff = getDiff (prunedActual , desiredMap , objectMapper );
114
108
log .debug (
115
- "Diff between actual and desired state for resource: {} with name: {} in namespace: {} is: \n {}" ,
109
+ "Diff between actual and desired state for resource: {} with name: {} in namespace: {} is:\n {}" ,
116
110
actual .getKind (), actual .getMetadata ().getName (), actual .getMetadata ().getNamespace (),
117
111
diff );
118
112
}
119
-
120
113
return matches ;
121
114
}
122
115
@@ -125,24 +118,23 @@ private String getDiff(Map<String, Object> prunedActualMap, Map<String, Object>
125
118
var actualYaml = serialization .asYaml (sortMap (prunedActualMap ));
126
119
var desiredYaml = serialization .asYaml (sortMap (desiredMap ));
127
120
if (log .isTraceEnabled ()) {
128
- log .trace ("Pruned actual resource: \n {} \n desired resource: \n {} " , actualYaml ,
129
- desiredYaml );
121
+ log .trace ("Pruned actual resource:\n {} \n desired resource:\n {} " , actualYaml , desiredYaml );
130
122
}
131
123
132
124
var patch = DiffUtils .diff (actualYaml .lines ().toList (), desiredYaml .lines ().toList ());
133
- List < String > unifiedDiff =
125
+ var unifiedDiff =
134
126
UnifiedDiffUtils .generateUnifiedDiff ("" , "" , actualYaml .lines ().toList (), patch , 1 );
135
127
return String .join ("\n " , unifiedDiff );
136
128
}
137
129
138
130
@ SuppressWarnings ("unchecked" )
139
131
Map <String , Object > sortMap (Map <String , Object > map ) {
140
- List < String > sortedKeys = new ArrayList <>(map .keySet ());
132
+ var sortedKeys = new ArrayList <>(map .keySet ());
141
133
Collections .sort (sortedKeys );
142
134
143
- Map < String , Object > sortedMap = new LinkedHashMap <>();
144
- for (String key : sortedKeys ) {
145
- Object value = map .get (key );
135
+ var sortedMap = new LinkedHashMap <String , Object >();
136
+ for (var key : sortedKeys ) {
137
+ var value = map .get (key );
146
138
if (value instanceof Map ) {
147
139
sortedMap .put (key , sortMap ((Map <String , Object >) value ));
148
140
} else if (value instanceof List ) {
@@ -156,8 +148,8 @@ Map<String, Object> sortMap(Map<String, Object> map) {
156
148
157
149
@ SuppressWarnings ("unchecked" )
158
150
List <Object > sortListItems (List <Object > list ) {
159
- List < Object > sortedList = new ArrayList <>();
160
- for (Object item : list ) {
151
+ var sortedList = new ArrayList <>();
152
+ for (var item : list ) {
161
153
if (item instanceof Map ) {
162
154
sortedList .add (sortMap ((Map <String , Object >) item ));
163
155
} else if (item instanceof List ) {
@@ -173,21 +165,22 @@ List<Object> sortListItems(List<Object> list) {
173
165
* Correct for known issue with SSA
174
166
*/
175
167
private void sanitizeState (R actual , R desired , Map <String , Object > actualMap ) {
176
- if (desired instanceof StatefulSet desiredStatefulSet ) {
177
- StatefulSet actualStatefulSet = (StatefulSet ) actual ;
178
- int claims = desiredStatefulSet .getSpec ().getVolumeClaimTemplates ().size ();
179
- if (claims == actualStatefulSet .getSpec ().getVolumeClaimTemplates ().size ()) {
168
+ if (actual instanceof StatefulSet actualStatefulSet
169
+ && desired instanceof StatefulSet desiredStatefulSet ) {
170
+ var actualSpec = actualStatefulSet .getSpec ();
171
+ var desiredSpec = desiredStatefulSet .getSpec ();
172
+ int claims = desiredSpec .getVolumeClaimTemplates ().size ();
173
+ if (claims == actualSpec .getVolumeClaimTemplates ().size ()) {
180
174
for (int i = 0 ; i < claims ; i ++) {
181
- if ( desiredStatefulSet . getSpec (). getVolumeClaimTemplates ().get (i ). getSpec ()
182
- .getVolumeMode () == null ) {
175
+ var claim = desiredSpec . getVolumeClaimTemplates ().get (i );
176
+ if ( claim . getSpec () .getVolumeMode () == null ) {
183
177
Optional .ofNullable (
184
178
GenericKubernetesResource .get (actualMap , "spec" , "volumeClaimTemplates" , i , "spec" ))
185
179
.map (Map .class ::cast ).ifPresent (m -> m .remove ("volumeMode" ));
186
180
}
187
- if (desiredStatefulSet .getSpec ().getVolumeClaimTemplates ().get (i ).getStatus () == null ) {
188
- Optional
189
- .ofNullable (
190
- GenericKubernetesResource .get (actualMap , "spec" , "volumeClaimTemplates" , i ))
181
+ if (claim .getStatus () == null ) {
182
+ Optional .ofNullable (
183
+ GenericKubernetesResource .get (actualMap , "spec" , "volumeClaimTemplates" , i ))
191
184
.map (Map .class ::cast ).ifPresent (m -> m .remove ("status" ));
192
185
}
193
186
}
@@ -212,19 +205,17 @@ private static void removeIrrelevantValues(Map<String, Object> desiredMap) {
212
205
private static void keepOnlyManagedFields (Map <String , Object > result ,
213
206
Map <String , Object > actualMap ,
214
207
Map <String , Object > managedFields , KubernetesSerialization objectMapper ) {
215
-
216
208
if (managedFields .isEmpty ()) {
217
209
result .putAll (actualMap );
218
210
return ;
219
211
}
220
- for (Map . Entry < String , Object > entry : managedFields .entrySet ()) {
221
- String key = entry .getKey ();
212
+ for (var entry : managedFields .entrySet ()) {
213
+ var key = entry .getKey ();
222
214
if (key .startsWith (F_PREFIX )) {
223
- String keyInActual = keyWithoutPrefix (key );
215
+ var keyInActual = keyWithoutPrefix (key );
224
216
var managedFieldValue = (Map <String , Object >) entry .getValue ();
225
217
if (isNestedValue (managedFieldValue )) {
226
218
var managedEntrySet = managedFieldValue .entrySet ();
227
-
228
219
// two special cases "k:" and "v:" prefixes
229
220
if (isListKeyEntrySet (managedEntrySet )) {
230
221
handleListKeyEntrySet (result , actualMap , objectMapper , keyInActual , managedEntrySet );
@@ -260,7 +251,6 @@ private static void fillResultsAndTraverseFurther(Map<String, Object> result,
260
251
result .put (keyInActual , emptyMapValue );
261
252
var actualMapValue = actualMap .getOrDefault (keyInActual , Collections .emptyMap ());
262
253
log .debug ("key: {} actual map value: managedFieldValue: {}" , keyInActual , managedFieldValue );
263
-
264
254
keepOnlyManagedFields (emptyMapValue , (Map <String , Object >) actualMapValue ,
265
255
(Map <String , Object >) managedFields .get (key ), objectMapper );
266
256
}
@@ -288,10 +278,10 @@ private static void handleListKeyEntrySet(Map<String, Object> result,
288
278
result .put (keyInActual , valueList );
289
279
var actualValueList = (List <Map <String , Object >>) actualMap .get (keyInActual );
290
280
291
- SortedMap <Integer , Map <String , Object >> targetValuesByIndex = new TreeMap < >();
292
- Map <Integer , Map <String , Object >> managedEntryByIndex = new HashMap < >();
281
+ var targetValuesByIndex = new TreeMap <Integer , Map <String , Object >>();
282
+ var managedEntryByIndex = new HashMap <Integer , Map <String , Object >>();
293
283
294
- for (Map . Entry < String , Object > listEntry : managedEntrySet ) {
284
+ for (var listEntry : managedEntrySet ) {
295
285
if (DOT_KEY .equals (listEntry .getKey ())) {
296
286
continue ;
297
287
}
@@ -310,42 +300,35 @@ private static void handleListKeyEntrySet(Map<String, Object> result,
310
300
}
311
301
312
302
/**
313
- * Set values, the "v:" prefix. Form in managed fields: "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}}
303
+ * Set values, the {@code "v:"} prefix. Form in managed fields:
304
+ * {@code "f:some-set":{"v:1":{}},"v:2":{},"v:3":{}}.
305
+ * <p>
314
306
* Note that this should be just used in very rare cases, actually was not able to produce a
315
307
* sample. Kubernetes developers who worked on this feature were not able to provide one either
316
308
* when prompted. Basically this method just adds the values from {@code "v:<value>"} to the
317
309
* result.
318
310
*/
319
- @ SuppressWarnings ("rawtypes" )
320
311
private static void handleSetValues (Map <String , Object > result , Map <String , Object > actualMap ,
321
312
KubernetesSerialization objectMapper , String keyInActual ,
322
313
Set <Entry <String , Object >> managedEntrySet ) {
323
314
var valueList = new ArrayList <>();
324
315
result .put (keyInActual , valueList );
325
- for (Map . Entry < String , Object > valueEntry : managedEntrySet ) {
316
+ for (var valueEntry : managedEntrySet ) {
326
317
// not clear if this can happen
327
318
if (DOT_KEY .equals (valueEntry .getKey ())) {
328
319
continue ;
329
320
}
330
- Class <?> targetClass = null ;
331
- List values = (List ) actualMap .get (keyInActual );
332
- if (!(values .get (0 ) instanceof Map )) {
333
- targetClass = values .get (0 ).getClass ();
334
- }
335
-
321
+ var values = (List <?>) actualMap .get (keyInActual );
322
+ var targetClass = (values .get (0 ) instanceof Map ) ? null : values .get (0 ).getClass ();
336
323
var value = parseKeyValue (keyWithoutPrefix (valueEntry .getKey ()), targetClass , objectMapper );
337
324
valueList .add (value );
338
325
}
339
326
}
340
327
341
328
public static Object parseKeyValue (String stringValue , Class <?> targetClass ,
342
329
KubernetesSerialization objectMapper ) {
343
- stringValue = stringValue .trim ();
344
- if (targetClass != null ) {
345
- return objectMapper .unmarshal (stringValue , targetClass );
346
- } else {
347
- return objectMapper .unmarshal (stringValue , Map .class );
348
- }
330
+ var type = Objects .requireNonNullElse (targetClass , Map .class );
331
+ return objectMapper .unmarshal (stringValue .trim (), type );
349
332
}
350
333
351
334
private static boolean isSetValueField (Set <Map .Entry <String , Object >> managedEntrySet ) {
@@ -372,30 +355,29 @@ private static boolean isKeyPrefixedSkippingDotKey(Set<Map.Entry<String, Object>
372
355
}
373
356
374
357
@ SuppressWarnings ("unchecked" )
375
- private static java . util . Map .Entry <Integer , Map <String , Object >> selectListEntryBasedOnKey (
358
+ private static Map .Entry <Integer , Map <String , Object >> selectListEntryBasedOnKey (
376
359
String key ,
377
360
List <Map <String , Object >> values , KubernetesSerialization objectMapper ) {
378
361
Map <String , Object > ids = objectMapper .unmarshal (key , Map .class );
379
- List < Map < String , Object >> possibleTargets = new ArrayList <>(1 );
380
- int index = -1 ;
362
+ var possibleTargets = new ArrayList <Map < String , Object > >(1 );
363
+ int lastIndex = -1 ;
381
364
for (int i = 0 ; i < values .size (); i ++) {
382
- var v = values .get (i );
383
- if (v .entrySet ().containsAll (ids .entrySet ())) {
384
- possibleTargets .add (v );
385
- index = i ;
365
+ var value = values .get (i );
366
+ if (value .entrySet ().containsAll (ids .entrySet ())) {
367
+ possibleTargets .add (value );
368
+ lastIndex = i ;
386
369
}
387
370
}
388
371
if (possibleTargets .isEmpty ()) {
389
- throw new IllegalStateException ("Cannot find list element for key:" + key + ", in map: "
372
+ throw new IllegalStateException ("Cannot find list element for key: " + key + " in map: "
390
373
+ values .stream ().map (Map ::keySet ).toList ());
391
374
}
392
375
if (possibleTargets .size () > 1 ) {
393
376
throw new IllegalStateException (
394
- "More targets found in list element for key:" + key + ", in map: "
377
+ "More targets found in list element for key: " + key + " in map: "
395
378
+ values .stream ().map (Map ::keySet ).toList ());
396
379
}
397
- final var finalIndex = index ;
398
- return new AbstractMap .SimpleEntry <>(finalIndex , possibleTargets .get (0 ));
380
+ return new AbstractMap .SimpleEntry <>(lastIndex , possibleTargets .get (0 ));
399
381
}
400
382
401
383
private Optional <ManagedFieldsEntry > checkIfFieldManagerExists (R actual , String fieldManager ) {
@@ -407,21 +389,21 @@ private Optional<ManagedFieldsEntry> checkIfFieldManagerExists(R actual, String
407
389
f -> f .getManager ().equals (fieldManager ) && f .getOperation ().equals (APPLY_OPERATION ))
408
390
.toList ();
409
391
if (targetManagedFields .isEmpty ()) {
410
- log .debug ("No field manager exists for resource {} with name: {} and operation Apply " ,
392
+ log .debug ("No field manager exists for resource: {} with name: {} and operation {} " ,
411
393
actual .getKind (),
412
- actual .getMetadata ().getName ());
394
+ actual .getMetadata ().getName (),
395
+ APPLY_OPERATION );
413
396
return Optional .empty ();
414
397
}
415
398
// this should not happen in theory
416
399
if (targetManagedFields .size () > 1 ) {
417
400
throw new OperatorException ("More than one field manager exists with name: " + fieldManager
418
- + "in resource: " + actual .getKind () + " with name: " + actual .getMetadata ().getName ());
401
+ + " in resource: " + actual .getKind () + " with name: " + actual .getMetadata ().getName ());
419
402
}
420
403
return Optional .of (targetManagedFields .get (0 ));
421
404
}
422
405
423
406
private static String keyWithoutPrefix (String key ) {
424
407
return key .substring (2 );
425
408
}
426
-
427
409
}
0 commit comments