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/4] 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/4] 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/4] 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/4] 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 + } + } + } }