Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .config/dotnet-tools.json

This file was deleted.

7 changes: 7 additions & 0 deletions KubeOps.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
67 changes: 67 additions & 0 deletions src/KubeOps.Abstractions/Entities/JsonPatchExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Method extensions for JSON diffing between two entities (<see cref="IKubernetesObject{TMetadata}"/>).
/// </summary>
public static class JsonPatchExtensions
{
/// <summary>
/// Convert a <see cref="IKubernetesObject{TMetadata}"/> into a <see cref="JsonNode"/>.
/// </summary>
/// <param name="entity">The entity to convert.</param>
/// <returns>Either the json node, or null if it failed.</returns>
public static JsonNode? ToNode(this IKubernetesObject<V1ObjectMeta> entity) =>
JsonNode.Parse(KubernetesJson.Serialize(entity));

/// <summary>
/// Computes the JSON Patch diff between two Kubernetes entities implementing <see cref="IKubernetesObject{V1ObjectMeta}"/>.
/// This method serializes both entities to JSON and calculates the difference as a JSON Patch document.
/// </summary>
/// <param name="from">The source entity to compare from.</param>
/// <param name="to">The target entity to compare to.</param>
/// <returns>A <see cref="JsonNode"/> representing the JSON Patch diff between the two entities.</returns>
/// <exception cref="InvalidOperationException">Thrown if the diff could not be created.</exception>
public static JsonPatch CreateJsonPatch(
this IKubernetesObject<V1ObjectMeta> from,
IKubernetesObject<V1ObjectMeta> to)
{
var fromNode = from.ToNode();
var toNode = to.ToNode();
var patch = fromNode.CreatePatch(toNode);

return patch;
}

/// <summary>
/// Create a <see cref="V1Patch"/> out of a <see cref="JsonPatch"/>.
/// This can be used to apply the patch to a Kubernetes entity using the Kubernetes client.
/// </summary>
/// <param name="patch">The patch that should be converted.</param>
/// <returns>A <see cref="V1Patch"/> that may be applied to Kubernetes objects.</returns>
public static V1Patch ToKubernetesPatch(this JsonPatch patch) =>
new(patch.ToJsonString(), V1Patch.PatchType.JsonPatch);

/// <summary>
/// Create the unformatted JSON string representation of a <see cref="JsonPatch"/>.
/// </summary>
/// <param name="patch">The <see cref="JsonPatch"/> to convert.</param>
/// <returns>A string that represents the unformatted JSON representation of the patch.</returns>
public static string ToJsonString(this JsonPatch patch) => patch.ToJsonDocument().RootElement.GetRawText();

/// <summary>
/// Create the base 64 representation of a <see cref="JsonPatch"/>.
/// </summary>
/// <param name="patch">The patch to convert.</param>
/// <returns>The base64 encoded representation of the patch.</returns>
public static string ToBase64String(this JsonPatch patch) =>
Convert.ToBase64String(Encoding.UTF8.GetBytes(patch.ToJsonString()));
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
namespace KubeOps.Abstractions.Entities;

/// <summary>
/// Method extensions for <see cref="IKubernetesObject{TMetadata}"/>.
/// Basic extensions for <see cref="IKubernetesObject{TMetadata}"/>.
/// Extensions that target the Kubernetes Object and its metadata.
/// </summary>
public static class Extensions
public static class KubernetesExtensions
{
/// <summary>
/// Sets the resource version of the specified Kubernetes object to the specified value.
Expand Down
1 change: 1 addition & 0 deletions src/KubeOps.Abstractions/KubeOps.Abstractions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JsonPatch.Net" Version="3.3.0" />
<PackageReference Include="KubernetesClient" Version="16.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6"/>
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.3.0" />
Expand Down
140 changes: 139 additions & 1 deletion src/KubeOps.KubernetesClient/IKubernetesClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using k8s;
using Json.Patch;

using k8s;
using k8s.Models;

using KubeOps.Abstractions.Entities;
Expand Down Expand Up @@ -320,6 +322,142 @@ Task<TEntity> UpdateStatusAsync<TEntity>(TEntity entity, CancellationToken cance
TEntity UpdateStatus<TEntity>(TEntity entity)
where TEntity : IKubernetesObject<V1ObjectMeta>;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity containing the desired updates.</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
/// <returns>The patched entity.</returns>
/// <exception cref="InvalidOperationException">Thrown if the entity to be patched does not exist on the API.</exception>
Task<TEntity> PatchAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta>
{
var currentEntity = Get<TEntity>(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);
}

/// <summary>
/// Patch a given entity on the Kubernetes API by calculating the diff between two provided entities.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="from">The current/original entity.</param>
/// <param name="to">The updated entity with desired changes.</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
/// <returns>The patched entity.</returns>
Task<TEntity> PatchAsync<TEntity>(TEntity from, TEntity to, CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta> =>
PatchAsync(from, from.CreateJsonPatch(to), cancellationToken);

/// <summary>
/// Patch a given entity on the Kubernetes API using a <see cref="JsonPatch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity to patch.</param>
/// <param name="patch">The <see cref="JsonPatch"/> representing the changes to apply.</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
/// <returns>The patched entity.</returns>
Task<TEntity> PatchAsync<TEntity>(TEntity entity, JsonPatch patch, CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta> =>
PatchAsync(entity, patch.ToKubernetesPatch(), cancellationToken);

/// <summary>
/// Patch a given entity on the Kubernetes API using a <see cref="V1Patch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity to patch.</param>
/// <param name="patch">The <see cref="V1Patch"/> representing the changes to apply.</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
/// <returns>The patched entity.</returns>
Task<TEntity> PatchAsync<TEntity>(TEntity entity, V1Patch patch, CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta> =>
PatchAsync<TEntity>(patch, entity.Name(), entity.Namespace(), cancellationToken);

/// <summary>
/// Patch a given entity on the Kubernetes API by name and namespace using a <see cref="V1Patch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="patch">The <see cref="V1Patch"/> representing the changes to apply.</param>
/// <param name="name">The name of the entity to patch.</param>
/// <param name="namespace">The namespace of the entity to patch (if applicable).</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests.</param>
/// <returns>The patched entity.</returns>
Task<TEntity> PatchAsync<TEntity>(
V1Patch patch,
string name,
string? @namespace = null,
CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta>;

/// <summary>
/// Patch a given entity on the Kubernetes API by calculating the diff between the current entity and the provided entity.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity containing the desired updates.</param>
/// <returns>The patched entity.</returns>
/// <exception cref="InvalidOperationException">Thrown if the entity to be patched does not exist on the API.</exception>
TEntity Patch<TEntity>(TEntity entity)
where TEntity : IKubernetesObject<V1ObjectMeta>
=> PatchAsync(entity).GetAwaiter().GetResult();

/// <summary>
/// Patch a given entity on the Kubernetes API by calculating the diff between two provided entities.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="from">The current/original entity.</param>
/// <param name="to">The updated entity with desired changes.</param>
/// <returns>The patched entity.</returns>
TEntity Patch<TEntity>(TEntity from, TEntity to)
where TEntity : IKubernetesObject<V1ObjectMeta>
=> PatchAsync(from, to).GetAwaiter().GetResult();

/// <summary>
/// Patch a given entity on the Kubernetes API using a <see cref="JsonPatch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity to patch.</param>
/// <param name="patch">The <see cref="JsonPatch"/> representing the changes to apply.</param>
/// <returns>The patched entity.</returns>
TEntity Patch<TEntity>(TEntity entity, JsonPatch patch)
where TEntity : IKubernetesObject<V1ObjectMeta>
=> PatchAsync(entity, patch).GetAwaiter().GetResult();

/// <summary>
/// Patch a given entity on the Kubernetes API using a <see cref="V1Patch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="entity">The entity to patch.</param>
/// <param name="patch">The <see cref="V1Patch"/> representing the changes to apply.</param>
/// <returns>The patched entity.</returns>
TEntity Patch<TEntity>(TEntity entity, V1Patch patch)
where TEntity : IKubernetesObject<V1ObjectMeta>
=> PatchAsync(entity, patch).GetAwaiter().GetResult();

/// <summary>
/// Patch a given entity on the Kubernetes API by name and namespace using a <see cref="V1Patch"/> object.
/// </summary>
/// <typeparam name="TEntity">The type of the Kubernetes entity.</typeparam>
/// <param name="patch">The <see cref="V1Patch"/> representing the changes to apply.</param>
/// <param name="name">The name of the entity to patch.</param>
/// <param name="namespace">The namespace of the entity to patch (if applicable).</param>
/// <returns>The patched entity.</returns>
TEntity Patch<TEntity>(
V1Patch patch,
string name,
string? @namespace = null)
where TEntity : IKubernetesObject<V1ObjectMeta>
=> PatchAsync<TEntity>(patch, name, @namespace).GetAwaiter().GetResult();

/// <inheritdoc cref="Delete{TEntity}(TEntity)"/>
/// <returns>A task that completes when the call was made.</returns>
Task DeleteAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default)
Expand Down
18 changes: 18 additions & 0 deletions src/KubeOps.KubernetesClient/KubernetesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,24 @@ public TEntity UpdateStatus<TEntity>(TEntity entity)
};
}

/// <inheritdoc />
public async Task<TEntity> PatchAsync<TEntity>(
V1Patch patch,
string name,
string? @namespace = null,
CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta>
{
ThrowIfDisposed();

using var client = CreateGenericClient<TEntity>();
return await (@namespace switch
{
not null => client.PatchNamespacedAsync<TEntity>(patch, @namespace, name, cancellationToken),
null => client.PatchAsync<TEntity>(patch, name, cancellationToken),
});
}

/// <inheritdoc />
public async Task DeleteAsync<TEntity>(
string name,
Expand Down
1 change: 0 additions & 1 deletion src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

<ItemGroup>
<PackageReference Include="Localtunnel" Version="2.0.0" NoWarn="NU5104" />
<PackageReference Include="SystemTextJson.JsonDiffPatch" Version="2.0.0" />
</ItemGroup>

</Project>
27 changes: 0 additions & 27 deletions src/KubeOps.Operator.Web/Webhooks/Admission/Mutation/JsonDiffer.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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(),
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using k8s;
using k8s.Models;

using KubeOps.Abstractions.Entities;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

Expand Down Expand Up @@ -104,11 +106,11 @@ public async Task<IActionResult> Mutate(
[FromBody] AdmissionRequest<TEntity> 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
{
Expand Down
1 change: 0 additions & 1 deletion src/KubeOps.Operator/KubeOps.Operator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6"/>
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading