44import java .util .ArrayList ;
55import java .util .Collections ;
66import java .util .HashMap ;
7+ import java .util .Iterator ;
78import java .util .LinkedHashMap ;
89import java .util .List ;
910import java .util .Map ;
2728import io .javaoperatorsdk .operator .OperatorException ;
2829import io .javaoperatorsdk .operator .api .reconciler .Context ;
2930import io .javaoperatorsdk .operator .processing .LoggingUtils ;
31+ import io .javaoperatorsdk .operator .processing .event .ResourceID ;
3032
3133import com .github .difflib .DiffUtils ;
3234import com .github .difflib .UnifiedDiffUtils ;
@@ -60,7 +62,13 @@ public class SSABasedGenericKubernetesResourceMatcher<R extends HasMetadata> {
6062 new SSABasedGenericKubernetesResourceMatcher <>();
6163
6264 private static final List <String > IGNORED_METADATA =
63- List .of ("creationTimestamp" , "deletionTimestamp" , "generation" , "selfLink" , "uid" );
65+ List .of (
66+ "creationTimestamp" ,
67+ "deletionTimestamp" ,
68+ "generation" ,
69+ "selfLink" ,
70+ "uid" ,
71+ "resourceVersion" );
6472
6573 @ SuppressWarnings ("unchecked" )
6674 public static <L extends HasMetadata > SSABasedGenericKubernetesResourceMatcher <L > getInstance () {
@@ -79,16 +87,134 @@ public static <L extends HasMetadata> SSABasedGenericKubernetesResourceMatcher<L
7987 private static final Logger log =
8088 LoggerFactory .getLogger (SSABasedGenericKubernetesResourceMatcher .class );
8189
82- @ SuppressWarnings ("unchecked" )
90+ record SSAState (Map <String , Object > desired , Map <String , Object > actual ) {}
91+
92+ record CacheKey (ResourceID id , int hash ) {}
93+
94+ private LinkedHashMap <CacheKey , SSAState > defaultsAndNormalizations =
95+ new LinkedHashMap <CacheKey , SSAState >();
96+
97+ public void findDefaultsAndNormalizations (R actual , R desired , Context <?> context ) {
98+ SSAState state = getSSAState (actual , desired , context );
99+
100+ if (state == null ) {
101+ // exception?
102+ }
103+
104+ // minimize by removing eveything that appears in both
105+ minimizeMaps (state .desired , state .actual );
106+
107+ if (!state .desired .isEmpty () || !state .actual .isEmpty ()) {
108+ defaultsAndNormalizations .put (
109+ new CacheKey (ResourceID .fromResource (actual ), desired .hashCode ()), state );
110+ }
111+ }
112+
113+ void minimizeMaps (Map <String , Object > actual , Map <String , Object > desired ) {
114+ for (Iterator <Map .Entry <String , Object >> iter = desired .entrySet ().iterator ();
115+ iter .hasNext (); ) {
116+ var entry = iter .next ();
117+ var desiredValue = entry .getValue ();
118+ var actualValue = actual .get (entry .getKey ());
119+ if (Objects .equals (desiredValue , actualValue )) {
120+ iter .remove ();
121+ actual .remove (entry .getKey ());
122+ } else if (desiredValue instanceof Map dm ) {
123+ if (actualValue instanceof Map am ) {
124+ minimizeMaps (am , dm );
125+ if (am .isEmpty ()) {
126+ actual .remove (entry .getKey ());
127+ }
128+ }
129+ if (dm .isEmpty ()) {
130+ iter .remove ();
131+ }
132+ } else if (desiredValue instanceof List dl ) {
133+ if (actualValue instanceof List al ) {
134+ minimizeLists (al , dl );
135+ if ((dl .isEmpty () || dl .stream ().allMatch (Objects ::isNull ))
136+ && (al .isEmpty () || al .stream ().allMatch (Objects ::isNull ))) {
137+ iter .remove ();
138+ actual .remove (entry .getKey ());
139+ }
140+ }
141+ }
142+ }
143+ }
144+
145+ void minimizeLists (List <Object > actual , List <Object > desired ) {
146+ for (int i = 0 ; i < desired .size (); i ++) {
147+ Object desiredValue = desired .get (i );
148+ if (actual .size () > i ) {
149+ Object actualValue = actual .get (i );
150+ if (Objects .equals (desiredValue , actualValue )) {
151+ actual .set (i , null );
152+ desired .set (i , null );
153+ } else if (desiredValue instanceof Map dm ) {
154+ if (actualValue instanceof Map am ) {
155+ minimizeMaps (am , dm );
156+ if (am .isEmpty ()) {
157+ actual .set (i , null );
158+ }
159+ }
160+ if (dm .isEmpty ()) {
161+ desired .set (i , null );
162+ }
163+ } else if (desiredValue instanceof List dl ) {
164+ if (actualValue instanceof List al ) {
165+ minimizeLists (al , dl );
166+ if ((dl .isEmpty () || dl .stream ().allMatch (Objects ::isNull ))
167+ && (al .isEmpty () || al .stream ().allMatch (Objects ::isNull ))) {
168+ actual .set (i , null );
169+ desired .set (i , null );
170+ }
171+ }
172+ }
173+ }
174+ }
175+ }
176+
83177 public boolean matches (R actual , R desired , Context <?> context ) {
178+ SSAState state = getSSAState (actual , desired , context );
179+
180+ if (state == null ) {
181+ return false ;
182+ }
183+
184+ SSAState overrides = defaultsAndNormalizations .get (new CacheKey (ResourceID .fromResource (actual ), desired .hashCode ()));
185+ if (overrides != null ) {
186+ // if containsAll(overrides.desired, state.desired) && containsAll(overrides.actual, state.actual)
187+
188+ //
189+ }
190+
191+ var matches = matches (state .actual (), state .desired (), actual , desired , context );
192+ if (!matches && log .isDebugEnabled () && LoggingUtils .isNotSensitiveResource (desired )) {
193+ var diff =
194+ getDiff (
195+ state .actual (), state .desired (), context .getClient ().getKubernetesSerialization ());
196+ log .debug (
197+ "Diff between actual and desired state for resource: {} with name: {} in namespace: {}"
198+ + " is:\n "
199+ + "{}" ,
200+ actual .getKind (),
201+ actual .getMetadata ().getName (),
202+ actual .getMetadata ().getNamespace (),
203+ diff );
204+ }
205+ return matches ;
206+ }
207+
208+ @ SuppressWarnings ("unchecked" )
209+ private SSAState getSSAState (R actual , R desired , Context <?> context ) {
84210 var optionalManagedFieldsEntry =
85211 checkIfFieldManagerExists (actual , context .getControllerConfiguration ().fieldManager ());
86212 // If no field is managed by our controller, that means the controller hasn't touched the
87213 // resource yet and the resource probably doesn't match the desired state. Not matching here
88214 // means that the resource will need to be updated and since this will be done using SSA, the
89215 // fields our controller cares about will become managed by it
90216 if (optionalManagedFieldsEntry .isEmpty ()) {
91- return false ;
217+ return null ;
92218 }
93219
94220 var managedFieldsEntry = optionalManagedFieldsEntry .orElseThrow ();
@@ -109,20 +235,8 @@ public boolean matches(R actual, R desired, Context<?> context) {
109235 objectMapper );
110236
111237 removeIrrelevantValues (desiredMap );
112-
113- var matches = matches (prunedActual , desiredMap , actual , desired , context );
114- if (!matches && log .isDebugEnabled () && LoggingUtils .isNotSensitiveResource (desired )) {
115- var diff = getDiff (prunedActual , desiredMap , objectMapper );
116- log .debug (
117- "Diff between actual and desired state for resource: {} with name: {} in namespace: {}"
118- + " is:\n "
119- + "{}" ,
120- actual .getKind (),
121- actual .getMetadata ().getName (),
122- actual .getMetadata ().getNamespace (),
123- diff );
124- }
125- return matches ;
238+ removeIrrelevantValues (actualMap );
239+ return new SSAState (desiredMap , prunedActual );
126240 }
127241
128242 /**
0 commit comments