1
1
package io .javaoperatorsdk .operator .processing .dependent .kubernetes ;
2
2
3
- import java .util .Objects ;
3
+ import java .util .* ;
4
4
5
5
import io .fabric8 .kubernetes .api .model .ConfigMap ;
6
6
import io .fabric8 .kubernetes .api .model .HasMetadata ;
7
7
import io .fabric8 .kubernetes .api .model .Secret ;
8
8
import io .fabric8 .zjsonpatch .JsonDiff ;
9
- import io .javaoperatorsdk .operator .ReconcilerUtils ;
10
9
import io .javaoperatorsdk .operator .api .config .ConfigurationServiceProvider ;
11
10
import io .javaoperatorsdk .operator .api .reconciler .Context ;
12
11
import io .javaoperatorsdk .operator .processing .dependent .Matcher ;
13
12
13
+ import com .fasterxml .jackson .databind .JsonNode ;
14
+
14
15
public class GenericKubernetesResourceMatcher <R extends HasMetadata , P extends HasMetadata >
15
16
implements Matcher <R , P > {
16
17
@@ -63,17 +64,50 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
63
64
*/
64
65
public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
65
66
boolean considerMetadata , boolean equality ) {
67
+ return match (desired , actualResource , considerMetadata , equality , Collections .emptyList ());
68
+ }
69
+
70
+ public static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
71
+ boolean considerMetadata , String ... ignoreList ) {
72
+ return match (desired , actualResource , considerMetadata , false , Arrays .asList (ignoreList ));
73
+ }
74
+
75
+
76
+ private static <R extends HasMetadata > Result <R > match (R desired , R actualResource ,
77
+ boolean considerMetadata , boolean equality , List <String > ignoreList ) {
78
+ if (equality && !ignoreList .isEmpty ()) {
79
+ throw new IllegalArgumentException (
80
+ "Equality should be false in case of ignore list provided" );
81
+ }
82
+
83
+ final var objectMapper = ConfigurationServiceProvider .instance ().getObjectMapper ();
84
+
85
+ var desiredNode = objectMapper .valueToTree (desired );
86
+ var actualNode = objectMapper .valueToTree (actualResource );
87
+ var wholeDiffJsonPatch = JsonDiff .asJson (desiredNode , actualNode );
88
+
89
+ var considerIgnoreList = !equality && !ignoreList .isEmpty ();
90
+
66
91
if (considerMetadata ) {
67
- final var desiredMetadata = desired .getMetadata ();
68
- final var actualMetadata = actualResource .getMetadata ();
69
- final var matched =
70
- Objects .equals (desiredMetadata .getAnnotations (), actualMetadata .getAnnotations ()) &&
71
- Objects .equals (desiredMetadata .getLabels (), actualMetadata .getLabels ());
72
- if (!matched ) {
73
- return Result .computed (false , desired );
92
+ if (equality ) {
93
+ final var desiredMetadata = desired .getMetadata ();
94
+ final var actualMetadata = actualResource .getMetadata ();
95
+
96
+ final var matched =
97
+ Objects .equals (desiredMetadata .getAnnotations (), actualMetadata .getAnnotations ()) &&
98
+ Objects .equals (desiredMetadata .getLabels (), actualMetadata .getLabels ());
99
+ if (!matched ) {
100
+ return Result .computed (false , desired );
101
+ }
102
+ } else {
103
+ var metadataJSonDiffs = getDiffsWithPathSuffix (wholeDiffJsonPatch ,
104
+ "/metadata/labels" ,
105
+ "/metadata/annotations" );
106
+ if (!allDiffsAreAddOps (metadataJSonDiffs )) {
107
+ return Result .computed (false , desired );
108
+ }
74
109
}
75
110
}
76
-
77
111
if (desired instanceof ConfigMap ) {
78
112
return Result .computed (
79
113
ResourceComparators .compareConfigMapData ((ConfigMap ) desired , (ConfigMap ) actualResource ),
@@ -83,29 +117,60 @@ public static <R extends HasMetadata> Result<R> match(R desired, R actualResourc
83
117
ResourceComparators .compareSecretData ((Secret ) desired , (Secret ) actualResource ),
84
118
desired );
85
119
} else {
86
- final var objectMapper = ConfigurationServiceProvider .instance ().getObjectMapper ();
87
-
88
120
// reflection will be replaced by this:
89
121
// https://github.com/fabric8io/kubernetes-client/issues/3816
90
- var desiredSpecNode = objectMapper .valueToTree (ReconcilerUtils .getSpec (desired ));
91
- var actualSpecNode = objectMapper .valueToTree (ReconcilerUtils .getSpec (actualResource ));
92
- var diffJsonPatch = JsonDiff .asJson (desiredSpecNode , actualSpecNode );
122
+ var specDiffJsonPatch = getDiffsWithPathSuffix (wholeDiffJsonPatch , "/spec" );
93
123
// In case of equality is set to true, no diffs are allowed, so we return early if diffs exist
94
124
// On contrary (if equality is false), "add" is allowed for cases when for some
95
125
// resources Kubernetes fills-in values into spec.
96
- if (equality && diffJsonPatch . size () > 0 ) {
126
+ if (equality && ! specDiffJsonPatch . isEmpty () ) {
97
127
return Result .computed (false , desired );
98
128
}
99
- for (int i = 0 ; i < diffJsonPatch .size (); i ++) {
100
- String operation = diffJsonPatch .get (i ).get ("op" ).asText ();
101
- if (!operation .equals ("add" )) {
129
+ if (considerIgnoreList ) {
130
+ if (!allDiffsOnIgnoreList (specDiffJsonPatch , ignoreList )) {
131
+ return Result .computed (false , desired );
132
+ }
133
+ } else {
134
+ if (!allDiffsAreAddOps (specDiffJsonPatch )) {
102
135
return Result .computed (false , desired );
103
136
}
104
137
}
105
138
return Result .computed (true , desired );
106
139
}
107
140
}
108
141
142
+ private static boolean allDiffsAreAddOps (List <JsonNode > metadataJSonDiffs ) {
143
+ if (metadataJSonDiffs .isEmpty ()) {
144
+ return true ;
145
+ }
146
+ return metadataJSonDiffs .stream ().allMatch (n -> "add" .equals (n .get ("op" ).asText ()));
147
+ }
148
+
149
+ private static boolean allDiffsOnIgnoreList (List <JsonNode > metadataJSonDiffs ,
150
+ List <String > ignoreList ) {
151
+ if (metadataJSonDiffs .isEmpty ()) {
152
+ return true ;
153
+ }
154
+ return metadataJSonDiffs .stream ().allMatch (n -> {
155
+ var path = n .get ("path" ).asText ();
156
+ return ignoreList .stream ().anyMatch (path ::startsWith );
157
+ });
158
+ }
159
+
160
+ private static List <JsonNode > getDiffsWithPathSuffix (JsonNode diffJsonPatch ,
161
+ String ... ignorePaths ) {
162
+ var res = new ArrayList <JsonNode >();
163
+ var prefixList = Arrays .asList (ignorePaths );
164
+ for (int i = 0 ; i < diffJsonPatch .size (); i ++) {
165
+ var node = diffJsonPatch .get (i );
166
+ String path = diffJsonPatch .get (i ).get ("path" ).asText ();
167
+ if (prefixList .stream ().anyMatch (path ::startsWith )) {
168
+ res .add (node );
169
+ }
170
+ }
171
+ return res ;
172
+ }
173
+
109
174
/**
110
175
* Determines whether the specified actual resource matches the desired state defined by the
111
176
* specified {@link KubernetesDependentResource} based on the observed state of the associated
@@ -133,6 +198,34 @@ public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
133
198
return match (desired , actualResource , considerMetadata , strongEquality );
134
199
}
135
200
201
+ /**
202
+ * Determines whether the specified actual resource matches the desired state defined by the
203
+ * specified {@link KubernetesDependentResource} based on the observed state of the associated
204
+ * specified primary resource.
205
+ *
206
+ * @param dependentResource the {@link KubernetesDependentResource} implementation used to compute
207
+ * the desired state associated with the specified primary resource
208
+ * @param actualResource the observed dependent resource for which we want to determine whether it
209
+ * matches the desired state or not
210
+ * @param primary the primary resource from which we want to compute the desired state
211
+ * @param context the {@link Context} instance within which this method is called
212
+ * @param considerMetadata {@code true} to consider the metadata of the actual resource when
213
+ * determining if it matches the desired state, {@code false} if matching should occur only
214
+ * considering the spec of the resources
215
+ * @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
216
+ * @param <R> the type of resource we want to determine whether they match or not
217
+ * @param <P> the type of primary resources associated with the secondary resources we want to
218
+ * match
219
+ * @param ignorePaths are paths in the resource that are ignored on matching. Anny related change
220
+ * on a calculated JSON Patch between actual and desired will be ignored.
221
+ */
222
+ public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
223
+ KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
224
+ Context <P > context , boolean considerMetadata , String ... ignorePaths ) {
225
+ final var desired = dependentResource .desired (primary , context );
226
+ return match (desired , actualResource , considerMetadata , ignorePaths );
227
+ }
228
+
136
229
public static <R extends HasMetadata , P extends HasMetadata > Result <R > match (
137
230
KubernetesDependentResource <R , P > dependentResource , R actualResource , P primary ,
138
231
Context <P > context , boolean considerMetadata ) {
0 commit comments