Skip to content

Commit 12a80ff

Browse files
authored
feat(patch): Patch of entity filters volatile metadata fields like UID which are not relevant for Diffs (#925)
Fields like "metadata.uid" and resourceversion are ever-changing. Those fields should be filtered during patch to reduce API load.
1 parent ff35411 commit 12a80ff

File tree

3 files changed

+102
-9
lines changed

3 files changed

+102
-9
lines changed

src/KubeOps.Abstractions/Entities/JsonPatchExtensions.cs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,54 @@ namespace KubeOps.Abstractions.Entities;
1818
/// </summary>
1919
public static class JsonPatchExtensions
2020
{
21+
/// <summary>
22+
/// <para>
23+
/// Ignored properties that should not be included (by default) in the JSON Patch diff.
24+
/// This mainly concerns metadata properties that are not relevant for the diff,
25+
/// like the UID of the object and resource version.
26+
/// </para>
27+
/// <para>Currently, contains the following properties:</para>
28+
/// <list type="bullet">
29+
/// <item><term>/metadata/creationTimestamp</term></item>
30+
/// <item><term>/metadata/deletionGracePeriodSeconds</term></item>
31+
/// <item><term>/metadata/deletionTimestamp</term></item>
32+
/// <item><term>/metadata/generation</term></item>
33+
/// <item><term>/metadata/managedFields</term></item>
34+
/// <item><term>/metadata/resourceVersion</term></item>
35+
/// <item><term>/metadata/selfLink</term></item>
36+
/// <item><term>/metadata/uid</term></item>
37+
/// </list>
38+
/// </summary>
39+
public static readonly string[] DefaultIgnoredProperties =
40+
[
41+
"/metadata/creationTimestamp",
42+
"/metadata/deletionGracePeriodSeconds",
43+
"/metadata/deletionTimestamp",
44+
"/metadata/generation",
45+
"/metadata/managedFields",
46+
"/metadata/resourceVersion",
47+
"/metadata/selfLink",
48+
"/metadata/uid",
49+
];
50+
51+
/// <summary>
52+
/// Default operations filter that filters out operations that are listed in <see cref="DefaultIgnoredProperties"/>.
53+
/// This filters out most properties in the metadata section of the entity that are not relevant for diffing.
54+
/// Currently, this filters out the following properties (<see cref="DefaultIgnoredProperties"/>):
55+
/// <list type="bullet">
56+
/// <item><term>/metadata/creationTimestamp</term></item>
57+
/// <item><term>/metadata/deletionGracePeriodSeconds</term></item>
58+
/// <item><term>/metadata/deletionTimestamp</term></item>
59+
/// <item><term>/metadata/generation</term></item>
60+
/// <item><term>/metadata/managedFields</term></item>
61+
/// <item><term>/metadata/resourceVersion</term></item>
62+
/// <item><term>/metadata/selfLink</term></item>
63+
/// <item><term>/metadata/uid</term></item>
64+
/// </list>
65+
/// </summary>
66+
public static readonly Func<IReadOnlyList<PatchOperation>, IReadOnlyList<PatchOperation>> DefaultOperationsFilter =
67+
operations => operations.Where(o => !DefaultIgnoredProperties.Contains(o.Path.ToString())).ToList();
68+
2169
/// <summary>
2270
/// Convert a <see cref="IKubernetesObject{TMetadata}"/> into a <see cref="JsonNode"/>.
2371
/// </summary>
@@ -30,21 +78,37 @@ public static class JsonPatchExtensions
3078
/// Computes the JSON Patch diff between two Kubernetes entities implementing <see cref="IKubernetesObject{V1ObjectMeta}"/>.
3179
/// This method serializes both entities to JSON and calculates the difference as a JSON Patch document.
3280
/// </summary>
81+
/// <typeparam name="TEntity">The entity type.</typeparam>
3382
/// <param name="from">The source entity to compare from.</param>
3483
/// <param name="to">The target entity to compare to.</param>
84+
/// <param name="operationsFilter">An optional filter action that filters the <see cref="PatchOperation"/>s that are contained in the <see cref="JsonPatch"/>.</param>
3585
/// <returns>A <see cref="JsonNode"/> representing the JSON Patch diff between the two entities.</returns>
3686
/// <exception cref="InvalidOperationException">Thrown if the diff could not be created.</exception>
37-
public static JsonPatch CreateJsonPatch(
38-
this IKubernetesObject<V1ObjectMeta> from,
39-
IKubernetesObject<V1ObjectMeta> to)
87+
public static JsonPatch CreateJsonPatch<TEntity>(
88+
this TEntity from,
89+
TEntity to,
90+
Func<IReadOnlyList<PatchOperation>, IReadOnlyList<PatchOperation>>? operationsFilter = null)
91+
where TEntity : IKubernetesObject<V1ObjectMeta>
4092
{
4193
var fromNode = from.ToNode();
4294
var toNode = to.ToNode();
4395
var patch = fromNode.CreatePatch(toNode);
4496

45-
return patch;
97+
return new JsonPatch((operationsFilter ?? DefaultOperationsFilter).Invoke(patch.Operations));
4698
}
4799

100+
/// <summary>
101+
/// Checks if two Kubernetes entities implementing <see cref="IKubernetesObject{V1ObjectMeta}"/> have changes.
102+
/// </summary>
103+
/// <typeparam name="TEntity">The entity type.</typeparam>
104+
/// <param name="from">Original object.</param>
105+
/// <param name="to">Changed object.</param>
106+
/// <returns>True if there are changes detected. Otherwise false.</returns>
107+
public static bool HasChanges<TEntity>(
108+
this TEntity from,
109+
TEntity to)
110+
where TEntity : IKubernetesObject<V1ObjectMeta> => from.CreateJsonPatch(to).Operations.Count > 0;
111+
48112
/// <summary>
49113
/// Create a <see cref="V1Patch"/> out of a <see cref="JsonPatch"/>.
50114
/// This can be used to apply the patch to a Kubernetes entity using the Kubernetes client.

src/KubeOps.KubernetesClient/IKubernetesClient.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,19 @@ TEntity UpdateStatus<TEntity>(TEntity entity)
329329
/// <summary>
330330
/// Patch a given entity on the Kubernetes API by calculating the diff between the current entity and the provided entity.
331331
/// This method fetches the current entity from the API, computes the patch, and applies it.
332+
/// The patch does return the same object if there were no changes detected.
333+
/// If no operationsFilter is provided, the default filter (<see cref="JsonPatchExtensions.DefaultOperationsFilter"/> is applied.
332334
/// </summary>
333335
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
334336
/// <param name="entity">The entity containing the desired updates.</param>
337+
/// <param name="operationsFilter">The filter that is applied to the <see cref="PatchOperation"/>s in the <see cref="JsonPatch"/> to determine if changes are present.</param>
335338
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
336339
/// <returns>The patched entity.</returns>
337340
/// <exception cref="InvalidOperationException">Thrown if the entity to be patched does not exist on the API.</exception>
338-
Task<TEntity> PatchAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default)
341+
Task<TEntity> PatchAsync<TEntity>(
342+
TEntity entity,
343+
Func<IReadOnlyList<PatchOperation>, IReadOnlyList<PatchOperation>>? operationsFilter = null,
344+
CancellationToken cancellationToken = default)
339345
where TEntity : IKubernetesObject<V1ObjectMeta>
340346
{
341347
var currentEntity = Get<TEntity>(entity.Name(), entity.Namespace());
@@ -347,21 +353,35 @@ Task<TEntity> PatchAsync<TEntity>(TEntity entity, CancellationToken cancellation
347353

348354
return PatchAsync(
349355
currentEntity,
350-
entity.WithResourceVersion(currentEntity.ResourceVersion()),
356+
entity,
357+
operationsFilter,
351358
cancellationToken);
352359
}
353360

354361
/// <summary>
355362
/// Patch a given entity on the Kubernetes API by calculating the diff between two provided entities.
363+
/// Returns the patched entity if changes were detected, otherwise returns the original entity.
364+
/// Detection of changes is done by creating a <see cref="JsonPatch"/> object
365+
/// and then applying the operationsFilter. Defaults to the <see cref="JsonPatchExtensions.DefaultOperationsFilter"/>.
356366
/// </summary>
357367
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
358368
/// <param name="from">The current/original entity.</param>
359369
/// <param name="to">The updated entity with desired changes.</param>
370+
/// <param name="operationsFilter">The filter that is applied to the <see cref="PatchOperation"/>s in the <see cref="JsonPatch"/> to determine if changes are present.</param>
360371
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
361372
/// <returns>The patched entity.</returns>
362-
Task<TEntity> PatchAsync<TEntity>(TEntity from, TEntity to, CancellationToken cancellationToken = default)
363-
where TEntity : IKubernetesObject<V1ObjectMeta> =>
364-
PatchAsync(from, from.CreateJsonPatch(to), cancellationToken);
373+
Task<TEntity> PatchAsync<TEntity>(
374+
TEntity from,
375+
TEntity to,
376+
Func<IReadOnlyList<PatchOperation>, IReadOnlyList<PatchOperation>>? operationsFilter = null,
377+
CancellationToken cancellationToken = default)
378+
where TEntity : IKubernetesObject<V1ObjectMeta>
379+
{
380+
var patch = from.CreateJsonPatch(to, operationsFilter);
381+
return patch.Operations.Count == 0
382+
? Task.FromResult(from)
383+
: PatchAsync(from, from.CreateJsonPatch(to), cancellationToken);
384+
}
365385

366386
/// <summary>
367387
/// Patch a given entity on the Kubernetes API using a <see cref="JsonPatch"/> object.

test/KubeOps.Abstractions.Test/Entities/JsonPatchExtensions.Test.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,13 @@ public void GetJsonDiff_Removes_Object_From_Containers_List()
188188
diff.ToJsonString().Should().Contain("/spec/template/spec/containers/1");
189189
diff.ToJsonString().Should().Contain("remove");
190190
}
191+
192+
[Fact]
193+
public void GetJsonDiff_Filters_Metadata_Fields()
194+
{
195+
var from = new V1ConfigMap { Metadata = new V1ObjectMeta { Name = "test", ResourceVersion = "1" } };
196+
var to = new V1ConfigMap { Metadata = new V1ObjectMeta { Name = "test", ResourceVersion = "2" } };
197+
var diff = from.CreateJsonPatch(to);
198+
diff.Operations.Should().HaveCount(0);
199+
}
191200
}

0 commit comments

Comments
 (0)