From 29da827dce5f04585a77d6f4b8e49b2eecde5677 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 00:45:46 +0000
Subject: [PATCH 1/8] Initial plan
From a4a5fc2fe01a939574f7dd7c53d17b52bb39f69a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 00:56:10 +0000
Subject: [PATCH 2/8] Implement KubectlGet functionality with tests
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
---
.../Beta/AsyncKubectl.Get.cs | 88 +++++++++
.../Beta/Kubectl.Get.cs | 81 +++++++++
tests/Kubectl.Tests/KubectlTests.Get.cs | 168 ++++++++++++++++++
3 files changed, 337 insertions(+)
create mode 100644 src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
create mode 100644 src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
create mode 100644 tests/Kubectl.Tests/KubectlTests.Get.cs
diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
new file mode 100644
index 000000000..62a2e33fe
--- /dev/null
+++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
@@ -0,0 +1,88 @@
+using k8s.Models;
+
+namespace k8s.kubectl.beta;
+
+public partial class AsyncKubectl
+{
+ ///
+ /// Get a pod by name in a namespace.
+ ///
+ /// The name of the pod.
+ /// The namespace of the pod. Defaults to "default".
+ /// Cancellation token.
+ /// The pod.
+ public async Task GetPodAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNamespacedPodAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a deployment by name in a namespace.
+ ///
+ /// The name of the deployment.
+ /// The namespace of the deployment. Defaults to "default".
+ /// Cancellation token.
+ /// The deployment.
+ public async Task GetDeploymentAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ {
+ return await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a service by name in a namespace.
+ ///
+ /// The name of the service.
+ /// The namespace of the service. Defaults to "default".
+ /// Cancellation token.
+ /// The service.
+ public async Task GetServiceAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNamespacedServiceAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a namespace by name.
+ ///
+ /// The name of the namespace.
+ /// Cancellation token.
+ /// The namespace.
+ public async Task GetNamespaceAsync(string name, CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNamespaceAsync(name, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a node by name.
+ ///
+ /// The name of the node.
+ /// Cancellation token.
+ /// The node.
+ public async Task GetNodeAsync(string name, CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNodeAsync(name, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a config map by name in a namespace.
+ ///
+ /// The name of the config map.
+ /// The namespace of the config map. Defaults to "default".
+ /// Cancellation token.
+ /// The config map.
+ public async Task GetConfigMapAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNamespacedConfigMapAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get a secret by name in a namespace.
+ ///
+ /// The name of the secret.
+ /// The namespace of the secret. Defaults to "default".
+ /// Cancellation token.
+ /// The secret.
+ public async Task GetSecretAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ {
+ return await client.CoreV1.ReadNamespacedSecretAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
new file mode 100644
index 000000000..4f5839c4e
--- /dev/null
+++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
@@ -0,0 +1,81 @@
+using k8s.Models;
+
+namespace k8s.kubectl.beta;
+
+public partial class Kubectl
+{
+ ///
+ /// Get a pod by name in a namespace.
+ ///
+ /// The name of the pod.
+ /// The namespace of the pod. Defaults to "default".
+ /// The pod.
+ public V1Pod GetPod(string name, string @namespace = "default")
+ {
+ return client.GetPodAsync(name, @namespace).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a deployment by name in a namespace.
+ ///
+ /// The name of the deployment.
+ /// The namespace of the deployment. Defaults to "default".
+ /// The deployment.
+ public V1Deployment GetDeployment(string name, string @namespace = "default")
+ {
+ return client.GetDeploymentAsync(name, @namespace).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a service by name in a namespace.
+ ///
+ /// The name of the service.
+ /// The namespace of the service. Defaults to "default".
+ /// The service.
+ public V1Service GetService(string name, string @namespace = "default")
+ {
+ return client.GetServiceAsync(name, @namespace).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a namespace by name.
+ ///
+ /// The name of the namespace.
+ /// The namespace.
+ public V1Namespace GetNamespace(string name)
+ {
+ return client.GetNamespaceAsync(name).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a node by name.
+ ///
+ /// The name of the node.
+ /// The node.
+ public V1Node GetNode(string name)
+ {
+ return client.GetNodeAsync(name).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a config map by name in a namespace.
+ ///
+ /// The name of the config map.
+ /// The namespace of the config map. Defaults to "default".
+ /// The config map.
+ public V1ConfigMap GetConfigMap(string name, string @namespace = "default")
+ {
+ return client.GetConfigMapAsync(name, @namespace).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Get a secret by name in a namespace.
+ ///
+ /// The name of the secret.
+ /// The namespace of the secret. Defaults to "default".
+ /// The secret.
+ public V1Secret GetSecret(string name, string @namespace = "default")
+ {
+ return client.GetSecretAsync(name, @namespace).GetAwaiter().GetResult();
+ }
+}
diff --git a/tests/Kubectl.Tests/KubectlTests.Get.cs b/tests/Kubectl.Tests/KubectlTests.Get.cs
new file mode 100644
index 000000000..5faebf17d
--- /dev/null
+++ b/tests/Kubectl.Tests/KubectlTests.Get.cs
@@ -0,0 +1,168 @@
+using k8s.E2E;
+using k8s.kubectl.beta;
+using k8s.Models;
+using Xunit;
+
+namespace k8s.kubectl.Tests;
+
+public partial class KubectlTests
+{
+ [MinikubeFact]
+ public void GetNamespace()
+ {
+ using var kubernetes = MinikubeTests.CreateClient();
+ var client = new Kubectl(kubernetes);
+
+ // Get the default namespace
+ var ns = client.GetNamespace("default");
+
+ Assert.NotNull(ns);
+ Assert.Equal("default", ns.Metadata.Name);
+ Assert.Equal("v1", ns.ApiVersion);
+ Assert.Equal("Namespace", ns.Kind);
+ }
+
+ [MinikubeFact]
+ public void GetPod()
+ {
+ using var kubernetes = MinikubeTests.CreateClient();
+ var client = new Kubectl(kubernetes);
+ var namespaceParameter = "default";
+ var podName = "k8scsharp-e2e-get-pod";
+
+ // Cleanup any existing pod
+ try
+ {
+ kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter);
+ System.Threading.Thread.Sleep(2000);
+ }
+ catch
+ {
+ // Ignore if pod doesn't exist
+ }
+
+ // Create a test pod
+ var pod = new V1Pod
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = podName,
+ NamespaceProperty = namespaceParameter,
+ },
+ Spec = new V1PodSpec
+ {
+ Containers = new[]
+ {
+ new V1Container
+ {
+ Name = "test",
+ Image = "nginx:latest",
+ },
+ },
+ },
+ };
+
+ kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter);
+
+ try
+ {
+ // Wait a moment for the pod to be created
+ System.Threading.Thread.Sleep(1000);
+
+ // Get the pod using kubectl
+ var retrievedPod = client.GetPod(podName, namespaceParameter);
+
+ Assert.NotNull(retrievedPod);
+ Assert.Equal(podName, retrievedPod.Metadata.Name);
+ Assert.Equal(namespaceParameter, retrievedPod.Metadata.NamespaceProperty);
+ Assert.Equal("Pod", retrievedPod.Kind);
+ Assert.Equal("v1", retrievedPod.ApiVersion);
+ }
+ finally
+ {
+ // Cleanup
+ try
+ {
+ kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
+ [MinikubeFact]
+ public void GetService()
+ {
+ using var kubernetes = MinikubeTests.CreateClient();
+ var client = new Kubectl(kubernetes);
+ var namespaceParameter = "default";
+ var serviceName = "k8scsharp-e2e-get-service";
+
+ // Cleanup any existing service
+ try
+ {
+ kubernetes.CoreV1.DeleteNamespacedService(serviceName, namespaceParameter);
+ System.Threading.Thread.Sleep(1000);
+ }
+ catch
+ {
+ // Ignore if service doesn't exist
+ }
+
+ // Create a test service
+ var service = new V1Service
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = serviceName,
+ NamespaceProperty = namespaceParameter,
+ },
+ Spec = new V1ServiceSpec
+ {
+ Ports = new[]
+ {
+ new V1ServicePort
+ {
+ Port = 80,
+ TargetPort = 80,
+ },
+ },
+ Selector = new Dictionary
+ {
+ { "app", "test" },
+ },
+ },
+ };
+
+ kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter);
+
+ try
+ {
+ // Wait a moment for the service to be created
+ System.Threading.Thread.Sleep(1000);
+
+ // Get the service using kubectl
+ var retrievedService = client.GetService(serviceName, namespaceParameter);
+
+ Assert.NotNull(retrievedService);
+ Assert.Equal(serviceName, retrievedService.Metadata.Name);
+ Assert.Equal(namespaceParameter, retrievedService.Metadata.NamespaceProperty);
+ Assert.Equal("Service", retrievedService.Kind);
+ Assert.Equal("v1", retrievedService.ApiVersion);
+ }
+ finally
+ {
+ // Cleanup
+ try
+ {
+ kubernetes.CoreV1.DeleteNamespacedService(serviceName, namespaceParameter);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+}
From 218395aa6c5c5085d20a55934f4ffb7c262dd3d3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 01:01:34 +0000
Subject: [PATCH 3/8] Address code review feedback and improve tests
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
---
tests/Kubectl.Tests/KubectlTests.Get.cs | 32 ++-----------------------
1 file changed, 2 insertions(+), 30 deletions(-)
diff --git a/tests/Kubectl.Tests/KubectlTests.Get.cs b/tests/Kubectl.Tests/KubectlTests.Get.cs
index 5faebf17d..256121648 100644
--- a/tests/Kubectl.Tests/KubectlTests.Get.cs
+++ b/tests/Kubectl.Tests/KubectlTests.Get.cs
@@ -30,17 +30,6 @@ public void GetPod()
var namespaceParameter = "default";
var podName = "k8scsharp-e2e-get-pod";
- // Cleanup any existing pod
- try
- {
- kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter);
- System.Threading.Thread.Sleep(2000);
- }
- catch
- {
- // Ignore if pod doesn't exist
- }
-
// Create a test pod
var pod = new V1Pod
{
@@ -62,12 +51,9 @@ public void GetPod()
},
};
- kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter);
-
try
{
- // Wait a moment for the pod to be created
- System.Threading.Thread.Sleep(1000);
+ kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter);
// Get the pod using kubectl
var retrievedPod = client.GetPod(podName, namespaceParameter);
@@ -100,17 +86,6 @@ public void GetService()
var namespaceParameter = "default";
var serviceName = "k8scsharp-e2e-get-service";
- // Cleanup any existing service
- try
- {
- kubernetes.CoreV1.DeleteNamespacedService(serviceName, namespaceParameter);
- System.Threading.Thread.Sleep(1000);
- }
- catch
- {
- // Ignore if service doesn't exist
- }
-
// Create a test service
var service = new V1Service
{
@@ -136,12 +111,9 @@ public void GetService()
},
};
- kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter);
-
try
{
- // Wait a moment for the service to be created
- System.Threading.Thread.Sleep(1000);
+ kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter);
// Get the service using kubectl
var retrievedService = client.GetService(serviceName, namespaceParameter);
From 2147fb42333ae212765cbbfb28356da815dd7b01 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 19:07:50 +0000
Subject: [PATCH 4/8] Refactor to use generic Get method with GenericClient
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
---
.../Beta/AsyncKubectl.Get.cs | 96 ++++---------------
.../Beta/Kubectl.Get.cs | 80 ++--------------
tests/Kubectl.Tests/KubectlTests.Get.cs | 88 +++++++++++++++--
3 files changed, 108 insertions(+), 156 deletions(-)
diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
index 62a2e33fe..bf290f8a1 100644
--- a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
+++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
@@ -1,88 +1,28 @@
-using k8s.Models;
-
namespace k8s.kubectl.beta;
public partial class AsyncKubectl
{
///
- /// Get a pod by name in a namespace.
- ///
- /// The name of the pod.
- /// The namespace of the pod. Defaults to "default".
- /// Cancellation token.
- /// The pod.
- public async Task GetPodAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
- {
- return await client.CoreV1.ReadNamespacedPodAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a deployment by name in a namespace.
- ///
- /// The name of the deployment.
- /// The namespace of the deployment. Defaults to "default".
- /// Cancellation token.
- /// The deployment.
- public async Task GetDeploymentAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
- {
- return await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a service by name in a namespace.
- ///
- /// The name of the service.
- /// The namespace of the service. Defaults to "default".
- /// Cancellation token.
- /// The service.
- public async Task GetServiceAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
- {
- return await client.CoreV1.ReadNamespacedServiceAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a namespace by name.
- ///
- /// The name of the namespace.
- /// Cancellation token.
- /// The namespace.
- public async Task GetNamespaceAsync(string name, CancellationToken cancellationToken = default)
- {
- return await client.CoreV1.ReadNamespaceAsync(name, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a node by name.
- ///
- /// The name of the node.
- /// Cancellation token.
- /// The node.
- public async Task GetNodeAsync(string name, CancellationToken cancellationToken = default)
- {
- return await client.CoreV1.ReadNodeAsync(name, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a config map by name in a namespace.
- ///
- /// The name of the config map.
- /// The namespace of the config map. Defaults to "default".
- /// Cancellation token.
- /// The config map.
- public async Task GetConfigMapAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
- {
- return await client.CoreV1.ReadNamespacedConfigMapAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Get a secret by name in a namespace.
+ /// Get a Kubernetes resource by name.
///
- /// The name of the secret.
- /// The namespace of the secret. Defaults to "default".
+ /// The type of Kubernetes resource to get.
+ /// The name of the resource.
+ /// The namespace of the resource (for namespaced resources). Optional.
/// Cancellation token.
- /// The secret.
- public async Task GetSecretAsync(string name, string @namespace = "default", CancellationToken cancellationToken = default)
+ /// The requested resource.
+ public async Task GetAsync(string name, string? @namespace = null, CancellationToken cancellationToken = default)
+ where T : IKubernetesObject
{
- return await client.CoreV1.ReadNamespacedSecretAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var metadata = typeof(T).GetKubernetesTypeMetadata();
+ var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false);
+
+ if (@namespace != null)
+ {
+ return await genericClient.ReadNamespacedAsync(@namespace, name, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ return await genericClient.ReadAsync(name, cancellationToken).ConfigureAwait(false);
+ }
}
}
diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
index 4f5839c4e..dda5acba4 100644
--- a/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
+++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs
@@ -1,81 +1,17 @@
-using k8s.Models;
-
namespace k8s.kubectl.beta;
public partial class Kubectl
{
///
- /// Get a pod by name in a namespace.
- ///
- /// The name of the pod.
- /// The namespace of the pod. Defaults to "default".
- /// The pod.
- public V1Pod GetPod(string name, string @namespace = "default")
- {
- return client.GetPodAsync(name, @namespace).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a deployment by name in a namespace.
- ///
- /// The name of the deployment.
- /// The namespace of the deployment. Defaults to "default".
- /// The deployment.
- public V1Deployment GetDeployment(string name, string @namespace = "default")
- {
- return client.GetDeploymentAsync(name, @namespace).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a service by name in a namespace.
- ///
- /// The name of the service.
- /// The namespace of the service. Defaults to "default".
- /// The service.
- public V1Service GetService(string name, string @namespace = "default")
- {
- return client.GetServiceAsync(name, @namespace).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a namespace by name.
- ///
- /// The name of the namespace.
- /// The namespace.
- public V1Namespace GetNamespace(string name)
- {
- return client.GetNamespaceAsync(name).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a node by name.
- ///
- /// The name of the node.
- /// The node.
- public V1Node GetNode(string name)
- {
- return client.GetNodeAsync(name).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a config map by name in a namespace.
- ///
- /// The name of the config map.
- /// The namespace of the config map. Defaults to "default".
- /// The config map.
- public V1ConfigMap GetConfigMap(string name, string @namespace = "default")
- {
- return client.GetConfigMapAsync(name, @namespace).GetAwaiter().GetResult();
- }
-
- ///
- /// Get a secret by name in a namespace.
+ /// Get a Kubernetes resource by name.
///
- /// The name of the secret.
- /// The namespace of the secret. Defaults to "default".
- /// The secret.
- public V1Secret GetSecret(string name, string @namespace = "default")
+ /// The type of Kubernetes resource to get.
+ /// The name of the resource.
+ /// The namespace of the resource (for namespaced resources). Optional.
+ /// The requested resource.
+ public T Get(string name, string? @namespace = null)
+ where T : IKubernetesObject
{
- return client.GetSecretAsync(name, @namespace).GetAwaiter().GetResult();
+ return client.GetAsync(name, @namespace).GetAwaiter().GetResult();
}
}
diff --git a/tests/Kubectl.Tests/KubectlTests.Get.cs b/tests/Kubectl.Tests/KubectlTests.Get.cs
index 256121648..f2114f0f5 100644
--- a/tests/Kubectl.Tests/KubectlTests.Get.cs
+++ b/tests/Kubectl.Tests/KubectlTests.Get.cs
@@ -13,8 +13,8 @@ public void GetNamespace()
using var kubernetes = MinikubeTests.CreateClient();
var client = new Kubectl(kubernetes);
- // Get the default namespace
- var ns = client.GetNamespace("default");
+ // Get the default namespace (cluster-scoped resource, no namespace parameter)
+ var ns = client.Get("default");
Assert.NotNull(ns);
Assert.Equal("default", ns.Metadata.Name);
@@ -55,8 +55,8 @@ public void GetPod()
{
kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter);
- // Get the pod using kubectl
- var retrievedPod = client.GetPod(podName, namespaceParameter);
+ // Get the pod using kubectl generic get
+ var retrievedPod = client.Get(podName, namespaceParameter);
Assert.NotNull(retrievedPod);
Assert.Equal(podName, retrievedPod.Metadata.Name);
@@ -115,8 +115,8 @@ public void GetService()
{
kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter);
- // Get the service using kubectl
- var retrievedService = client.GetService(serviceName, namespaceParameter);
+ // Get the service using kubectl generic get
+ var retrievedService = client.Get(serviceName, namespaceParameter);
Assert.NotNull(retrievedService);
Assert.Equal(serviceName, retrievedService.Metadata.Name);
@@ -137,4 +137,80 @@ public void GetService()
}
}
}
+
+ [MinikubeFact]
+ public void GetDeployment()
+ {
+ using var kubernetes = MinikubeTests.CreateClient();
+ var client = new Kubectl(kubernetes);
+ var namespaceParameter = "default";
+ var deploymentName = "k8scsharp-e2e-get-deployment";
+
+ // Create a test deployment
+ var deployment = new V1Deployment
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = deploymentName,
+ NamespaceProperty = namespaceParameter,
+ },
+ Spec = new V1DeploymentSpec
+ {
+ Replicas = 1,
+ Selector = new V1LabelSelector
+ {
+ MatchLabels = new Dictionary
+ {
+ { "app", "test" },
+ },
+ },
+ Template = new V1PodTemplateSpec
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Labels = new Dictionary
+ {
+ { "app", "test" },
+ },
+ },
+ Spec = new V1PodSpec
+ {
+ Containers = new[]
+ {
+ new V1Container
+ {
+ Name = "test",
+ Image = "nginx:latest",
+ },
+ },
+ },
+ },
+ },
+ };
+
+ try
+ {
+ kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter);
+
+ // Get the deployment using kubectl generic get
+ var retrievedDeployment = client.Get(deploymentName, namespaceParameter);
+
+ Assert.NotNull(retrievedDeployment);
+ Assert.Equal(deploymentName, retrievedDeployment.Metadata.Name);
+ Assert.Equal(namespaceParameter, retrievedDeployment.Metadata.NamespaceProperty);
+ Assert.Equal("Deployment", retrievedDeployment.Kind);
+ }
+ finally
+ {
+ // Cleanup
+ try
+ {
+ kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
}
From f0dbebba51a00febb11fe85c0992a445857e8c60 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 08:09:57 +0000
Subject: [PATCH 5/8] Address code review feedback: dispose GenericClient, use
ternary operator, specific exception handling
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
---
.../Beta/AsyncKubectl.Get.cs | 13 ++++---------
tests/Kubectl.Tests/KubectlTests.Get.cs | 13 +++++++------
2 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
index bf290f8a1..a78f1cc67 100644
--- a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
+++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs
@@ -14,15 +14,10 @@ public async Task GetAsync(string name, string? @namespace = null, Cancell
where T : IKubernetesObject
{
var metadata = typeof(T).GetKubernetesTypeMetadata();
- var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false);
+ using var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false);
- if (@namespace != null)
- {
- return await genericClient.ReadNamespacedAsync(@namespace, name, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- return await genericClient.ReadAsync(name, cancellationToken).ConfigureAwait(false);
- }
+ return @namespace != null
+ ? await genericClient.ReadNamespacedAsync(@namespace, name, cancellationToken).ConfigureAwait(false)
+ : await genericClient.ReadAsync(name, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/tests/Kubectl.Tests/KubectlTests.Get.cs b/tests/Kubectl.Tests/KubectlTests.Get.cs
index f2114f0f5..4782a0179 100644
--- a/tests/Kubectl.Tests/KubectlTests.Get.cs
+++ b/tests/Kubectl.Tests/KubectlTests.Get.cs
@@ -1,3 +1,4 @@
+using k8s.Autorest;
using k8s.E2E;
using k8s.kubectl.beta;
using k8s.Models;
@@ -71,9 +72,9 @@ public void GetPod()
{
kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter);
}
- catch
+ catch (HttpOperationException)
{
- // Ignore cleanup errors
+ // Ignore cleanup errors if pod was already deleted or doesn't exist
}
}
}
@@ -131,9 +132,9 @@ public void GetService()
{
kubernetes.CoreV1.DeleteNamespacedService(serviceName, namespaceParameter);
}
- catch
+ catch (HttpOperationException)
{
- // Ignore cleanup errors
+ // Ignore cleanup errors if service was already deleted or doesn't exist
}
}
}
@@ -207,9 +208,9 @@ public void GetDeployment()
{
kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter);
}
- catch
+ catch (HttpOperationException)
{
- // Ignore cleanup errors
+ // Ignore cleanup errors if deployment was already deleted or doesn't exist
}
}
}
From b4aeeed7bcf40cd362845bfd1c74962d65781046 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 18 Nov 2025 02:05:39 -0800
Subject: [PATCH 6/8] Fix datetime serialization to always output 6 decimal
places for microseconds (#1687)
* Initial plan
* Fix datetime serialization to always output 6 decimal places for fractional seconds
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
---
src/KubernetesClient/KubernetesJson.cs | 20 +++++++++-----
.../KubernetesJsonTests.cs | 27 +++++++++++++++++++
2 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/src/KubernetesClient/KubernetesJson.cs b/src/KubernetesClient/KubernetesJson.cs
index 69ecdf43e..35cfe3acb 100644
--- a/src/KubernetesClient/KubernetesJson.cs
+++ b/src/KubernetesClient/KubernetesJson.cs
@@ -60,12 +60,20 @@ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSeri
// Output as RFC3339Micro
var date = value.ToUniversalTime();
- var basePart = date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
- var frac = date.ToString(".ffffff", CultureInfo.InvariantCulture)
- .TrimEnd('0')
- .TrimEnd('.');
-
- writer.WriteStringValue(basePart + frac + "Z");
+ // Check if there are any fractional seconds
+ var ticks = date.Ticks % TimeSpan.TicksPerSecond;
+ if (ticks == 0)
+ {
+ // No fractional seconds - use format without fractional part
+ var basePart = date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
+ writer.WriteStringValue(basePart + "Z");
+ }
+ else
+ {
+ // Has fractional seconds - always use exactly 6 decimal places
+ var formatted = date.ToString(RFC3339MicroFormat, CultureInfo.InvariantCulture);
+ writer.WriteStringValue(formatted);
+ }
}
}
diff --git a/tests/KubernetesClient.Tests/KubernetesJsonTests.cs b/tests/KubernetesClient.Tests/KubernetesJsonTests.cs
index 785e38fea..e8f199456 100644
--- a/tests/KubernetesClient.Tests/KubernetesJsonTests.cs
+++ b/tests/KubernetesClient.Tests/KubernetesJsonTests.cs
@@ -142,4 +142,31 @@ public void ReadWriteDatesJson()
Assert.Equal(kManifest, jsonFromObj2);
}
+
+ [Fact]
+ public void DateTimeWithFractionalSecondsAlwaysHasSixDigits()
+ {
+ // Test that datetime fields with fractional seconds always output exactly 6 decimal places
+ // This is required by Kubernetes API which expects RFC3339Micro format
+
+ // Create a datetime with 5 digits of precision (962170 microseconds = .96217 seconds)
+ var dt = new DateTime(2025, 11, 17, 22, 52, 34, 962, DateTimeKind.Utc).AddTicks(1700);
+
+ var secret = new V1Secret
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = "test-secret",
+ CreationTimestamp = dt,
+ },
+ };
+
+ var json = KubernetesJson.Serialize(secret);
+
+ // Verify the datetime is serialized with exactly 6 decimal places
+ Assert.Contains("2025-11-17T22:52:34.962170Z", json);
+
+ // Also verify it doesn't have 5 digits (which would fail in Kubernetes)
+ Assert.DoesNotContain("2025-11-17T22:52:34.96217Z", json);
+ }
}
From 5e7ec12a24cc164c6a9a8dd404874e3d4e8ceb2f Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 18 Nov 2025 18:47:58 -0800
Subject: [PATCH 7/8] Add .NET 10 target framework support (#1686)
* Initial plan
* Add .NET 10 support to all projects and workflows
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
* Fix datetime serialization to always output 6 decimal places for microseconds (#1687)
* Initial plan
* Fix datetime serialization to always output 6 decimal places for fractional seconds
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
* Initial plan
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tg123 <170430+tg123@users.noreply.github.com>
---
.github/workflows/buildtest.yaml | 4 +++-
.github/workflows/codeql-analysis.yml | 1 +
.github/workflows/docfx.yaml | 1 +
.github/workflows/draft.yaml | 1 +
.github/workflows/nuget.yaml | 1 +
README.md | 2 +-
src/KubernetesClient.Aot/KubernetesClient.Aot.csproj | 2 +-
src/KubernetesClient.Kubectl/KubernetesClient.Kubectl.csproj | 2 +-
src/KubernetesClient/KubernetesClient.csproj | 2 +-
tests/E2E.Tests/E2E.Tests.csproj | 2 +-
tests/Kubectl.Tests/Kubectl.Tests.csproj | 2 +-
.../KubernetesClient.Classic.Tests.csproj | 4 ++--
tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj | 2 +-
13 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml
index 26d585ec7..2935bd0a9 100644
--- a/.github/workflows/buildtest.yaml
+++ b/.github/workflows/buildtest.yaml
@@ -17,6 +17,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Build
run: dotnet build --configuration Release
- name: Test
@@ -46,7 +47,7 @@ jobs:
- name: Setup dotnet SDK
uses: actions/setup-dotnet@v5
with:
- dotnet-version: '9.0.x'
+ dotnet-version: '10.0.x'
- name: Restore nugets (msbuild)
run: msbuild .\src\KubernetesClient\ -t:restore -p:RestorePackagesConfig=true
- name: Build (msbuild)
@@ -64,6 +65,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Minikube
run: minikube start
- name: Test
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 6355396eb..a277f3de6 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -36,6 +36,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/docfx.yaml b/.github/workflows/docfx.yaml
index 3eec06ec3..e453af103 100644
--- a/.github/workflows/docfx.yaml
+++ b/.github/workflows/docfx.yaml
@@ -35,6 +35,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Build
run: dotnet build -c Release
diff --git a/.github/workflows/draft.yaml b/.github/workflows/draft.yaml
index 01b098518..4d273fb70 100644
--- a/.github/workflows/draft.yaml
+++ b/.github/workflows/draft.yaml
@@ -23,6 +23,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: dotnet restore
run: dotnet restore --verbosity minimal --configfile nuget.config
diff --git a/.github/workflows/nuget.yaml b/.github/workflows/nuget.yaml
index fa654822f..ae1709133 100644
--- a/.github/workflows/nuget.yaml
+++ b/.github/workflows/nuget.yaml
@@ -20,6 +20,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: dotnet restore
run: dotnet restore --verbosity minimal --configfile nuget.config
diff --git a/README.md b/README.md
index c8eb91626..87c464814 100644
--- a/README.md
+++ b/README.md
@@ -154,7 +154,7 @@ ${GEN_DIR}/openapi/csharp.sh ${REPO_DIR}/src/KubernetesClient ${REPO_DIR}/csharp
| SDK Version | Kubernetes Version | .NET Targeting |
|-------------|--------------------|-----------------------------------------------------|
-| 18.0 | 1.34 | net8.0;net9.0;net48*;netstandard2.0* |
+| 18.0 | 1.34 | net8.0;net9.0;net10.0;net48*;netstandard2.0* |
| 17.0 | 1.33 | net8.0;net9.0;net48*;netstandard2.0* |
| 16.0 | 1.32 | net8.0;net9.0;net48*;netstandard2.0* |
| 15.0 | 1.31 | net6.0;net8.0;net48*;netstandard2.0* |
diff --git a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj
index 5c7cf8fed..3db7e6139 100644
--- a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj
+++ b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj
@@ -1,7 +1,7 @@
- net8.0;net9.0
+ net8.0;net9.0;net10.0
k8s
true
true
diff --git a/src/KubernetesClient.Kubectl/KubernetesClient.Kubectl.csproj b/src/KubernetesClient.Kubectl/KubernetesClient.Kubectl.csproj
index 25440a1f7..53bce8543 100644
--- a/src/KubernetesClient.Kubectl/KubernetesClient.Kubectl.csproj
+++ b/src/KubernetesClient.Kubectl/KubernetesClient.Kubectl.csproj
@@ -1,7 +1,7 @@
- net8.0;net9.0
+ net8.0;net9.0;net10.0
enable
enable
k8s.kubectl
diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj
index dba319136..acd7c079e 100644
--- a/src/KubernetesClient/KubernetesClient.csproj
+++ b/src/KubernetesClient/KubernetesClient.csproj
@@ -1,7 +1,7 @@
- net8.0;net9.0
+ net8.0;net9.0;net10.0
k8s
true
diff --git a/tests/E2E.Tests/E2E.Tests.csproj b/tests/E2E.Tests/E2E.Tests.csproj
index 2c4da8d5e..9b2e278e1 100644
--- a/tests/E2E.Tests/E2E.Tests.csproj
+++ b/tests/E2E.Tests/E2E.Tests.csproj
@@ -2,7 +2,7 @@
false
k8s.E2E
- net8.0;net9.0
+ net8.0;net9.0;net10.0
diff --git a/tests/Kubectl.Tests/Kubectl.Tests.csproj b/tests/Kubectl.Tests/Kubectl.Tests.csproj
index b00ac0151..16eaa3abb 100644
--- a/tests/Kubectl.Tests/Kubectl.Tests.csproj
+++ b/tests/Kubectl.Tests/Kubectl.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0;net9.0
+ net8.0;net9.0;net10.0
enable
enable
false
diff --git a/tests/KubernetesClient.Classic.Tests/KubernetesClient.Classic.Tests.csproj b/tests/KubernetesClient.Classic.Tests/KubernetesClient.Classic.Tests.csproj
index 52810130e..c3fb239da 100644
--- a/tests/KubernetesClient.Classic.Tests/KubernetesClient.Classic.Tests.csproj
+++ b/tests/KubernetesClient.Classic.Tests/KubernetesClient.Classic.Tests.csproj
@@ -2,8 +2,8 @@
false
k8s.Tests
- net8.0;net9.0
- net8.0;net9.0;net48
+ net8.0;net9.0;net10.0
+ net8.0;net9.0;net10.0;net48
diff --git a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
index b6a22afaf..8492b6760 100644
--- a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
+++ b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
@@ -2,7 +2,7 @@
false
k8s.Tests
- net8.0;net9.0
+ net8.0;net9.0;net10.0
From e5b49954687b5a5f71db29cd3a2cfe38c9f9ca03 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 00:45:46 +0000
Subject: [PATCH 8/8] Initial plan