Skip to content

Commit 144eacd

Browse files
committed
wip
1 parent 616e29a commit 144eacd

File tree

8 files changed

+387
-307
lines changed

8 files changed

+387
-307
lines changed

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -450,45 +450,4 @@ public static <P extends HasMetadata> P addFinalizerWithSSA(
450450
e);
451451
}
452452
}
453-
454-
public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) {
455-
return compareResourceVersions(
456-
h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion());
457-
}
458-
459-
public static int compareResourceVersions(String v1, String v2) {
460-
int v1Length = validateResourceVersion(v1);
461-
int v2Length = validateResourceVersion(v2);
462-
int comparison = v1Length - v2Length;
463-
if (comparison != 0) {
464-
return comparison;
465-
}
466-
for (int i = 0; i < v2Length; i++) {
467-
int comp = v1.charAt(i) - v2.charAt(i);
468-
if (comp != 0) {
469-
return comp;
470-
}
471-
}
472-
return 0;
473-
}
474-
475-
private static int validateResourceVersion(String v1) {
476-
int v1Length = v1.length();
477-
if (v1Length == 0) {
478-
throw new NonComparableResourceVersionException("Resource version is empty");
479-
}
480-
for (int i = 0; i < v1Length; i++) {
481-
char char1 = v1.charAt(i);
482-
if (char1 == '0') {
483-
if (i == 0) {
484-
throw new NonComparableResourceVersionException(
485-
"Resource version cannot begin with 0: " + v1);
486-
}
487-
} else if (char1 < '0' || char1 > '9') {
488-
throw new NonComparableResourceVersionException(
489-
"Non numeric characters in resource version: " + v1);
490-
}
491-
}
492-
return v1Length;
493-
}
494453
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java

Lines changed: 248 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,36 @@
1515
*/
1616
package io.javaoperatorsdk.operator.api.reconciler;
1717

18+
import java.lang.reflect.InvocationTargetException;
19+
import java.util.function.Predicate;
1820
import java.util.function.UnaryOperator;
1921

22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
2025
import io.fabric8.kubernetes.api.model.HasMetadata;
26+
import io.fabric8.kubernetes.api.model.ObjectMeta;
27+
import io.fabric8.kubernetes.client.KubernetesClient;
28+
import io.fabric8.kubernetes.client.KubernetesClientException;
2129
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
2230
import io.fabric8.kubernetes.client.dsl.base.PatchType;
31+
import io.javaoperatorsdk.operator.OperatorException;
32+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
2333
import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource;
2434

35+
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
36+
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion;
37+
2538
public class ReconcileUtils {
2639

40+
private static final Logger log = LoggerFactory.getLogger(ReconcileUtils.class);
41+
42+
public static final int DEFAULT_MAX_RETRY = 10;
43+
2744
private ReconcileUtils() {}
2845

2946
// todo move finalizers mtehods & deprecate
30-
// todo namespace handling
47+
// todo test namespace handling
3148
// todo compare resource version if multiple event sources provide the same resource
3249
// todo for json patch make sense to retry for ?
3350

@@ -322,4 +339,234 @@ public static <R extends HasMetadata> R resourcePatch(
322339
? (R) ies.eventFilteringUpdateAndCacheResource(resource, updateOperation)
323340
: (R) ies.updateAndCacheResource(resource, updateOperation);
324341
}
342+
343+
public static <P extends HasMetadata> P addFinalizer(Context<P> context) {
344+
return addFinalizer(context, context.getControllerConfiguration().getFinalizerName());
345+
}
346+
347+
/**
348+
* Adds finalizer to the primary resource from the context using JSON Patch. Retries conflicts and
349+
* unprocessable content (HTTP 422), see {@link
350+
* PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator,
351+
* Predicate)} for details on retry. It does not add finalizer if there is already a finalizer or
352+
* resource is marked for deletion.
353+
*
354+
* @return updated resource from the server response
355+
*/
356+
public static <P extends HasMetadata> P addFinalizer(Context<P> context, String finalizer) {
357+
return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer);
358+
}
359+
360+
/**
361+
* Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content
362+
* (HTTP 422), see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient,
363+
* HasMetadata, UnaryOperator, Predicate)} for details on retry. It does not try to add finalizer
364+
* if there is already a finalizer or resource is marked for deletion.
365+
*
366+
* @return updated resource from the server response
367+
*/
368+
public static <P extends HasMetadata> P addFinalizer(
369+
KubernetesClient client, P resource, String finalizerName) {
370+
if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) {
371+
return resource;
372+
}
373+
return conflictRetryingPatch(
374+
client,
375+
resource,
376+
r -> {
377+
r.addFinalizer(finalizerName);
378+
return r;
379+
},
380+
r -> !r.hasFinalizer(finalizerName));
381+
}
382+
383+
public static <P extends HasMetadata> P removeFinalizer(Context<P> context) {
384+
return removeFinalizer(context, context.getControllerConfiguration().getFinalizerName());
385+
}
386+
387+
/**
388+
* Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and
389+
* handles retries, see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient,
390+
* HasMetadata, UnaryOperator, Predicate)} for details. It does not try to remove finalizer if
391+
* finalizer is not present on the resource.
392+
*
393+
* @return updated resource from the server response
394+
*/
395+
public static <P extends HasMetadata> P removeFinalizer(
396+
Context<P> context, String finalizerName) {
397+
return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName);
398+
}
399+
400+
/**
401+
* Removes the target finalizer from target resource. Uses JSON Patch and handles retries, see
402+
* {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata,
403+
* UnaryOperator, Predicate)} for details. It does not try to remove finalizer if finalizer is not
404+
* present on the resource.
405+
*
406+
* @return updated resource from the server response
407+
*/
408+
public static <P extends HasMetadata> P removeFinalizer(
409+
KubernetesClient client, P resource, String finalizerName) {
410+
if (!resource.hasFinalizer(finalizerName)) {
411+
return resource;
412+
}
413+
return conflictRetryingPatch(
414+
client,
415+
resource,
416+
r -> {
417+
r.removeFinalizer(finalizerName);
418+
return r;
419+
},
420+
r -> r.hasFinalizer(finalizerName));
421+
}
422+
423+
/**
424+
* Patches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or
425+
* unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in
426+
* {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}.
427+
*
428+
* @param client KubernetesClient
429+
* @param resource to update
430+
* @param resourceChangesOperator changes to be done on the resource before update
431+
* @param preCondition condition to check if the patch operation still needs to be performed or
432+
* not.
433+
* @return updated resource from the server or unchanged if the precondition does not hold.
434+
* @param <P> resource type
435+
*/
436+
@SuppressWarnings("unchecked")
437+
public static <P extends HasMetadata> P conflictRetryingPatch(
438+
KubernetesClient client,
439+
P resource,
440+
UnaryOperator<P> resourceChangesOperator,
441+
Predicate<P> preCondition) {
442+
if (log.isDebugEnabled()) {
443+
log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource));
444+
}
445+
int retryIndex = 0;
446+
while (true) {
447+
try {
448+
if (!preCondition.test(resource)) {
449+
return resource;
450+
}
451+
return client.resource(resource).edit(resourceChangesOperator);
452+
} catch (KubernetesClientException e) {
453+
log.trace("Exception during patch for resource: {}", resource);
454+
retryIndex++;
455+
// only retry on conflict (409) and unprocessable content (422) which
456+
// can happen if JSON Patch is not a valid request since there was
457+
// a concurrent request which already removed another finalizer:
458+
// List element removal from a list is by index in JSON Patch
459+
// so if addressing a second finalizer but first is meanwhile removed
460+
// it is a wrong request.
461+
if (e.getCode() != 409 && e.getCode() != 422) {
462+
throw e;
463+
}
464+
if (retryIndex >= DEFAULT_MAX_RETRY) {
465+
throw new OperatorException(
466+
"Exceeded maximum ("
467+
+ DEFAULT_MAX_RETRY
468+
+ ") retry attempts to patch resource: "
469+
+ ResourceID.fromResource(resource));
470+
}
471+
log.debug(
472+
"Retrying patch for resource name: {}, namespace: {}; HTTP code: {}",
473+
resource.getMetadata().getName(),
474+
resource.getMetadata().getNamespace(),
475+
e.getCode());
476+
var operation = client.resources(resource.getClass());
477+
if (resource.getMetadata().getNamespace() != null) {
478+
resource =
479+
(P)
480+
operation
481+
.inNamespace(resource.getMetadata().getNamespace())
482+
.withName(resource.getMetadata().getName())
483+
.get();
484+
} else {
485+
resource = (P) operation.withName(resource.getMetadata().getName()).get();
486+
}
487+
}
488+
}
489+
}
490+
491+
public static <P extends HasMetadata> P addFinalizerWithSSA(Context<P> context) {
492+
return addFinalizerWithSSA(context, context.getControllerConfiguration().getFinalizerName());
493+
}
494+
495+
/**
496+
* Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of
497+
* the target resource, setting only name, namespace and finalizer. Does not use optimistic
498+
* locking for the patch.
499+
*
500+
* @return the patched resource from the server response
501+
*/
502+
public static <P extends HasMetadata> P addFinalizerWithSSA(
503+
Context<P> context, String finalizerName) {
504+
var originalResource = context.getPrimaryResource();
505+
if (log.isDebugEnabled()) {
506+
log.debug(
507+
"Adding finalizer (using SSA) for resource: {} version: {}",
508+
getUID(originalResource),
509+
getVersion(originalResource));
510+
}
511+
try {
512+
P resource = (P) originalResource.getClass().getConstructor().newInstance();
513+
ObjectMeta objectMeta = new ObjectMeta();
514+
objectMeta.setName(originalResource.getMetadata().getName());
515+
objectMeta.setNamespace(originalResource.getMetadata().getNamespace());
516+
resource.setMetadata(objectMeta);
517+
resource.addFinalizer(finalizerName);
518+
519+
return serverSideApplyPrimary(context, resource);
520+
} catch (InstantiationException
521+
| IllegalAccessException
522+
| InvocationTargetException
523+
| NoSuchMethodException e) {
524+
throw new RuntimeException(
525+
"Issue with creating custom resource instance with reflection."
526+
+ " Custom Resources must provide a no-arg constructor. Class: "
527+
+ originalResource.getClass().getName(),
528+
e);
529+
}
530+
}
531+
532+
public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) {
533+
return compareResourceVersions(
534+
h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion());
535+
}
536+
537+
public static int compareResourceVersions(String v1, String v2) {
538+
int v1Length = validateResourceVersion(v1);
539+
int v2Length = validateResourceVersion(v2);
540+
int comparison = v1Length - v2Length;
541+
if (comparison != 0) {
542+
return comparison;
543+
}
544+
for (int i = 0; i < v2Length; i++) {
545+
int comp = v1.charAt(i) - v2.charAt(i);
546+
if (comp != 0) {
547+
return comp;
548+
}
549+
}
550+
return 0;
551+
}
552+
553+
private static int validateResourceVersion(String v1) {
554+
int v1Length = v1.length();
555+
if (v1Length == 0) {
556+
throw new NonComparableResourceVersionException("Resource version is empty");
557+
}
558+
for (int i = 0; i < v1Length; i++) {
559+
char char1 = v1.charAt(i);
560+
if (char1 == '0') {
561+
if (i == 0) {
562+
throw new NonComparableResourceVersionException(
563+
"Resource version cannot begin with 0: " + v1);
564+
}
565+
} else if (char1 < '0' || char1 > '9') {
566+
throw new NonComparableResourceVersionException(
567+
"Non numeric characters in resource version: " + v1);
568+
}
569+
}
570+
return v1Length;
571+
}
325572
}

0 commit comments

Comments
 (0)