revert change to structs from classes for IntOrString and ResourceQuantity, and handle null values in YAML converters (#1673)

* feat: add V2HorizontalPodAutoscaler integration test

* fix: change structs to classes for IntOrString and ResourceQuantity, and handle null values in YAML converters

* feat: implement equality members for ResourceQuantity class
This commit is contained in:
Boshi Lian
2025-10-15 08:59:35 -07:00
committed by GitHub
parent 6d27bd900b
commit 292262755a
5 changed files with 211 additions and 6 deletions

View File

@@ -1,9 +1,9 @@
namespace k8s.Models
{
[JsonConverter(typeof(IntOrStringJsonConverter))]
public struct IntOrString
public class IntOrString
{
public string? Value { get; private init; }
public string Value { get; private init; }
public static implicit operator IntOrString(int v)
{
@@ -17,7 +17,7 @@ namespace k8s.Models
public static implicit operator string(IntOrString v)
{
return v.Value;
return v?.Value;
}
public static implicit operator IntOrString(string v)

View File

@@ -35,7 +35,7 @@ namespace k8s.Models
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
var obj = (IntOrString)value;
emitter?.Emit(new YamlDotNet.Core.Events.Scalar(obj.Value));
emitter?.Emit(new YamlDotNet.Core.Events.Scalar(obj?.Value));
}
}
}

View File

@@ -54,7 +54,7 @@ namespace k8s.Models
/// cause implementors to also use a fixed point implementation.
/// </summary>
[JsonConverter(typeof(ResourceQuantityJsonConverter))]
public struct ResourceQuantity
public class ResourceQuantity
{
public enum SuffixFormat
{
@@ -179,6 +179,46 @@ namespace k8s.Models
return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
}
public bool Equals(ResourceQuantity other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return _unitlessValue.Equals(other._unitlessValue);
}
public override bool Equals(object obj)
{
return Equals(obj as ResourceQuantity);
}
public override int GetHashCode()
{
return _unitlessValue.GetHashCode();
}
public static bool operator ==(ResourceQuantity left, ResourceQuantity right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
public static bool operator !=(ResourceQuantity left, ResourceQuantity right)
{
return !(left == right);
}
private sealed class Suffixer
{
private static readonly IReadOnlyDictionary<string, (int, int)> BinSuffixes =

View File

@@ -35,7 +35,7 @@ namespace k8s.Models
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
var obj = (ResourceQuantity)value;
emitter?.Emit(new YamlDotNet.Core.Events.Scalar(obj.ToString()));
emitter?.Emit(new YamlDotNet.Core.Events.Scalar(obj?.ToString()));
}
}
}

View File

@@ -958,6 +958,171 @@ namespace k8s.E2E
}
}
[MinikubeFact]
public async Task V2HorizontalPodAutoscalerTestAsync()
{
var namespaceParameter = "default";
var deploymentName = "k8scsharp-e2e-hpa-deployment";
var hpaName = "k8scsharp-e2e-hpa";
using var client = CreateClient();
async Task CleanupAsync()
{
var deleteOptions = new V1DeleteOptions { PropagationPolicy = "Foreground" };
try
{
await client.AutoscalingV2.DeleteNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter, deleteOptions).ConfigureAwait(false);
}
catch (HttpOperationException e)
{
if (e.Response?.StatusCode != System.Net.HttpStatusCode.NotFound)
{
throw;
}
}
try
{
await client.AppsV1.DeleteNamespacedDeploymentAsync(deploymentName, namespaceParameter, deleteOptions).ConfigureAwait(false);
}
catch (HttpOperationException e)
{
if (e.Response?.StatusCode != System.Net.HttpStatusCode.NotFound)
{
throw;
}
}
var attempts = 10;
while (attempts-- > 0)
{
var hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
var deploymentList = await client.AppsV1.ListNamespacedDeploymentAsync(namespaceParameter).ConfigureAwait(false);
if (hpaList.Items.All(item => item.Metadata.Name != hpaName) && deploymentList.Items.All(item => item.Metadata.Name != deploymentName))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
}
try
{
await CleanupAsync().ConfigureAwait(false);
var labels = new Dictionary<string, string> { ["app"] = "k8scsharp-hpa" };
await client.AppsV1.CreateNamespacedDeploymentAsync(
new V1Deployment
{
Metadata = new V1ObjectMeta { Name = deploymentName, Labels = new Dictionary<string, string>(labels) },
Spec = new V1DeploymentSpec
{
Replicas = 1,
Selector = new V1LabelSelector { MatchLabels = new Dictionary<string, string>(labels) },
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta { Labels = new Dictionary<string, string>(labels) },
Spec = new V1PodSpec
{
Containers = new[]
{
new V1Container
{
Name = "k8scsharp-hpa",
Image = "nginx",
Resources = new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>
{
{ "cpu", new ResourceQuantity("100m") },
{ "memory", new ResourceQuantity("128Mi") },
},
Limits = new Dictionary<string, ResourceQuantity>
{
{ "cpu", new ResourceQuantity("200m") },
{ "memory", new ResourceQuantity("256Mi") },
},
},
},
},
},
},
},
},
namespaceParameter).ConfigureAwait(false);
var hpa = new V2HorizontalPodAutoscaler
{
Metadata = new V1ObjectMeta { Name = hpaName },
Spec = new V2HorizontalPodAutoscalerSpec
{
MinReplicas = 1,
MaxReplicas = 3,
ScaleTargetRef = new V2CrossVersionObjectReference
{
ApiVersion = "apps/v1",
Kind = "Deployment",
Name = deploymentName,
},
Metrics = new List<V2MetricSpec>
{
new V2MetricSpec
{
Type = "Resource",
Resource = new V2ResourceMetricSource
{
Name = "cpu",
Target = new V2MetricTarget
{
Type = "Utilization",
AverageUtilization = 50,
},
},
},
},
},
};
await client.AutoscalingV2.CreateNamespacedHorizontalPodAutoscalerAsync(hpa, namespaceParameter).ConfigureAwait(false);
var hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
Assert.Contains(hpaList.Items, item => item.Metadata.Name == hpaName);
var created = await client.AutoscalingV2.ReadNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter).ConfigureAwait(false);
Assert.Equal(1, created.Spec.MinReplicas);
created.Spec.MinReplicas = 2;
await client.AutoscalingV2.ReplaceNamespacedHorizontalPodAutoscalerAsync(created, hpaName, namespaceParameter).ConfigureAwait(false);
var updated = await client.AutoscalingV2.ReadNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter).ConfigureAwait(false);
Assert.Equal(2, updated.Spec.MinReplicas);
await client.AutoscalingV2.DeleteNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter, new V1DeleteOptions { PropagationPolicy = "Foreground" }).ConfigureAwait(false);
var retries = 10;
while (retries-- > 0)
{
hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
if (hpaList.Items.All(item => item.Metadata.Name != hpaName))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
Assert.DoesNotContain(hpaList.Items, item => item.Metadata.Name == hpaName);
}
finally
{
await CleanupAsync().ConfigureAwait(false);
}
}
public static IKubernetes CreateClient()
{
return new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig());