Skip to content

Commit

Permalink
Added new methods and updated classes in Neon.K8s.Core and Neon.Opera…
Browse files Browse the repository at this point in the history
…tor.Xunit namespaces

Added new methods to the `JsonHelper` and `ICustomObjectsOperations` classes in the `Neon.K8s.Core` namespace for object deep copying and status replacing. Updated method comments in `JsonHelper` for better clarity. Updated `TestApiServer` and `ResourceApiGroupStatusController` classes in `Neon.Operator.Xunit` namespace to handle resource addition and replacement. Enhanced `Test_Operator` class in `Test.Neon.Operator` namespace to clone resources before status modification and to test new status update methods. Also, fixed a typo in the `Test_Operator` class.
  • Loading branch information
marcusbooyah committed Mar 30, 2024
1 parent 821df14 commit 620b67d
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 44 deletions.
56 changes: 44 additions & 12 deletions src/Neon.Kubernetes.Core/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ public static partial class KubernetesHelper

private sealed class Iso8601TimeSpanConverter : JsonConverter<TimeSpan>
{
/// <inheritdoc/>
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
return XmlConvert.ToTimeSpan(str);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
var iso8601TimeSpanString = XmlConvert.ToString(value); // XmlConvert for TimeSpan uses ISO8601, so delegate serialization to it
Expand All @@ -56,6 +58,7 @@ private sealed class KubernetesDateTimeOffsetConverter : JsonConverter<DateTimeO
private const string RFC3339NanoFormat = "yyyy-MM-dd'T'HH':'mm':'ss.fffffffK";
private const string RFC3339Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ssK";

/// <inheritdoc/>
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
Expand All @@ -76,6 +79,7 @@ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConver
throw new FormatException($"Unable to parse {originalstr} as RFC3339 RFC3339Micro or RFC3339Nano");
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(RFC3339MicroFormat));
Expand All @@ -85,11 +89,14 @@ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSeri
private sealed class KubernetesDateTimeConverter : JsonConverter<DateTime>
{
private static readonly JsonConverter<DateTimeOffset> UtcConverter = new KubernetesDateTimeOffsetConverter();

/// <inheritdoc/>
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return UtcConverter.Read(ref reader, typeToConvert, options).UtcDateTime;
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
UtcConverter.Write(writer, value, options);
Expand Down Expand Up @@ -130,10 +137,10 @@ public static void AddJsonOptions(Action<JsonSerializerOptions> configure)
/// <summary>
/// Deserializes a JSON string to a value of type <typeparamref name="TValue"/>.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="json"></param>
/// <param name="jsonSerializerOptions"></param>
/// <returns></returns>
/// <typeparam name="TValue">The type of the value to deserialize.</typeparam>
/// <param name="json">The JSON string to deserialize.</param>
/// <param name="jsonSerializerOptions">The optional <see cref="JsonSerializerOptions"/> to use for deserialization.</param>
/// <returns>The deserialized value of type <typeparamref name="TValue"/>.</returns>
public static TValue JsonDeserialize<TValue>(string json, JsonSerializerOptions jsonSerializerOptions = null)
{
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
Expand All @@ -142,24 +149,49 @@ public static TValue JsonDeserialize<TValue>(string json, JsonSerializerOptions
/// <summary>
/// Deserializes a JSON stream to a value of type <typeparamref name="TValue"/>.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="json"></param>
/// <param name="jsonSerializerOptions"></param>
/// <returns></returns>
/// <typeparam name="TValue">The type of the value to deserialize.</typeparam>
/// <param name="json">The JSON stream to deserialize.</param>
/// <param name="jsonSerializerOptions">The optional <see cref="JsonSerializerOptions"/> to use for deserialization.</param>
/// <returns>The deserialized value of type <typeparamref name="TValue"/>.</returns>
public static TValue JsonDeserialize<TValue>(Stream json, JsonSerializerOptions jsonSerializerOptions = null)
{
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
}

/// <summary>
/// Serializes an object.
/// Serializes an object to a JSON string.
/// </summary>
/// <param name="value"></param>
/// <param name="jsonSerializerOptions"></param>
/// <returns></returns>
/// <param name="value">The object to serialize.</param>
/// <param name="jsonSerializerOptions">The optional <see cref="JsonSerializerOptions"/> to use for serialization.</param>
/// <returns>The JSON string representation of the object.</returns>
public static string JsonSerialize(object value, JsonSerializerOptions jsonSerializerOptions = null)
{
return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions);
}

/// <summary>
/// Creates a deep copy of an object by serializing and deserializing it.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="value">The object to clone.</param>
/// <param name="jsonSerializerOptions">The optional <see cref="JsonSerializerOptions"/> to use for serialization and deserialization.</param>
/// <returns>A deep copy of the object.</returns>
public static T JsonClone<T>(T value, JsonSerializerOptions jsonSerializerOptions = null)
{
return JsonDeserialize<T>(
json: JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions),
jsonSerializerOptions: jsonSerializerOptions ?? JsonSerializerOptions);
}

/// <summary>
/// Compares two objects by serializing them to JSON and checking for equality.
/// </summary>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
/// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
public static bool JsonEquals<T>(this T x, T y)
{
return JsonSerialize(x) == JsonSerialize(y);
}
}
}
58 changes: 49 additions & 9 deletions src/Neon.Kubernetes/KubernetesExtensions.ClusterCustomObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,18 @@
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;

using Neon.Common;
using Neon.Tasks;
using System.Threading.Tasks;

using k8s;
using k8s.Autorest;
using k8s.Models;

using Neon.Tasks;

namespace Neon.K8s
{
public static partial class KubernetesExtensions
Expand Down Expand Up @@ -881,5 +875,51 @@ await k8s.DeleteClusterCustomObjectAsync(
}
}
}

/// <summary>
/// Replaces the status of a cluster custom object.
/// </summary>
/// <typeparam name="T">The type of the cluster custom object.</typeparam>
/// <param name="k8s">The <see cref="ICustomObjectsOperations"/> instance.</param>
/// <param name="object">The cluster custom object to replace the status for.</param>
/// <param name="namespaceParameter">The namespace of the cluster custom object.</param>
/// <param name="gracePeriodSeconds">The duration in seconds before the object should be deleted.</param>
/// <param name="orphanDependents">Determines whether to orphan the dependents of the object.</param>
/// <param name="propagationPolicy">The propagation policy for the object deletion.</param>
/// <param name="dryRun">The dry run option for the operation.</param>
/// <param name="fieldManager">The field manager for the operation.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The replaced cluster custom object.</returns>
public static async Task<T> ReplaceClusterCustomObjectStatusAsync<T>(
this ICustomObjectsOperations k8s,
T @object,
string namespaceParameter,
int? gracePeriodSeconds = null,
bool? orphanDependents = null,
string propagationPolicy = null,
string dryRun = null,
string fieldManager = null,
CancellationToken cancellationToken = default)

where T : IKubernetesObject<V1ObjectMeta>, new()
{
await SyncContext.Clear;
Covenant.Requires<ArgumentNullException>(@object != null, nameof(@object));
Covenant.Requires<ArgumentNullException>(!string.IsNullOrEmpty(namespaceParameter), nameof(namespaceParameter));

var typeMetadata = typeof(T).GetKubernetesTypeMetadata();

var result = await k8s.ReplaceClusterCustomObjectStatusAsync(
body: @object,
group: typeMetadata.Group,
version: typeMetadata.ApiVersion,
plural: typeMetadata.PluralName,
name: @object.Name(),
dryRun: dryRun,
fieldManager: fieldManager,
cancellationToken: cancellationToken);

return ((JsonElement)result).Deserialize<T>(options: serializeOptions);
}
}
}
85 changes: 67 additions & 18 deletions src/Neon.Kubernetes/KubernetesExtensions.NamespacedCustom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using IdentityModel.OidcClient;

using k8s;
using k8s.Autorest;
using k8s.Models;
Expand Down Expand Up @@ -829,14 +831,14 @@ await k8s.DeleteNamespacedCustomObjectAsync(
/// <returns>The tracking <see cref="Task"/>.</returns>
public static async Task DeleteNamespacedCustomObjectAsync<T>(
this ICustomObjectsOperations k8s,
string namespaceParameter,
T @object,
V1DeleteOptions body = null,
int? gracePeriodSeconds = null,
bool? orphanDependents = null,
string propagationPolicy = null,
string dryRun = null,
CancellationToken cancellationToken = default)
string namespaceParameter,
T @object,
V1DeleteOptions body = null,
int? gracePeriodSeconds = null,
bool? orphanDependents = null,
string propagationPolicy = null,
string dryRun = null,
CancellationToken cancellationToken = default)

where T : IKubernetesObject<V1ObjectMeta>, new()
{
Expand All @@ -849,17 +851,17 @@ public static async Task DeleteNamespacedCustomObjectAsync<T>(
var typeMetadata = typeof(T).GetKubernetesTypeMetadata();

await k8s.DeleteNamespacedCustomObjectAsync(
group: typeMetadata.Group,
version: typeMetadata.ApiVersion,
namespaceParameter: namespaceParameter,
plural: typeMetadata.PluralName,
name: @object.Name(),
body: body,
group: typeMetadata.Group,
version: typeMetadata.ApiVersion,
namespaceParameter: namespaceParameter,
plural: typeMetadata.PluralName,
name: @object.Name(),
body: body,
gracePeriodSeconds: gracePeriodSeconds,
orphanDependents: orphanDependents,
propagationPolicy: propagationPolicy,
dryRun: dryRun,
cancellationToken: cancellationToken);
orphanDependents: orphanDependents,
propagationPolicy: propagationPolicy,
dryRun: dryRun,
cancellationToken: cancellationToken);
}
catch (HttpOperationException e)
{
Expand All @@ -873,5 +875,52 @@ await k8s.DeleteNamespacedCustomObjectAsync(
}
}
}

/// <summary>
/// Replaces the status of a namespaced custom object.
/// </summary>
/// <typeparam name="T">The type of the custom object.</typeparam>
/// <param name="k8s">The <see cref="ICustomObjectsOperations"/> instance.</param>
/// <param name="object">The custom object to replace the status for.</param>
/// <param name="namespaceParameter">The namespace of the custom object.</param>
/// <param name="gracePeriodSeconds">The duration in seconds before the object should be deleted.</param>
/// <param name="orphanDependents">Determines whether to orphan the dependents of the object.</param>
/// <param name="propagationPolicy">The propagation policy for the object.</param>
/// <param name="dryRun">The dry run option for the operation.</param>
/// <param name="fieldManager">The field manager for the operation.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The replaced custom object.</returns>
public static async Task<T> ReplaceNamespacedCustomObjectStatusAsync<T>(
this ICustomObjectsOperations k8s,
T @object,
string namespaceParameter,
int? gracePeriodSeconds = null,
bool? orphanDependents = null,
string propagationPolicy = null,
string dryRun = null,
string fieldManager = null,
CancellationToken cancellationToken = default)

where T : IKubernetesObject<V1ObjectMeta>, new()
{
await SyncContext.Clear;
Covenant.Requires<ArgumentNullException>(@object != null, nameof(@object));
Covenant.Requires<ArgumentNullException>(!string.IsNullOrEmpty(namespaceParameter), nameof(namespaceParameter));

var typeMetadata = typeof(T).GetKubernetesTypeMetadata();

var result = await k8s.ReplaceNamespacedCustomObjectStatusAsync(
group: typeMetadata.Group,
version: typeMetadata.ApiVersion,
namespaceParameter: namespaceParameter,
plural: typeMetadata.PluralName,
name: @object.Name(),
body: @object,
fieldManager: fieldManager,
dryRun: dryRun,
cancellationToken: cancellationToken);

return ((JsonElement)result).Deserialize<T>(options: serializeOptions);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using System.Threading.Tasks;

using k8s;
using k8s.Models;

using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -81,6 +82,12 @@ public ResourceApiGroupStatusController(ITestApiServer testApiServer, JsonSerial
[FromRoute]
public string Name { get; set; }

/// <summary>
/// The namespace name of the <see cref="IKubernetesObject"/>.
/// </summary>
[FromRoute]
public string Namespace { get; set; }

/// <summary>
/// Get the list of resources
/// </summary>
Expand Down Expand Up @@ -130,8 +137,14 @@ public async Task<ActionResult<ResourceObject>> PatchAsync([FromBody] object pat
if (testApiServer.Types.TryGetValue(key, out Type type))
{
var typeMetadata = type.GetKubernetesTypeMetadata();
var resource = testApiServer.Resources.Where(r => r.Kind == typeMetadata.Kind && r.Metadata.Name == Name)
.Single();
var resourceQuery = testApiServer.Resources.Where(r => r.Kind == typeMetadata.Kind && r.Metadata.Name == Name);

if (!string.IsNullOrEmpty(Namespace))
{
resourceQuery = resourceQuery.Where(r => r.EnsureMetadata().NamespaceProperty == Namespace);
}

var resource = resourceQuery.Single();

p0.ApplyTo(resource);

Expand All @@ -140,5 +153,45 @@ public async Task<ActionResult<ResourceObject>> PatchAsync([FromBody] object pat

throw new TypeNotRegisteredException(Group, Version, Plural);
}

/// <summary>
/// Replaces a resource and stores it in <see cref="TestApiServer.Resources"/>
/// </summary>
/// <param name="resource">Specifies the replacement resource.</param>
/// <returns>An action result containing the resource.</returns>
[HttpPut]
public async Task<ActionResult<ResourceObject>> UpdateAsync([FromBody] object resource)
{
await SyncContext.Clear;
Covenant.Requires<ArgumentNullException>(resource != null, nameof(resource));

var key = ApiHelper.CreateKey(Group, Version, Plural);

if (testApiServer.Types.TryGetValue(key, out Type type))
{
var typeMetadata = type.GetKubernetesTypeMetadata();
var json = JsonSerializer.Serialize(resource, jsonSerializerOptions);
var instance = JsonSerializer.Deserialize(json, type, jsonSerializerOptions);
var resourceQuery = testApiServer.Resources.Where(resource => resource.Kind == typeMetadata.Kind && resource.Metadata.Name == Name);

if (!string.IsNullOrEmpty(Namespace))
{
resourceQuery = resourceQuery.Where(r => r.EnsureMetadata().NamespaceProperty == Namespace);
}

dynamic existing = resourceQuery.SingleOrDefault();

if (existing == null)
{
return NotFound();
}

existing.Status = ((dynamic)instance).Status;

return Ok(resource);
}

throw new TypeNotRegisteredException(Group, Version, Plural);
}
}
}
Loading

0 comments on commit 620b67d

Please sign in to comment.