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
6 changes: 3 additions & 3 deletions src/KubernetesClient/Models/IntOrString.cs
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/KubernetesClient/Models/IntOrStringYamlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
42 changes: 41 additions & 1 deletion src/KubernetesClient/Models/ResourceQuantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
/// cause implementors to also use a fixed point implementation.
/// </summary>
[JsonConverter(typeof(ResourceQuantityJsonConverter))]
public struct ResourceQuantity
public class ResourceQuantity
{
public enum SuffixFormat
{
Expand Down Expand Up @@ -171,7 +171,7 @@

public static implicit operator decimal(ResourceQuantity v)
{
return v.ToDecimal();

Check warning on line 174 in src/KubernetesClient/Models/ResourceQuantity.cs

View workflow job for this annotation

GitHub Actions / e2e

In externally visible method 'ResourceQuantity.implicit operator decimal(ResourceQuantity v)', validate parameter 'v' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)
}

public static implicit operator ResourceQuantity(decimal v)
Expand All @@ -179,6 +179,46 @@
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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
}
165 changes: 165 additions & 0 deletions tests/E2E.Tests/MinikubeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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());
Expand Down
Loading