1
1
package io .javaoperatorsdk .operator .processing .dependent .kubernetes ;
2
2
3
- import java .util .Objects ;
3
+ import java .util .* ;
4
4
5
+ import io .fabric8 .kubernetes .api .model .ConfigMap ;
5
6
import io .fabric8 .kubernetes .api .model .HasMetadata ;
7
+ import io .fabric8 .kubernetes .api .model .Secret ;
8
+ import io .fabric8 .zjsonpatch .JsonDiff ;
9
+ import io .javaoperatorsdk .operator .api .config .ConfigurationServiceProvider ;
6
10
import io .javaoperatorsdk .operator .api .reconciler .Context ;
7
11
import io .javaoperatorsdk .operator .processing .dependent .Matcher ;
8
12
import io .javaoperatorsdk .operator .processing .dependent .kubernetes .processors .GenericResourceUpdatePreProcessor ;
9
13
14
+ import com .fasterxml .jackson .databind .JsonNode ;
15
+ import com .fasterxml .jackson .databind .ObjectMapper ;
16
+
10
17
public class GenericKubernetesResourceMatcher <R extends HasMetadata , P extends HasMetadata >
11
18
implements Matcher <R , P > {
12
19
20
+
21
+ private static final String ADD = "add" ;
22
+ private static final String OP = "op" ;
23
+ private static final String METADATA_LABELS = "/metadata/labels" ;
24
+ private static final String METADATA_ANNOTATIONS = "/metadata/annotations" ;
25
+
26
+ private static final String PATH = "path" ;
27
+ private static final String [] EMPTY_ARRAY = {};
13
28
private final KubernetesDependentResource <R , P > dependentResource ;
14
29
15
30
private GenericKubernetesResourceMatcher (KubernetesDependentResource <R , P > dependentResource ) {
16
31
this .dependentResource = dependentResource ;
17
32
}
18
33
19
- @ SuppressWarnings ({"unchecked" , "rawtypes" })
34
+ @ SuppressWarnings ({"unchecked" , "rawtypes" , "unused" })
20
35
static <R extends HasMetadata , P extends HasMetadata > Matcher <R , P > matcherFor (
21
36
KubernetesDependentResource <R , P > dependentResource ) {
22
37
return new GenericKubernetesResourceMatcher (dependentResource );
23
38
}
24
39
40
+ /**
41
+ * {@inheritDoc}
42
+ * <p/>
43
+ * This implementation attempts to cover most common cases out of the box by considering
44
+ * non-additive changes to the resource's spec (if the resource in question has a {@code spec}
45
+ * field), making special provisions for {@link ConfigMap} and {@link Secret} resources. Additive
46
+ * changes (i.e. a field is added that previously didn't exist) are not considered as triggering a
47
+ * mismatch by default to account for validating webhooks that might add default values
48
+ * automatically when not present or some other controller adding labels and/or annotations.
49
+ * <p/>
50
+ * It should be noted that this implementation is potentially intensive because it generically
51
+ * attempts to cover common use cases by performing diffs on the JSON representation of objects.
52
+ * If performance is a concern, it might be easier / simpler to provide a {@link Matcher}
53
+ * implementation optimized for your use case.
54
+ */
25
55
@ Override
26
56
public Result <R > match (R actualResource , P primary , Context <P > context ) {
27
57
var desired = dependentResource .desired (primary , context );
28
- return match (desired , actualResource , false , false );
29
- }
30
-
31
- public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
32
- boolean considerMetadata ) {
33
- return match (desired , actualResource , considerMetadata , false );
58
+ return match (desired , actualResource , false , false , false );
34
59
}
35
60
36
61
/**
@@ -39,10 +64,13 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
39
64
*
40
65
* @param desired the desired resource
41
66
* @param actualResource the actual resource
42
- * @param considerMetadata {@code true} if labels and annotations will be checked for equality,
43
- * {@code false} otherwise (meaning that metadata changes will be ignored for matching
44
- * purposes)
45
- * @param equality if {@code false}, the algorithm checks if the properties in the desired
67
+ * @param considerLabelsAndAnnotations {@code true} if labels and annotations will be checked for
68
+ * equality, {@code false} otherwise (meaning that metadata changes will be ignored for
69
+ * matching purposes)
70
+ * @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
71
+ * and desired state if false, additional elements are allowed in actual annotations.
72
+ * Considered only if considerLabelsAndAnnotations is true.
73
+ * @param specEquality if {@code false}, the algorithm checks if the properties in the desired
46
74
* resource spec are same as in the actual resource spec. The reason is that admission
47
75
* controllers and default Kubernetes controllers might add default values to some
48
76
* properties which are not set in the desired resources' spec and comparing it with simple
@@ -53,61 +81,191 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
53
81
* all properties and values are equal. This could be implemented also by overriding equals
54
82
* method of spec, should be done as an optimization - this implementation does not require
55
83
* that.
56
- *
57
- * @return results of matching
58
84
* @param <R> resource
85
+ * @return results of matching
59
86
*/
60
- @ SuppressWarnings ("unchecked" )
61
87
public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
62
- boolean considerMetadata , boolean equality ) {
63
- if (considerMetadata ) {
64
- final var desiredMetadata = desired .getMetadata ();
65
- final var actualMetadata = actualResource .getMetadata ();
66
- final var matched =
67
- Objects .equals (desiredMetadata .getAnnotations (), actualMetadata .getAnnotations ()) &&
68
- Objects .equals (desiredMetadata .getLabels (), actualMetadata .getLabels ());
69
- if (!matched ) {
70
- return Result .computed (false , desired );
71
- }
72
- }
88
+ boolean considerLabelsAndAnnotations , boolean labelsAndAnnotationsEquality ,
89
+ boolean specEquality ) {
90
+ return match (desired , actualResource , considerLabelsAndAnnotations ,
91
+ labelsAndAnnotationsEquality , specEquality , EMPTY_ARRAY );
92
+ }
73
93
74
- final ResourceUpdatePreProcessor <R > processor =
75
- GenericResourceUpdatePreProcessor .processorFor ((Class <R >) desired .getClass ());
76
- final var matched = processor .matches (actualResource , desired , equality );
77
- return Result .computed (matched , desired );
94
+ /**
95
+ * Determines whether the specified actual resource matches the specified desired resource,
96
+ * possibly considering metadata and deeper equality checks.
97
+ *
98
+ * @param desired the desired resource
99
+ * @param actualResource the actual resource
100
+ * @param considerLabelsAndAnnotations {@code true} if labels and annotations will be checked for
101
+ * equality, {@code false} otherwise (meaning that metadata changes will be ignored for
102
+ * matching purposes)
103
+ * @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
104
+ * and desired state if false, additional elements are allowed in actual annotations.
105
+ * Considered only if considerLabelsAndAnnotations is true.
106
+ * @param ignorePaths are paths in the resource that are ignored on matching (basically an ignore
107
+ * list). All changes with a target prefix path on a calculated JSON Patch between actual
108
+ * and desired will be ignored. If there are other changes, non-present on ignore list
109
+ * match fails.
110
+ * @param <R> resource
111
+ * @return results of matching
112
+ */
113
+ public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
114
+ boolean considerLabelsAndAnnotations , boolean labelsAndAnnotationsEquality ,
115
+ String ... ignorePaths ) {
116
+ return match (desired , actualResource , considerLabelsAndAnnotations ,
117
+ labelsAndAnnotationsEquality , false , ignorePaths );
78
118
}
79
119
80
120
/**
81
121
* Determines whether the specified actual resource matches the desired state defined by the
82
122
* specified {@link KubernetesDependentResource} based on the observed state of the associated
83
123
* specified primary resource.
84
124
*
85
- * @param dependentResource the {@link KubernetesDependentResource} implementation used to
86
- * computed the desired state associated with the specified primary resource
125
+ * @param dependentResource the {@link KubernetesDependentResource} implementation used to compute
126
+ * the desired state associated with the specified primary resource
87
127
* @param actualResource the observed dependent resource for which we want to determine whether it
88
128
* matches the desired state or not
89
129
* @param primary the primary resource from which we want to compute the desired state
90
130
* @param context the {@link Context} instance within which this method is called
91
- * @param considerMetadata {@code true} to consider the metadata of the actual resource when
92
- * determining if it matches the desired state, {@code false} if matching should occur only
93
- * considering the spec of the resources
94
- * @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
131
+ * @param considerLabelsAndAnnotations {@code true} to consider the metadata of the actual
132
+ * resource when determining if it matches the desired state, {@code false} if matching
133
+ * should occur only considering the spec of the resources
134
+ * @param labelsAndAnnotationsEquality if true labels and annotation match exactly in the actual
135
+ * and desired state if false, additional elements are allowed in actual annotations.
136
+ * Considered only if considerLabelsAndAnnotations is true.
95
137
* @param <R> the type of resource we want to determine whether they match or not
96
138
* @param <P> the type of primary resources associated with the secondary resources we want to
97
139
* match
98
- * @param strongEquality if the resource should match exactly
140
+ * @param ignorePaths are paths in the resource that are ignored on matching (basically an ignore
141
+ * list). All changes with a target prefix path on a calculated JSON Patch between actual
142
+ * and desired will be ignored. If there are other changes, non-present on ignore list
143
+ * match fails.
144
+ * @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
99
145
*/
100
146
public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
101
147
KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
102
- Context <P > context , boolean considerMetadata , boolean strongEquality ) {
148
+ Context <P > context , boolean considerLabelsAndAnnotations ,
149
+ boolean labelsAndAnnotationsEquality ,
150
+ String ... ignorePaths ) {
151
+ final var desired = dependentResource .desired (primary , context );
152
+ return match (desired , actualResource , considerLabelsAndAnnotations ,
153
+ labelsAndAnnotationsEquality , ignorePaths );
154
+ }
155
+
156
+ public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
157
+ KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
158
+ Context <P > context , boolean considerLabelsAndAnnotations ,
159
+ boolean labelsAndAnnotationsEquality ,
160
+ boolean specEquality ) {
161
+ final var desired = dependentResource .desired (primary , context );
162
+ return match (desired , actualResource , considerLabelsAndAnnotations ,
163
+ labelsAndAnnotationsEquality , specEquality );
164
+ }
165
+
166
+ @ SuppressWarnings ("unchecked" )
167
+ public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
168
+ boolean considerMetadata , boolean labelsAndAnnotationsEquality , boolean specEquality ,
169
+ String ... ignoredPaths ) {
170
+ final List <String > ignoreList =
171
+ ignoredPaths != null && ignoredPaths .length > 0 ? Arrays .asList (ignoredPaths )
172
+ : Collections .emptyList ();
173
+
174
+ if (specEquality && !ignoreList .isEmpty ()) {
175
+ throw new IllegalArgumentException (
176
+ "Equality should be false in case of ignore list provided" );
177
+ }
178
+ final var objectMapper = ConfigurationServiceProvider .instance ().getObjectMapper ();
179
+
180
+ if (considerMetadata ) {
181
+ Optional <Result <R >> res =
182
+ matchMetadata (desired , actualResource , labelsAndAnnotationsEquality , objectMapper );
183
+ if (res .isPresent ()) {
184
+ return res .orElseThrow ();
185
+ }
186
+ }
187
+
188
+ final ResourceUpdatePreProcessor <R > processor =
189
+ GenericResourceUpdatePreProcessor .processorFor ((Class <R >) desired .getClass ());
190
+ final var matched = processor .matches (actualResource , desired , specEquality , ignoredPaths );
191
+ return Result .computed (matched , desired );
192
+ }
193
+
194
+ private static <R extends HasMetadata > Optional <Result <R >> matchMetadata (R desired ,
195
+ R actualResource ,
196
+ boolean labelsAndAnnotationsEquality ,
197
+ ObjectMapper objectMapper ) {
198
+
199
+ if (labelsAndAnnotationsEquality ) {
200
+ final var desiredMetadata = desired .getMetadata ();
201
+ final var actualMetadata = actualResource .getMetadata ();
202
+
203
+ final var matched =
204
+ Objects .equals (desiredMetadata .getAnnotations (), actualMetadata .getAnnotations ()) &&
205
+ Objects .equals (desiredMetadata .getLabels (), actualMetadata .getLabels ());
206
+ if (!matched ) {
207
+ return Optional .of (Result .computed (false , desired ));
208
+ }
209
+ } else {
210
+ var desiredNode = objectMapper .valueToTree (desired );
211
+ var actualNode = objectMapper .valueToTree (actualResource );
212
+ var wholeDiffJsonPatch = JsonDiff .asJson (desiredNode , actualNode );
213
+ var metadataJSonDiffs = getDiffsImpactingPathsWithPrefixes (wholeDiffJsonPatch ,
214
+ METADATA_LABELS ,
215
+ METADATA_ANNOTATIONS );
216
+ if (!allDiffsAreAddOps (metadataJSonDiffs )) {
217
+ return Optional .of (Result .computed (false , desired ));
218
+ }
219
+ }
220
+ return Optional .empty ();
221
+ }
222
+
223
+ static boolean nodeIsChildOf (JsonNode n , List <String > prefixes ) {
224
+ var path = getPath (n );
225
+ return prefixes .stream ().anyMatch (path ::startsWith );
226
+ }
227
+
228
+ static String getPath (JsonNode n ) {
229
+ return n .get (PATH ).asText ();
230
+ }
231
+
232
+ static boolean allDiffsAreAddOps (List <JsonNode > metadataJSonDiffs ) {
233
+ if (metadataJSonDiffs .isEmpty ()) {
234
+ return true ;
235
+ }
236
+ return metadataJSonDiffs .stream ().allMatch (n -> ADD .equals (n .get (OP ).asText ()));
237
+ }
238
+
239
+ public static List <JsonNode > getDiffsImpactingPathsWithPrefixes (JsonNode diffJsonPatch ,
240
+ String ... prefixes ) {
241
+ if (prefixes != null && prefixes .length > 0 ) {
242
+ var res = new ArrayList <JsonNode >();
243
+ var prefixList = Arrays .asList (prefixes );
244
+ for (int i = 0 ; i < diffJsonPatch .size (); i ++) {
245
+ var node = diffJsonPatch .get (i );
246
+ if (nodeIsChildOf (node , prefixList )) {
247
+ res .add (node );
248
+ }
249
+ }
250
+ return res ;
251
+ }
252
+ return Collections .emptyList ();
253
+ }
254
+
255
+ @ Deprecated (forRemoval = true )
256
+ public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
257
+ KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
258
+ Context <P > context , boolean considerLabelsAndAnnotations , boolean specEquality ) {
103
259
final var desired = dependentResource .desired (primary , context );
104
- return match (desired , actualResource , considerMetadata , strongEquality );
260
+ return match (desired , actualResource , considerLabelsAndAnnotations , specEquality );
105
261
}
106
262
263
+ @ Deprecated (forRemoval = true )
107
264
public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
108
265
KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
109
- Context <P > context , boolean considerMetadata ) {
266
+ Context <P > context , boolean considerLabelsAndAnnotations , String ... ignorePaths ) {
110
267
final var desired = dependentResource .desired (primary , context );
111
- return match (desired , actualResource , considerMetadata , false );
268
+ return match (desired , actualResource , considerLabelsAndAnnotations , true , ignorePaths );
112
269
}
270
+
113
271
}
0 commit comments