diff --git a/src/KubernetesClient/Models/IntOrString.cs b/src/KubernetesClient/Models/IntOrString.cs index e79ef246..65e9cd48 100644 --- a/src/KubernetesClient/Models/IntOrString.cs +++ b/src/KubernetesClient/Models/IntOrString.cs @@ -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 @@ public static implicit operator IntOrString(long v) public static implicit operator string(IntOrString v) { - return v.Value; + return v?.Value; } public static implicit operator IntOrString(string v) diff --git a/src/KubernetesClient/Models/IntOrStringYamlConverter.cs b/src/KubernetesClient/Models/IntOrStringYamlConverter.cs index 514f2be9..cfaa4220 100644 --- a/src/KubernetesClient/Models/IntOrStringYamlConverter.cs +++ b/src/KubernetesClient/Models/IntOrStringYamlConverter.cs @@ -35,7 +35,7 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria 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)); } } } diff --git a/src/KubernetesClient/Models/ResourceQuantity.cs b/src/KubernetesClient/Models/ResourceQuantity.cs index 6b4ff8d8..eee89c67 100644 --- a/src/KubernetesClient/Models/ResourceQuantity.cs +++ b/src/KubernetesClient/Models/ResourceQuantity.cs @@ -54,7 +54,7 @@ namespace k8s.Models /// cause implementors to also use a fixed point implementation. /// [JsonConverter(typeof(ResourceQuantityJsonConverter))] - public struct ResourceQuantity + public class ResourceQuantity { public enum SuffixFormat { @@ -179,6 +179,46 @@ public static implicit operator ResourceQuantity(decimal v) 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 BinSuffixes = diff --git a/src/KubernetesClient/Models/ResourceQuantityYamlConverter.cs b/src/KubernetesClient/Models/ResourceQuantityYamlConverter.cs index 50523ca6..1006a3cd 100644 --- a/src/KubernetesClient/Models/ResourceQuantityYamlConverter.cs +++ b/src/KubernetesClient/Models/ResourceQuantityYamlConverter.cs @@ -35,7 +35,7 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria 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())); } } } diff --git a/tests/E2E.Tests/MinikubeTests.cs b/tests/E2E.Tests/MinikubeTests.cs index 3ca37888..8a7834a5 100644 --- a/tests/E2E.Tests/MinikubeTests.cs +++ b/tests/E2E.Tests/MinikubeTests.cs @@ -958,6 +958,171 @@ async Task AssertMd5sumAsync(string file, byte[] orig) } } + [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 { ["app"] = "k8scsharp-hpa" }; + + await client.AppsV1.CreateNamespacedDeploymentAsync( + new V1Deployment + { + Metadata = new V1ObjectMeta { Name = deploymentName, Labels = new Dictionary(labels) }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector { MatchLabels = new Dictionary(labels) }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta { Labels = new Dictionary(labels) }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "k8scsharp-hpa", + Image = "nginx", + Resources = new V1ResourceRequirements + { + Requests = new Dictionary + { + { "cpu", new ResourceQuantity("100m") }, + { "memory", new ResourceQuantity("128Mi") }, + }, + Limits = new Dictionary + { + { "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 + { + 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());