Skip to content

Commit ea11a51

Browse files
Implement KubectlTop for node and pod metrics (#1703)
* Initial plan * Implement KubectlTop with TopNodes and TopPods methods Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> * Update metrics example to demonstrate KubectlTop usage Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
1 parent 31df7ed commit ea11a51

File tree

5 files changed

+521
-0
lines changed

5 files changed

+521
-0
lines changed

examples/metrics/Program.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using k8s;
2+
using k8s.kubectl.beta;
23
using System;
34
using System.Linq;
45
using System.Threading.Tasks;
@@ -43,9 +44,79 @@ async Task PodsMetrics(IKubernetes client)
4344
}
4445
}
4546

47+
async Task TopNodes(Kubectl kubectl)
48+
{
49+
Console.WriteLine("=== Top Nodes by CPU ===");
50+
var topNodesCpu = kubectl.TopNodes("cpu");
51+
52+
// Show top 5
53+
var topNodesCpuList = topNodesCpu.Take(5).ToList();
54+
foreach (var item in topNodesCpuList)
55+
{
56+
var cpuUsage = item.Metrics.Usage["cpu"];
57+
var cpuCapacity = item.Resource.Status.Capacity["cpu"];
58+
var cpuPercent = (cpuUsage.ToDouble() / cpuCapacity.ToDouble()) * 100;
59+
60+
Console.WriteLine($"{item.Resource.Metadata.Name}: CPU {cpuUsage} / {cpuCapacity} ({cpuPercent:F2}%)");
61+
}
62+
63+
Console.WriteLine(Environment.NewLine);
64+
Console.WriteLine("=== Top Nodes by Memory ===");
65+
var topNodesMemory = kubectl.TopNodes("memory");
66+
67+
// Show top 5
68+
var topNodesMemoryList = topNodesMemory.Take(5).ToList();
69+
foreach (var item in topNodesMemoryList)
70+
{
71+
var memUsage = item.Metrics.Usage["memory"];
72+
var memCapacity = item.Resource.Status.Capacity["memory"];
73+
var memPercent = (memUsage.ToDouble() / memCapacity.ToDouble()) * 100;
74+
75+
Console.WriteLine($"{item.Resource.Metadata.Name}: Memory {memUsage} / {memCapacity} ({memPercent:F2}%)");
76+
}
77+
}
78+
79+
async Task TopPods(Kubectl kubectl)
80+
{
81+
Console.WriteLine("=== Top Pods by CPU (kube-system namespace) ===");
82+
var topPodsCpu = kubectl.TopPods("kube-system", "cpu");
83+
84+
// Show top 5
85+
var topPodsCpuList = topPodsCpu.Take(5).ToList();
86+
foreach (var item in topPodsCpuList)
87+
{
88+
var cpuSum = item.Metrics.Containers.Sum(c =>
89+
c.Usage.ContainsKey("cpu") ? c.Usage["cpu"].ToDouble() : 0);
90+
91+
Console.WriteLine($"{item.Resource.Metadata.Name}: CPU {cpuSum:F3} cores");
92+
}
93+
94+
Console.WriteLine(Environment.NewLine);
95+
Console.WriteLine("=== Top Pods by Memory (kube-system namespace) ===");
96+
var topPodsMemory = kubectl.TopPods("kube-system", "memory");
97+
98+
// Show top 5
99+
var topPodsMemoryList = topPodsMemory.Take(5).ToList();
100+
foreach (var item in topPodsMemoryList)
101+
{
102+
var memSum = item.Metrics.Containers.Sum(c =>
103+
c.Usage.ContainsKey("memory") ? c.Usage["memory"].ToDouble() : 0);
104+
105+
Console.WriteLine($"{item.Resource.Metadata.Name}: Memory {memSum / (1024 * 1024):F2} MiB");
106+
}
107+
}
108+
46109
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
47110
var client = new Kubernetes(config);
111+
var kubectl = new Kubectl(client);
48112

113+
Console.WriteLine("=== Raw Metrics API ===");
49114
await NodesMetrics(client).ConfigureAwait(false);
50115
Console.WriteLine(Environment.NewLine);
51116
await PodsMetrics(client).ConfigureAwait(false);
117+
118+
Console.WriteLine(Environment.NewLine);
119+
Console.WriteLine("=== Kubectl Top API ===");
120+
await TopNodes(kubectl).ConfigureAwait(false);
121+
Console.WriteLine(Environment.NewLine);
122+
await TopPods(kubectl).ConfigureAwait(false);

examples/metrics/metrics.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
<OutputType>Exe</OutputType>
55
</PropertyGroup>
66

7+
<ItemGroup>
8+
<ProjectReference Include="..\..\src\KubernetesClient.Kubectl\KubernetesClient.Kubectl.csproj" />
9+
</ItemGroup>
10+
711
</Project>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using k8s.Models;
2+
3+
namespace k8s.kubectl.beta;
4+
5+
public partial class AsyncKubectl
6+
{
7+
/// <summary>
8+
/// Describes a pair of Kubernetes resource and its metrics.
9+
/// </summary>
10+
/// <typeparam name="TResource">The type of Kubernetes resource.</typeparam>
11+
/// <typeparam name="TMetrics">The type of metrics.</typeparam>
12+
public class ResourceMetrics<TResource, TMetrics>
13+
{
14+
/// <summary>
15+
/// Gets or sets the Kubernetes resource.
16+
/// </summary>
17+
public required TResource Resource { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the metrics for the resource.
21+
/// </summary>
22+
public required TMetrics Metrics { get; set; }
23+
}
24+
25+
/// <summary>
26+
/// Get top nodes sorted by CPU or memory usage.
27+
/// </summary>
28+
/// <param name="metric">The metric to sort by ("cpu" or "memory"). Defaults to "cpu".</param>
29+
/// <param name="cancellationToken">Cancellation token.</param>
30+
/// <returns>A list of nodes with their metrics, sorted by the specified metric in descending order.</returns>
31+
public async Task<List<ResourceMetrics<V1Node, NodeMetrics>>> TopNodesAsync(string metric = "cpu", CancellationToken cancellationToken = default)
32+
{
33+
if (metric != "cpu" && metric != "memory")
34+
{
35+
throw new ArgumentException("Metric must be either 'cpu' or 'memory'", nameof(metric));
36+
}
37+
38+
// Get all nodes
39+
var nodes = await client.CoreV1.ListNodeAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
40+
41+
// Get node metrics
42+
var nodeMetrics = await client.GetKubernetesNodesMetricsAsync().ConfigureAwait(false);
43+
44+
// Create a dictionary for quick lookup of metrics by node name
45+
var metricsDict = nodeMetrics.Items.ToDictionary(m => m.Metadata.Name, m => m);
46+
47+
// Combine nodes with their metrics and calculate percentage
48+
var result = new List<ResourceMetrics<V1Node, NodeMetrics>>();
49+
foreach (var node in nodes.Items)
50+
{
51+
if (metricsDict.TryGetValue(node.Metadata.Name, out var metrics))
52+
{
53+
result.Add(new ResourceMetrics<V1Node, NodeMetrics>
54+
{
55+
Resource = node,
56+
Metrics = metrics,
57+
});
58+
}
59+
}
60+
61+
// Sort by metric value (percentage of capacity) in descending order
62+
result.Sort((a, b) =>
63+
{
64+
var percentageA = CalculateNodePercentage(a.Resource, a.Metrics, metric);
65+
var percentageB = CalculateNodePercentage(b.Resource, b.Metrics, metric);
66+
return percentageB.CompareTo(percentageA); // Descending order
67+
});
68+
69+
return result;
70+
}
71+
72+
/// <summary>
73+
/// Get top pods in a namespace sorted by CPU or memory usage.
74+
/// </summary>
75+
/// <param name="namespace">The namespace to get pod metrics from.</param>
76+
/// <param name="metric">The metric to sort by ("cpu" or "memory"). Defaults to "cpu".</param>
77+
/// <param name="cancellationToken">Cancellation token.</param>
78+
/// <returns>A list of pods with their metrics, sorted by the specified metric in descending order.</returns>
79+
public async Task<List<ResourceMetrics<V1Pod, PodMetrics>>> TopPodsAsync(string @namespace, string metric = "cpu", CancellationToken cancellationToken = default)
80+
{
81+
if (string.IsNullOrEmpty(@namespace))
82+
{
83+
throw new ArgumentException("Namespace cannot be null or empty", nameof(@namespace));
84+
}
85+
86+
if (metric != "cpu" && metric != "memory")
87+
{
88+
throw new ArgumentException("Metric must be either 'cpu' or 'memory'", nameof(metric));
89+
}
90+
91+
// Get all pods in the namespace
92+
var pods = await client.CoreV1.ListNamespacedPodAsync(@namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
93+
94+
// Get pod metrics for the namespace
95+
var podMetrics = await client.GetKubernetesPodsMetricsByNamespaceAsync(@namespace).ConfigureAwait(false);
96+
97+
// Create a dictionary for quick lookup of metrics by pod name
98+
var metricsDict = podMetrics.Items.ToDictionary(m => m.Metadata.Name, m => m);
99+
100+
// Combine pods with their metrics
101+
var result = new List<ResourceMetrics<V1Pod, PodMetrics>>();
102+
foreach (var pod in pods.Items)
103+
{
104+
if (metricsDict.TryGetValue(pod.Metadata.Name, out var metrics))
105+
{
106+
result.Add(new ResourceMetrics<V1Pod, PodMetrics>
107+
{
108+
Resource = pod,
109+
Metrics = metrics,
110+
});
111+
}
112+
}
113+
114+
// Sort by metric value (sum across all containers) in descending order
115+
result.Sort((a, b) =>
116+
{
117+
var sumA = CalculatePodMetricSum(a.Metrics, metric);
118+
var sumB = CalculatePodMetricSum(b.Metrics, metric);
119+
return sumB.CompareTo(sumA); // Descending order
120+
});
121+
122+
return result;
123+
}
124+
125+
/// <summary>
126+
/// Calculate the percentage of node capacity used by the specified metric.
127+
/// </summary>
128+
private static double CalculateNodePercentage(V1Node node, NodeMetrics metrics, string metric)
129+
{
130+
if (metrics?.Usage == null || !metrics.Usage.TryGetValue(metric, out var usage))
131+
{
132+
return 0;
133+
}
134+
135+
if (node?.Status?.Capacity == null || !node.Status.Capacity.TryGetValue(metric, out var capacity))
136+
{
137+
return double.PositiveInfinity;
138+
}
139+
140+
var usageValue = usage.ToDouble();
141+
var capacityValue = capacity.ToDouble();
142+
143+
if (capacityValue == 0)
144+
{
145+
return double.PositiveInfinity;
146+
}
147+
148+
return usageValue / capacityValue;
149+
}
150+
151+
/// <summary>
152+
/// Calculate the sum of a metric across all containers in a pod.
153+
/// </summary>
154+
private static double CalculatePodMetricSum(PodMetrics podMetrics, string metric)
155+
{
156+
if (podMetrics?.Containers == null)
157+
{
158+
return 0;
159+
}
160+
161+
double sum = 0;
162+
foreach (var container in podMetrics.Containers)
163+
{
164+
if (container?.Usage != null && container.Usage.TryGetValue(metric, out var value))
165+
{
166+
sum += value.ToDouble();
167+
}
168+
}
169+
170+
return sum;
171+
}
172+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using k8s.Models;
2+
3+
namespace k8s.kubectl.beta;
4+
5+
public partial class Kubectl
6+
{
7+
/// <summary>
8+
/// Get top nodes sorted by CPU or memory usage.
9+
/// </summary>
10+
/// <param name="metric">The metric to sort by ("cpu" or "memory"). Defaults to "cpu".</param>
11+
/// <returns>A list of nodes with their metrics, sorted by the specified metric in descending order.</returns>
12+
public List<AsyncKubectl.ResourceMetrics<V1Node, NodeMetrics>> TopNodes(string metric = "cpu")
13+
{
14+
return client.TopNodesAsync(metric).GetAwaiter().GetResult();
15+
}
16+
17+
/// <summary>
18+
/// Get top pods in a namespace sorted by CPU or memory usage.
19+
/// </summary>
20+
/// <param name="namespace">The namespace to get pod metrics from.</param>
21+
/// <param name="metric">The metric to sort by ("cpu" or "memory"). Defaults to "cpu".</param>
22+
/// <returns>A list of pods with their metrics, sorted by the specified metric in descending order.</returns>
23+
public List<AsyncKubectl.ResourceMetrics<V1Pod, PodMetrics>> TopPods(string @namespace, string metric = "cpu")
24+
{
25+
return client.TopPodsAsync(@namespace, metric).GetAwaiter().GetResult();
26+
}
27+
}

0 commit comments

Comments
 (0)