diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs index a78f1cc6..bed8ee36 100644 --- a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs +++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Get.cs @@ -20,4 +20,32 @@ public async Task GetAsync(string name, string? @namespace = null, Cancell ? await genericClient.ReadNamespacedAsync(@namespace, name, cancellationToken).ConfigureAwait(false) : await genericClient.ReadAsync(name, cancellationToken).ConfigureAwait(false); } + + /// + /// List Kubernetes resources of a specific type. + /// + /// The type of Kubernetes resource list to get (e.g., V1PodList). + /// The namespace to list resources from. If null, lists cluster-scoped resources or resources across all namespaces for namespaced resources. + /// A selector to restrict the list of returned objects by their labels. Defaults to everything. + /// A selector to restrict the list of returned objects by their fields. Defaults to everything. + /// Maximum number of responses to return for a list call. + /// The continue option should be set when retrieving more results from the server. + /// Cancellation token. + /// The list of requested resources. + public async Task ListAsync( + string? @namespace = null, + string? labelSelector = null, + string? fieldSelector = null, + int? limit = null, + string? continueToken = null, + CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + var metadata = typeof(T).GetKubernetesTypeMetadata(); + using var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false); + + return @namespace != null + ? await genericClient.ListNamespacedAsync(@namespace, labelSelector, fieldSelector, limit, continueToken, cancellationToken).ConfigureAwait(false) + : await genericClient.ListAsync(labelSelector, fieldSelector, limit, continueToken, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs index dda5acba..96b0299c 100644 --- a/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs +++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Get.cs @@ -14,4 +14,25 @@ public T Get(string name, string? @namespace = null) { return client.GetAsync(name, @namespace).GetAwaiter().GetResult(); } + + /// + /// List Kubernetes resources of a specific type. + /// + /// The type of Kubernetes resource list to get (e.g., V1PodList). + /// The namespace to list resources from. If null, lists cluster-scoped resources or resources across all namespaces for namespaced resources. + /// A selector to restrict the list of returned objects by their labels. Defaults to everything. + /// A selector to restrict the list of returned objects by their fields. Defaults to everything. + /// Maximum number of responses to return for a list call. + /// The continue option should be set when retrieving more results from the server. + /// The list of requested resources. + public T List( + string? @namespace = null, + string? labelSelector = null, + string? fieldSelector = null, + int? limit = null, + string? continueToken = null) + where T : IKubernetesObject + { + return client.ListAsync(@namespace, labelSelector, fieldSelector, limit, continueToken).GetAwaiter().GetResult(); + } } diff --git a/src/KubernetesClient/GenericClient.cs b/src/KubernetesClient/GenericClient.cs index a250aad2..9e295c97 100644 --- a/src/KubernetesClient/GenericClient.cs +++ b/src/KubernetesClient/GenericClient.cs @@ -42,17 +42,17 @@ public async Task CreateNamespacedAsync(T obj, string ns, CancellationToke return resp.Body; } - public async Task ListAsync(CancellationToken cancel = default) + public async Task ListAsync(string labelSelector = null, string fieldSelector = null, int? limit = null, string continueToken = null, CancellationToken cancel = default) where T : IKubernetesObject { - var resp = await kubernetes.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync(group, version, plural, cancellationToken: cancel).ConfigureAwait(false); + var resp = await kubernetes.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync(group, version, plural, labelSelector: labelSelector, fieldSelector: fieldSelector, limit: limit, continueParameter: continueToken, cancellationToken: cancel).ConfigureAwait(false); return resp.Body; } - public async Task ListNamespacedAsync(string ns, CancellationToken cancel = default) + public async Task ListNamespacedAsync(string ns, string labelSelector = null, string fieldSelector = null, int? limit = null, string continueToken = null, CancellationToken cancel = default) where T : IKubernetesObject { - var resp = await kubernetes.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync(group, version, ns, plural, cancellationToken: cancel).ConfigureAwait(false); + var resp = await kubernetes.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync(group, version, ns, plural, labelSelector: labelSelector, fieldSelector: fieldSelector, limit: limit, continueParameter: continueToken, cancellationToken: cancel).ConfigureAwait(false); return resp.Body; } diff --git a/tests/Kubectl.Tests/KubectlTests.Get.cs b/tests/Kubectl.Tests/KubectlTests.Get.cs index 4782a017..48789850 100644 --- a/tests/Kubectl.Tests/KubectlTests.Get.cs +++ b/tests/Kubectl.Tests/KubectlTests.Get.cs @@ -214,4 +214,375 @@ public void GetDeployment() } } } + + [MinikubeFact] + public void ListNamespaces() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + + // List all namespaces (cluster-scoped resources) + var namespaces = client.List(); + + Assert.NotNull(namespaces); + Assert.NotNull(namespaces.Items); + Assert.Contains(namespaces.Items, ns => ns.Metadata.Name == "default"); + Assert.Contains(namespaces.Items, ns => ns.Metadata.Name == "kube-system"); + } + + [MinikubeFact] + public void ListPods() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var podName = "k8scsharp-e2e-list-pod"; + + // Create a test pod with a label + var pod = new V1Pod + { + Metadata = new V1ObjectMeta + { + Name = podName, + NamespaceProperty = namespaceParameter, + Labels = new Dictionary + { + { "app", "k8scsharp-e2e-test" }, + { "test-type", "list" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter); + + // List pods in the namespace + var pods = client.List(namespaceParameter); + + Assert.NotNull(pods); + Assert.NotNull(pods.Items); + Assert.Contains(pods.Items, p => p.Metadata.Name == podName); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors if pod was already deleted or doesn't exist + } + } + } + + [MinikubeFact] + public void ListPodsWithLabelSelector() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var podNameA = "k8scsharp-e2e-list-selector-a"; + var podNameB = "k8scsharp-e2e-list-selector-b"; + + // Create two test pods with different labels + var podA = new V1Pod + { + Metadata = new V1ObjectMeta + { + Name = podNameA, + NamespaceProperty = namespaceParameter, + Labels = new Dictionary + { + { "app", "k8scsharp-e2e-selector-test" }, + { "tier", "frontend" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }; + + var podB = new V1Pod + { + Metadata = new V1ObjectMeta + { + Name = podNameB, + NamespaceProperty = namespaceParameter, + Labels = new Dictionary + { + { "app", "k8scsharp-e2e-selector-test" }, + { "tier", "backend" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedPod(podA, namespaceParameter); + kubernetes.CoreV1.CreateNamespacedPod(podB, namespaceParameter); + + // List pods with label selector for frontend tier only + var frontendPods = client.List( + namespaceParameter, + labelSelector: "tier=frontend,app=k8scsharp-e2e-selector-test"); + + Assert.NotNull(frontendPods); + Assert.NotNull(frontendPods.Items); + Assert.Single(frontendPods.Items); + Assert.Equal(podNameA, frontendPods.Items[0].Metadata.Name); + + // List pods with label selector for app only (should return both) + var allTestPods = client.List( + namespaceParameter, + labelSelector: "app=k8scsharp-e2e-selector-test"); + + Assert.NotNull(allTestPods); + Assert.NotNull(allTestPods.Items); + Assert.Equal(2, allTestPods.Items.Count); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedPod(podNameA, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + + try + { + kubernetes.CoreV1.DeleteNamespacedPod(podNameB, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void ListPodsWithFieldSelector() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var podName = "k8scsharp-e2e-list-field"; + + // 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", + }, + }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedPod(pod, namespaceParameter); + + // List pods with field selector for specific metadata.name + var pods = client.List( + namespaceParameter, + fieldSelector: $"metadata.name={podName}"); + + Assert.NotNull(pods); + Assert.NotNull(pods.Items); + Assert.Single(pods.Items); + Assert.Equal(podName, pods.Items[0].Metadata.Name); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedPod(podName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void ListServicesInNamespace() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var serviceName = "k8scsharp-e2e-list-service"; + + // 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" }, + }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter); + + // List services in the namespace + var services = client.List(namespaceParameter); + + Assert.NotNull(services); + Assert.NotNull(services.Items); + Assert.Contains(services.Items, s => s.Metadata.Name == serviceName); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedService(serviceName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void ListDeployments() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var deploymentName = "k8scsharp-e2e-list-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); + + // List deployments in the namespace + var deployments = client.List(namespaceParameter); + + Assert.NotNull(deployments); + Assert.NotNull(deployments.Items); + Assert.Contains(deployments.Items, d => d.Metadata.Name == deploymentName); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } }