diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
deleted file mode 100644
index c818a304..00000000
--- a/.config/dotnet-tools.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "version": 1,
- "isRoot": true,
- "tools": {
- "docfx": {
- "version": "2.78.3",
- "commands": ["docfx"]
- }
- }
-}
diff --git a/KubeOps.sln b/KubeOps.sln
index 2731c267..c8ea81f9 100644
--- a/KubeOps.sln
+++ b/KubeOps.sln
@@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebhookOperator", "examples
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.Templates", "src\KubeOps.Templates\KubeOps.Templates.csproj", "{26237038-7172-4D01-B5E1-2A5E3F6B369E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.Abstractions.Test", "test\KubeOps.Abstractions.Test\KubeOps.Abstractions.Test.csproj", "{4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -85,6 +87,7 @@ Global
{7F7744B2-CF3F-4309-9C2D-037278017D49} = {C587731F-8191-4A19-8662-B89A60FE79A1}
{0BFE2297-9537-49BE-8B1F-431A8ACD654D} = {DC760E69-D0EA-417F-AE38-B12D0B04DE39}
{26237038-7172-4D01-B5E1-2A5E3F6B369E} = {4DB01062-6DC5-4028-BB72-C0619C2F5F2E}
+ {4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C} = {C587731F-8191-4A19-8662-B89A60FE79A1}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E9A0B04E-D90E-4B94-90E0-DD3666B098FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -155,5 +158,9 @@ Global
{26237038-7172-4D01-B5E1-2A5E3F6B369E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26237038-7172-4D01-B5E1-2A5E3F6B369E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26237038-7172-4D01-B5E1-2A5E3F6B369E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E9EEFD6-DA44-41F2-AF02-8A7F2150AC0C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/src/KubeOps.Abstractions/Entities/JsonPatchExtensions.cs b/src/KubeOps.Abstractions/Entities/JsonPatchExtensions.cs
new file mode 100644
index 00000000..5f0025c2
--- /dev/null
+++ b/src/KubeOps.Abstractions/Entities/JsonPatchExtensions.cs
@@ -0,0 +1,67 @@
+using System.Text;
+using System.Text.Json.Nodes;
+
+using Json.More;
+using Json.Patch;
+
+using k8s;
+using k8s.Models;
+
+namespace KubeOps.Abstractions.Entities;
+
+///
+/// Method extensions for JSON diffing between two entities ().
+///
+public static class JsonPatchExtensions
+{
+ ///
+ /// Convert a into a .
+ ///
+ /// The entity to convert.
+ /// Either the json node, or null if it failed.
+ public static JsonNode? ToNode(this IKubernetesObject entity) =>
+ JsonNode.Parse(KubernetesJson.Serialize(entity));
+
+ ///
+ /// Computes the JSON Patch diff between two Kubernetes entities implementing .
+ /// This method serializes both entities to JSON and calculates the difference as a JSON Patch document.
+ ///
+ /// The source entity to compare from.
+ /// The target entity to compare to.
+ /// A representing the JSON Patch diff between the two entities.
+ /// Thrown if the diff could not be created.
+ public static JsonPatch CreateJsonPatch(
+ this IKubernetesObject from,
+ IKubernetesObject to)
+ {
+ var fromNode = from.ToNode();
+ var toNode = to.ToNode();
+ var patch = fromNode.CreatePatch(toNode);
+
+ return patch;
+ }
+
+ ///
+ /// Create a out of a .
+ /// This can be used to apply the patch to a Kubernetes entity using the Kubernetes client.
+ ///
+ /// The patch that should be converted.
+ /// A that may be applied to Kubernetes objects.
+ public static V1Patch ToKubernetesPatch(this JsonPatch patch) =>
+ new(patch.ToJsonString(), V1Patch.PatchType.JsonPatch);
+
+ ///
+ /// Create the unformatted JSON string representation of a .
+ ///
+ /// The to convert.
+ /// A string that represents the unformatted JSON representation of the patch.
+ public static string ToJsonString(this JsonPatch patch) => patch.ToJsonDocument().RootElement.GetRawText();
+
+ ///
+ /// Create the base 64 representation of a .
+ ///
+ /// The patch to convert.
+ /// The base64 encoded representation of the patch.
+ public static string ToBase64String(this JsonPatch patch) =>
+ Convert.ToBase64String(Encoding.UTF8.GetBytes(patch.ToJsonString()));
+}
diff --git a/src/KubeOps.Abstractions/Entities/Extensions.cs b/src/KubeOps.Abstractions/Entities/KubernetesExtensions.cs
similarity index 95%
rename from src/KubeOps.Abstractions/Entities/Extensions.cs
rename to src/KubeOps.Abstractions/Entities/KubernetesExtensions.cs
index cd69d1c8..bb61dee9 100644
--- a/src/KubeOps.Abstractions/Entities/Extensions.cs
+++ b/src/KubeOps.Abstractions/Entities/KubernetesExtensions.cs
@@ -4,9 +4,10 @@
namespace KubeOps.Abstractions.Entities;
///
-/// Method extensions for .
+/// Basic extensions for .
+/// Extensions that target the Kubernetes Object and its metadata.
///
-public static class Extensions
+public static class KubernetesExtensions
{
///
/// Sets the resource version of the specified Kubernetes object to the specified value.
diff --git a/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj b/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj
index 93e54942..80c92d49 100644
--- a/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj
+++ b/src/KubeOps.Abstractions/KubeOps.Abstractions.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/KubeOps.KubernetesClient/IKubernetesClient.cs b/src/KubeOps.KubernetesClient/IKubernetesClient.cs
index 0be236dd..ae923d18 100644
--- a/src/KubeOps.KubernetesClient/IKubernetesClient.cs
+++ b/src/KubeOps.KubernetesClient/IKubernetesClient.cs
@@ -1,4 +1,6 @@
-using k8s;
+using Json.Patch;
+
+using k8s;
using k8s.Models;
using KubeOps.Abstractions.Entities;
@@ -320,6 +322,142 @@ Task UpdateStatusAsync(TEntity entity, CancellationToken cance
TEntity UpdateStatus(TEntity entity)
where TEntity : IKubernetesObject;
+ ///
+ /// Patch a given entity on the Kubernetes API by calculating the diff between the current entity and the provided entity.
+ /// This method fetches the current entity from the API, computes the patch, and applies it.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity containing the desired updates.
+ /// Cancellation token to monitor for cancellation requests.
+ /// The patched entity.
+ /// Thrown if the entity to be patched does not exist on the API.
+ Task PatchAsync(TEntity entity, CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject
+ {
+ var currentEntity = Get(entity.Name(), entity.Namespace());
+ if (currentEntity is null)
+ {
+ throw new InvalidOperationException(
+ $"Cannot patch entity {typeof(TEntity).Name} with name {entity.Name()} in namespace {entity.Namespace()}: Entity does not exist.");
+ }
+
+ return PatchAsync(
+ currentEntity,
+ entity.WithResourceVersion(currentEntity.ResourceVersion()),
+ cancellationToken);
+ }
+
+ ///
+ /// Patch a given entity on the Kubernetes API by calculating the diff between two provided entities.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The current/original entity.
+ /// The updated entity with desired changes.
+ /// Cancellation token to monitor for cancellation requests.
+ /// The patched entity.
+ Task PatchAsync(TEntity from, TEntity to, CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject =>
+ PatchAsync(from, from.CreateJsonPatch(to), cancellationToken);
+
+ ///
+ /// Patch a given entity on the Kubernetes API using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity to patch.
+ /// The representing the changes to apply.
+ /// Cancellation token to monitor for cancellation requests.
+ /// The patched entity.
+ Task PatchAsync(TEntity entity, JsonPatch patch, CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject =>
+ PatchAsync(entity, patch.ToKubernetesPatch(), cancellationToken);
+
+ ///
+ /// Patch a given entity on the Kubernetes API using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity to patch.
+ /// The representing the changes to apply.
+ /// Cancellation token to monitor for cancellation requests.
+ /// The patched entity.
+ Task PatchAsync(TEntity entity, V1Patch patch, CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject =>
+ PatchAsync(patch, entity.Name(), entity.Namespace(), cancellationToken);
+
+ ///
+ /// Patch a given entity on the Kubernetes API by name and namespace using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The representing the changes to apply.
+ /// The name of the entity to patch.
+ /// The namespace of the entity to patch (if applicable).
+ /// Cancellation token to monitor for cancellation requests.
+ /// The patched entity.
+ Task PatchAsync(
+ V1Patch patch,
+ string name,
+ string? @namespace = null,
+ CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject;
+
+ ///
+ /// Patch a given entity on the Kubernetes API by calculating the diff between the current entity and the provided entity.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity containing the desired updates.
+ /// The patched entity.
+ /// Thrown if the entity to be patched does not exist on the API.
+ TEntity Patch(TEntity entity)
+ where TEntity : IKubernetesObject
+ => PatchAsync(entity).GetAwaiter().GetResult();
+
+ ///
+ /// Patch a given entity on the Kubernetes API by calculating the diff between two provided entities.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The current/original entity.
+ /// The updated entity with desired changes.
+ /// The patched entity.
+ TEntity Patch(TEntity from, TEntity to)
+ where TEntity : IKubernetesObject
+ => PatchAsync(from, to).GetAwaiter().GetResult();
+
+ ///
+ /// Patch a given entity on the Kubernetes API using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity to patch.
+ /// The representing the changes to apply.
+ /// The patched entity.
+ TEntity Patch(TEntity entity, JsonPatch patch)
+ where TEntity : IKubernetesObject
+ => PatchAsync(entity, patch).GetAwaiter().GetResult();
+
+ ///
+ /// Patch a given entity on the Kubernetes API using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The entity to patch.
+ /// The representing the changes to apply.
+ /// The patched entity.
+ TEntity Patch(TEntity entity, V1Patch patch)
+ where TEntity : IKubernetesObject
+ => PatchAsync(entity, patch).GetAwaiter().GetResult();
+
+ ///
+ /// Patch a given entity on the Kubernetes API by name and namespace using a object.
+ ///
+ /// The type of the Kubernetes entity.
+ /// The representing the changes to apply.
+ /// The name of the entity to patch.
+ /// The namespace of the entity to patch (if applicable).
+ /// The patched entity.
+ TEntity Patch(
+ V1Patch patch,
+ string name,
+ string? @namespace = null)
+ where TEntity : IKubernetesObject
+ => PatchAsync(patch, name, @namespace).GetAwaiter().GetResult();
+
///
/// A task that completes when the call was made.
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default)
diff --git a/src/KubeOps.KubernetesClient/KubernetesClient.cs b/src/KubeOps.KubernetesClient/KubernetesClient.cs
index ac2bbb4f..b15d1bf1 100644
--- a/src/KubeOps.KubernetesClient/KubernetesClient.cs
+++ b/src/KubeOps.KubernetesClient/KubernetesClient.cs
@@ -309,6 +309,24 @@ public TEntity UpdateStatus(TEntity entity)
};
}
+ ///
+ public async Task PatchAsync(
+ V1Patch patch,
+ string name,
+ string? @namespace = null,
+ CancellationToken cancellationToken = default)
+ where TEntity : IKubernetesObject
+ {
+ ThrowIfDisposed();
+
+ using var client = CreateGenericClient();
+ return await (@namespace switch
+ {
+ not null => client.PatchNamespacedAsync(patch, @namespace, name, cancellationToken),
+ null => client.PatchAsync(patch, name, cancellationToken),
+ });
+ }
+
///
public async Task DeleteAsync(
string name,
diff --git a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
index 1d77f27c..20c02e22 100644
--- a/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
+++ b/src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
@@ -27,7 +27,6 @@
-
diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/JsonDiffer.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/JsonDiffer.cs
deleted file mode 100644
index 283066bb..00000000
--- a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/JsonDiffer.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Text;
-using System.Text.Json.JsonDiffPatch;
-using System.Text.Json.JsonDiffPatch.Diffs.Formatters;
-using System.Text.Json.Nodes;
-
-using k8s;
-
-namespace KubeOps.Operator.Web.Webhooks.Admission.Mutation;
-
-internal static class JsonDiffer
-{
- public static string Base64Diff(this JsonNode from, object? to)
- {
- var formatter = new JsonPatchDeltaFormatter();
-
- var toToken = GetNode(to);
- var patch = from.Diff(toToken, formatter)!;
-
- return Convert.ToBase64String(Encoding.UTF8.GetBytes(patch.ToString()));
- }
-
- public static JsonNode? GetNode(object? o)
- {
- var json = KubernetesJson.Serialize(o);
- return JsonNode.Parse(json);
- }
-}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs
index eb48944d..267c0ff1 100644
--- a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationResult.cs
@@ -1,8 +1,12 @@
using System.Text.Json.Nodes;
+using Json.Patch;
+
using k8s;
using k8s.Models;
+using KubeOps.Abstractions.Entities;
+
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -70,7 +74,9 @@ await response.WriteAsJsonAsync(
Status = Status,
Warnings = Warnings.ToArray(),
PatchType = ModifiedObject is null ? null : JsonPatch,
- Patch = ModifiedObject is null ? null : OriginalObject!.Base64Diff(ModifiedObject),
+ Patch = ModifiedObject is null
+ ? null
+ : OriginalObject!.CreatePatch(ModifiedObject.ToNode()).ToBase64String(),
},
});
}
diff --git a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationWebhook{TEntity}.cs b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationWebhook{TEntity}.cs
index c17c08f8..4f1eaa01 100644
--- a/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationWebhook{TEntity}.cs
+++ b/src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/MutationWebhook{TEntity}.cs
@@ -1,6 +1,8 @@
using k8s;
using k8s.Models;
+using KubeOps.Abstractions.Entities;
+
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -104,11 +106,11 @@ public async Task Mutate(
[FromBody] AdmissionRequest request,
CancellationToken cancellationToken)
{
- var original = JsonDiffer.GetNode(request.Request.Operation switch
+ var original = request.Request.Operation switch
{
- CreateOperation or UpdateOperation => request.Request.Object!,
- _ => request.Request.OldObject!,
- });
+ CreateOperation or UpdateOperation => request.Request.Object!.ToNode(),
+ _ => request.Request.OldObject!.ToNode(),
+ };
var result = request.Request.Operation switch
{
diff --git a/src/KubeOps.Operator/KubeOps.Operator.csproj b/src/KubeOps.Operator/KubeOps.Operator.csproj
index a120b9ba..5361c6e6 100644
--- a/src/KubeOps.Operator/KubeOps.Operator.csproj
+++ b/src/KubeOps.Operator/KubeOps.Operator.csproj
@@ -17,7 +17,6 @@
-
diff --git a/test/KubeOps.Abstractions.Test/Entities/JsonPatchExtensions.Test.cs b/test/KubeOps.Abstractions.Test/Entities/JsonPatchExtensions.Test.cs
new file mode 100644
index 00000000..8cc3fbca
--- /dev/null
+++ b/test/KubeOps.Abstractions.Test/Entities/JsonPatchExtensions.Test.cs
@@ -0,0 +1,187 @@
+using FluentAssertions;
+
+using k8s.Models;
+
+using KubeOps.Abstractions.Entities;
+
+namespace KubeOps.Abstractions.Test.Entities;
+
+public class JsonPatchExtensionsTest
+{
+ [Fact]
+ public void GetJsonDiff_Adds_Property_In_Spec()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 1 },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 1, RevisionHistoryLimit = 2 },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should()
+ .Contain("/spec/revisionHistoryLimit")
+ .And.Contain("2")
+ .And.Contain("add");
+ }
+
+ [Fact]
+ public void GetJsonDiff_Updates_Property_In_Spec()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 1 },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 2 },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should()
+ .Contain("replace")
+ .And.Contain("/spec/replicas")
+ .And.Contain("2");
+ }
+
+ [Fact]
+ public void GetJsonDiff_Removes_Property_In_Spec()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 1, RevisionHistoryLimit = 2 },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec { Replicas = 1 },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should().Contain("/spec/revisionHistoryLimit").And.Contain("remove");
+ }
+
+ [Fact]
+ public void GetJsonDiff_Adds_Object_To_Containers_List()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec { Containers = new List() },
+ },
+ },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec
+ {
+ Containers = new List
+ {
+ new V1Container { Name = "nginx", Image = "nginx:latest" },
+ },
+ },
+ },
+ },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should().Contain("/spec/template/spec/containers/0");
+ diff.ToJsonString().Should().Contain("nginx:latest");
+ }
+
+ [Fact]
+ public void GetJsonDiff_Updates_Object_In_Containers_List()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec
+ {
+ Containers = new List
+ {
+ new V1Container { Name = "nginx", Image = "nginx:1.14" },
+ },
+ },
+ },
+ },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec
+ {
+ Containers = new List
+ {
+ new V1Container { Name = "nginx", Image = "nginx:1.16" },
+ },
+ },
+ },
+ },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should().Contain("replace").And.Contain("/spec/template/spec/containers/0/image");
+ }
+
+ [Fact]
+ public void GetJsonDiff_Removes_Object_From_Containers_List()
+ {
+ var from = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec
+ {
+ Containers = new List
+ {
+ new V1Container { Name = "nginx", Image = "nginx:latest" },
+ new V1Container { Name = "nginx2", Image = "nginx:latest" },
+ },
+ },
+ },
+ },
+ };
+ var to = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta { Name = "test" },
+ Spec = new V1DeploymentSpec
+ {
+ Template = new V1PodTemplateSpec
+ {
+ Spec = new V1PodSpec
+ {
+ Containers = new List
+ {
+ new V1Container { Name = "nginx", Image = "nginx:latest" },
+ },
+ },
+ },
+ },
+ };
+ var diff = from.CreateJsonPatch(to);
+ diff.ToJsonString().Should().Contain("/spec/template/spec/containers/1");
+ diff.ToJsonString().Should().Contain("remove");
+ }
+}
diff --git a/test/KubeOps.Abstractions.Test/KubeOps.Abstractions.Test.csproj b/test/KubeOps.Abstractions.Test/KubeOps.Abstractions.Test.csproj
new file mode 100644
index 00000000..7bc34335
--- /dev/null
+++ b/test/KubeOps.Abstractions.Test/KubeOps.Abstractions.Test.csproj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs
index 3adfdfa8..bdedf2ac 100644
--- a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs
+++ b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs
@@ -163,6 +163,43 @@ public void Should_Not_Throw_On_Not_Found_Delete()
_client.Delete(config);
}
+ [Fact]
+ public void Should_Patch_ConfigMap_Sync()
+ {
+ // Add
+ var config = _client.Create(
+ new V1ConfigMap
+ {
+ Kind = V1ConfigMap.KubeKind,
+ ApiVersion = V1ConfigMap.KubeApiVersion,
+ Metadata = new(name: RandomName(), namespaceProperty: "default"),
+ Data = new Dictionary { { "foo", "bar" } },
+ });
+ _objects.Add(config);
+
+ // Add a new key using Patch(config)
+ config.Data["hello"] = "world";
+ config = _client.Patch(config);
+ config.Data.Should().ContainKey("hello").And.ContainValue("world");
+
+ // Replace a value using Patch(from, to)
+ var from = config;
+ var to = new V1ConfigMap
+ {
+ Kind = V1ConfigMap.KubeKind,
+ ApiVersion = V1ConfigMap.KubeApiVersion,
+ Metadata = from.Metadata,
+ Data = new Dictionary { { "foo", "baz" }, { "hello", "world" } },
+ };
+ config = _client.Patch(from, to);
+ config.Data["foo"].Should().Be("baz");
+
+ // Remove a key using Patch(config)
+ config.Data.Remove("hello");
+ config = _client.Patch(config);
+ config.Data.Should().NotContainKey("hello");
+ }
+
public void Dispose()
{
_client.Delete(_objects);
diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs
index 2c6440eb..0a2a0d00 100644
--- a/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs
+++ b/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs
@@ -163,6 +163,69 @@ public async Task Should_Not_Throw_On_Not_Found_Delete()
await _client.DeleteAsync(config);
}
+ [Fact]
+ public async Task Should_Patch_ConfigMap_Async()
+ {
+ // Add
+ var config = await _client.CreateAsync(
+ new V1ConfigMap
+ {
+ Kind = V1ConfigMap.KubeKind,
+ ApiVersion = V1ConfigMap.KubeApiVersion,
+ Metadata = new(name: RandomName(), namespaceProperty: "default"),
+ Data = new Dictionary { { "foo", "bar" } },
+ });
+ _objects.Add(config);
+
+ // Add a new key using PatchAsync(config)
+ config.Data["hello"] = "world";
+ config = await _client.PatchAsync(config);
+ config.Data.Should().ContainKey("hello").And.ContainValue("world");
+
+ // Replace a value using PatchAsync(from, to)
+ var from = config;
+ var to = new V1ConfigMap
+ {
+ Kind = V1ConfigMap.KubeKind,
+ ApiVersion = V1ConfigMap.KubeApiVersion,
+ Metadata = from.Metadata,
+ Data = new Dictionary { { "foo", "baz" }, { "hello", "world" } },
+ };
+ config = await _client.PatchAsync(from, to);
+ config.Data["foo"].Should().Be("baz");
+
+ // Remove a key using PatchAsync(config)
+ config.Data.Remove("hello");
+ config = await _client.PatchAsync(config);
+ config.Data.Should().NotContainKey("hello");
+ }
+
+ [Fact]
+ public async Task Should_Patch_ConfigMap_With_Stale_Base_Async()
+ {
+ // Step 1: Create the ConfigMap
+ var original = await _client.CreateAsync(
+ new V1ConfigMap
+ {
+ Kind = V1ConfigMap.KubeKind,
+ ApiVersion = V1ConfigMap.KubeApiVersion,
+ Metadata = new(name: RandomName(), namespaceProperty: "default"),
+ Data = new Dictionary { { "foo", "bar" } },
+ });
+ _objects.Add(original);
+
+ // Step 2: Update the ConfigMap via client (simulate external change)
+ original.Data["hello"] = "world";
+ var updated = await _client.UpdateAsync(original);
+
+ // Step 3: Patch using the original object as the base, adding another key
+ original.Data["newkey"] = "newvalue";
+ var patched = await _client.PatchAsync(original);
+
+ patched.Data.Should().ContainKey("hello").And.ContainKey("newkey");
+ patched.Data["newkey"].Should().Be("newvalue");
+ }
+
public void Dispose()
{
_client.Delete(_objects);