-
Notifications
You must be signed in to change notification settings - Fork 307
Implement Kubectl Get using generic client for any Kubernetes resource #1685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
29da827
a4a5fc2
218395a
2147fb4
f0dbebb
b4aeeed
5e7ec12
e5b4995
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||||
| namespace k8s.kubectl.beta; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public partial class AsyncKubectl | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||
| /// Get a Kubernetes resource by name. | ||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||
| /// <typeparam name="T">The type of Kubernetes resource to get.</typeparam> | ||||||||||||||||||||||||
| /// <param name="name">The name of the resource.</param> | ||||||||||||||||||||||||
| /// <param name="namespace">The namespace of the resource (for namespaced resources). Optional.</param> | ||||||||||||||||||||||||
| /// <param name="cancellationToken">Cancellation token.</param> | ||||||||||||||||||||||||
| /// <returns>The requested resource.</returns> | ||||||||||||||||||||||||
| public async Task<T> GetAsync<T>(string name, string? @namespace = null, CancellationToken cancellationToken = default) | ||||||||||||||||||||||||
| where T : IKubernetesObject | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| 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<T>(@namespace, name, cancellationToken).ConfigureAwait(false); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| return await genericClient.ReadAsync<T>(name, cancellationToken).ConfigureAwait(false); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| if (@namespace != null) | |
| { | |
| return await genericClient.ReadNamespacedAsync<T>(@namespace, name, cancellationToken).ConfigureAwait(false); | |
| } | |
| else | |
| { | |
| return await genericClient.ReadAsync<T>(name, cancellationToken).ConfigureAwait(false); | |
| } | |
| return @namespace != null | |
| ? await genericClient.ReadNamespacedAsync<T>(@namespace, name, cancellationToken).ConfigureAwait(false) | |
| : await genericClient.ReadAsync<T>(name, cancellationToken).ConfigureAwait(false); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| namespace k8s.kubectl.beta; | ||
|
|
||
| public partial class Kubectl | ||
| { | ||
| /// <summary> | ||
| /// Get a Kubernetes resource by name. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of Kubernetes resource to get.</typeparam> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="namespace">The namespace of the resource (for namespaced resources). Optional.</param> | ||
| /// <returns>The requested resource.</returns> | ||
| public T Get<T>(string name, string? @namespace = null) | ||
| where T : IKubernetesObject | ||
| { | ||
| return client.GetAsync<T>(name, @namespace).GetAwaiter().GetResult(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,216 @@ | ||||||||||||||||
| 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 (cluster-scoped resource, no namespace parameter) | ||||||||||||||||
| var ns = client.Get<V1Namespace>("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"; | ||||||||||||||||
|
|
||||||||||||||||
| // 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); | ||||||||||||||||
|
|
||||||||||||||||
| // Get the pod using kubectl generic get | ||||||||||||||||
| var retrievedPod = client.Get<V1Pod>(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"; | ||||||||||||||||
|
|
||||||||||||||||
| // 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<string, string> | ||||||||||||||||
| { | ||||||||||||||||
| { "app", "test" }, | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| try | ||||||||||||||||
| { | ||||||||||||||||
| kubernetes.CoreV1.CreateNamespacedService(service, namespaceParameter); | ||||||||||||||||
|
|
||||||||||||||||
| // Get the service using kubectl generic get | ||||||||||||||||
| var retrievedService = client.Get<V1Service>(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 | ||||||||||||||||
| } | ||||||||||||||||
|
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [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<string, string> | ||||||||||||||||
| { | ||||||||||||||||
| { "app", "test" }, | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| Template = new V1PodTemplateSpec | ||||||||||||||||
| { | ||||||||||||||||
| Metadata = new V1ObjectMeta | ||||||||||||||||
| { | ||||||||||||||||
| Labels = new Dictionary<string, string> | ||||||||||||||||
| { | ||||||||||||||||
| { "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<V1Deployment>(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 | ||||||||||||||||
|
||||||||||||||||
| catch | |
| { | |
| // Ignore cleanup errors | |
| catch (Exception ex) | |
| { | |
| // Ignore cleanup errors, but log for visibility | |
| Console.WriteLine($"Cleanup error in DeleteNamespacedDeployment: {ex}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
GenericClientinstance is not disposed after use. WhiledisposeClient: falseprevents disposal of the underlying Kubernetes client, the GenericClient itself should be disposed to follow proper IDisposable patterns. Consider wrapping it in a using statement or calling Dispose() after the Read operation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GenericClient disposal issue has been addressed. Added
using varstatement on line 17 of AsyncKubectl.Get.cs to properly dispose the GenericClient instance.Additionally applied other code review suggestions:
Commit: f0dbebb