diff --git a/examples/GenericKubernetesApi/GenericKubernetesApi.csproj b/examples/GenericKubernetesApi/GenericKubernetesApi.csproj
new file mode 100644
index 0000000..d9f6862
--- /dev/null
+++ b/examples/GenericKubernetesApi/GenericKubernetesApi.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
diff --git a/examples/GenericKubernetesApi/Program.cs b/examples/GenericKubernetesApi/Program.cs
new file mode 100644
index 0000000..5a093fa
--- /dev/null
+++ b/examples/GenericKubernetesApi/Program.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using k8s;
+using k8s.Models;
+using k8s.Util.Common;
+using k8s.Util.Common.Generic;
+
+namespace GenericKubernetesApiExample
+{
+ public class Program
+ {
+ private static GenericKubernetesApi _genericKubernetesApi;
+
+ public static void Main(string[] args)
+ {
+ var config = KubernetesClientConfiguration.BuildDefaultConfig();
+ IKubernetes client = new Kubernetes(config);
+ var cts = new CancellationTokenSource();
+
+ _genericKubernetesApi = new GenericKubernetesApi(
+ apiGroup: "pod",
+ apiVersion: "v1",
+ resourcePlural: "pods",
+ apiClient: client);
+
+ var aPod = GetNamespacedPod(Namespaces.NamespaceDefault, "my-pod-name", cts.Token);
+ var aListOfPods = ListPodsInNamespace(Namespaces.NamespaceDefault, cts.Token);
+
+ // Watch for pod actions in a namespsace
+ using var watch = _genericKubernetesApi.Watch(
+ Namespaces.NamespaceDefault,
+ (eventType, pod) => { Console.WriteLine("The event {0} happened on pod named {1}", eventType, pod.Metadata.Name); },
+ exception => { Console.WriteLine("Oh no! An exception happened while watching pods. The message was '{0}'.", exception.Message); },
+ () => { Console.WriteLine("The server closed the connection."); });
+
+ Console.WriteLine("press ctrl + c to stop watching");
+
+ var ctrlc = new ManualResetEventSlim(false);
+ Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
+ ctrlc.Wait();
+ cts.Cancel();
+ }
+
+ private static V1Pod GetNamespacedPod(string @namespace, string podName, CancellationToken cancellationToken)
+ {
+ var resp = Task.Run(
+ async () => await _genericKubernetesApi.GetAsync(@namespace, podName, cancellationToken).ConfigureAwait(false), cancellationToken);
+
+ return resp.Result;
+ }
+
+ private static V1PodList ListPodsInNamespace(string @namespace, CancellationToken cancellationToken)
+ {
+ var resp = Task.Run(
+ async () => await _genericKubernetesApi.ListAsync(@namespace, cancellationToken).ConfigureAwait(false), cancellationToken);
+
+ return resp.Result;
+ }
+ }
+}
diff --git a/kubernetes-client.sln b/kubernetes-client.sln
index 88a107e..1acaff9 100644
--- a/kubernetes-client.sln
+++ b/kubernetes-client.sln
@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customResource", "examples\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubernetesGenerator", "gen\KubernetesGenerator\KubernetesGenerator.csproj", "{79BA7C4A-98AA-467E-80D4-0E4F03EE6DDE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericKubernetesApi", "examples\GenericKubernetesApi\GenericKubernetesApi.csproj", "{F81AE4C4-E044-4225-BD76-385A0DE621FD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -245,6 +247,18 @@ Global
{79BA7C4A-98AA-467E-80D4-0E4F03EE6DDE}.Release|x64.Build.0 = Release|Any CPU
{79BA7C4A-98AA-467E-80D4-0E4F03EE6DDE}.Release|x86.ActiveCfg = Release|Any CPU
{79BA7C4A-98AA-467E-80D4-0E4F03EE6DDE}.Release|x86.Build.0 = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|x64.Build.0 = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Debug|x86.Build.0 = Debug|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|x64.ActiveCfg = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|x64.Build.0 = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|x86.ActiveCfg = Release|Any CPU
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -266,6 +280,7 @@ Global
{4D2AE427-F856-49E5-B61D-EA6B17D89051} = {8AF4A5C2-F0CE-47D5-A4C5-FE4AB83CA509}
{95672061-5799-4454-ACDB-D6D330DB1EC4} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40}
{79BA7C4A-98AA-467E-80D4-0E4F03EE6DDE} = {879F8787-C3BB-43F3-A92D-6D4C7D3A5285}
+ {F81AE4C4-E044-4225-BD76-385A0DE621FD} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {049A763A-C891-4E8D-80CF-89DD3E22ADC7}
diff --git a/src/KubernetesClient/Util/Common/BadNotificationException.cs b/src/KubernetesClient/Util/Common/BadNotificationException.cs
new file mode 100644
index 0000000..1546bb8
--- /dev/null
+++ b/src/KubernetesClient/Util/Common/BadNotificationException.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace k8s.Util.Common
+{
+ public class BadNotificationException : Exception
+ {
+ public BadNotificationException()
+ {
+ }
+
+ public BadNotificationException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/src/KubernetesClient/Util/Common/Config.cs b/src/KubernetesClient/Util/Common/Config.cs
new file mode 100644
index 0000000..f4609d8
--- /dev/null
+++ b/src/KubernetesClient/Util/Common/Config.cs
@@ -0,0 +1,15 @@
+namespace k8s.Util.Common
+{
+ public static class Config
+ {
+ public static string ServiceAccountCaPath => KubernetesClientConfiguration.ServiceAccountPath + "/ca.crt";
+ public static string ServiceAccountTokenPath => KubernetesClientConfiguration.ServiceAccountPath + "/token";
+ public static string ServiceAccountNamespacePath => KubernetesClientConfiguration.ServiceAccountPath + "/namespace";
+ public static string EnvKubeconfig => "KUBECONFIG";
+ public static string EnvServiceHost => "KUBERNETES_SERVICE_HOST";
+ public static string EnvServicePort => "KUBERNETES_SERVICE_PORT";
+
+ // The last resort host to try
+ public static string DefaultFallbackHost => "http://localhost:8080";
+ }
+}
diff --git a/src/KubernetesClient/Util/Common/Generic/GenericKubernetesApi.cs b/src/KubernetesClient/Util/Common/Generic/GenericKubernetesApi.cs
new file mode 100644
index 0000000..38e4968
--- /dev/null
+++ b/src/KubernetesClient/Util/Common/Generic/GenericKubernetesApi.cs
@@ -0,0 +1,657 @@
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using k8s.Models;
+using k8s.Util.Common.Generic.Options;
+using Microsoft.Rest;
+using Microsoft.Rest.Serialization;
+
+namespace k8s.Util.Common.Generic
+{
+ ///
+ ///
+ /// The Generic kubernetes api provides a unified client interface for not only the non-core-group
+ /// built-in resources from kubernetes but also the custom-resources models meet the following
+ /// requirements:
+ ///
+ /// 1. there's a `V1ObjectMeta` field in the model along with its getter/setter. 2. there's a
+ /// `V1ListMeta` field in the list model along with its getter/setter. - supports Json
+ /// serialization/deserialization. 3. the generic kubernetes api covers all the basic operations over
+ /// the custom resources including {get, list, watch, create, update, patch, delete}.
+ ///
+ /// - For kubernetes-defined failures, the server will return a {@link V1Status} with 4xx/5xx
+ /// code. The status object will be nested in {@link KubernetesApiResponse#getStatus()} - For the
+ /// other unknown reason (including network, JVM..), throws an unchecked exception.
+ ///
+ public class GenericKubernetesApi
+ {
+ private readonly string _apiGroup;
+ private readonly string _apiVersion;
+ private readonly string _resourcePlural;
+ private readonly IKubernetes _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// the api group">
+ /// the api version">
+ /// the resource plural, e.g. "jobs"">
+ /// optional client">
+ public GenericKubernetesApi(string apiGroup = default, string apiVersion = default, string resourcePlural = default, IKubernetes apiClient = default)
+ {
+ _apiGroup = apiGroup ?? throw new ArgumentNullException(nameof(apiGroup));
+ _apiVersion = apiVersion ?? throw new ArgumentNullException(nameof(apiVersion));
+ _resourcePlural = resourcePlural ?? throw new ArgumentNullException(nameof(resourcePlural));
+ _client = apiClient ?? new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig());
+ }
+
+ public TimeSpan ClientTimeout => _client.HttpClient.Timeout;
+
+ public void SetClientTimeout(TimeSpan value)
+ {
+ _client.HttpClient.Timeout = value;
+ }
+
+ ///
+ /// Get kubernetes object.
+ ///
+ /// the object type
+ /// the object name
+ /// the token
+ /// The object
+ public Task GetAsync(string name, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return GetAsync(name, new GetOptions(), cancellationToken);
+ }
+
+ ///
+ /// Get kubernetes object under the namespaceProperty.
+ ///
+ /// the object type
+ /// the namespaceProperty
+ /// the name
+ /// the token
+ /// the kubernetes object
+ public Task GetAsync(string namespaceProperty, string name, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return GetAsync(namespaceProperty, name, new GetOptions(), cancellationToken);
+ }
+
+ ///
+ /// List kubernetes object cluster-scoped.
+ ///
+ /// the object type
+ /// the token
+ /// the kubernetes object
+ public Task ListAsync(CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return ListAsync(new ListOptions(), cancellationToken);
+ }
+
+ ///
+ /// List kubernetes object under the namespaceProperty.
+ ///
+ /// the object type
+ /// the namespace
+ /// the token
+ /// the kubernetes object
+ public Task ListAsync(string namespaceProperty, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return ListAsync(namespaceProperty, new ListOptions(), cancellationToken);
+ }
+
+ ///
+ /// Create kubernetes object, if the namespaceProperty in the object is present, it will send a
+ /// namespaceProperty-scoped requests, vice versa.
+ ///
+ /// the object type
+ /// the object
+ /// the token
+ /// the kubernetes object
+ public Task CreateAsync(T obj, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return CreateAsync(obj, new CreateOptions(), cancellationToken);
+ }
+
+ ///
+ /// Create kubernetes object, if the namespaceProperty in the object is present, it will send a
+ /// namespaceProperty-scoped requests, vice versa.
+ ///
+ /// the object
+ /// the token
+ /// the object type
+ /// the kubernetes object
+ public Task UpdateAsync(T obj, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return UpdateAsync(obj, new UpdateOptions(), cancellationToken);
+ }
+
+ ///
+ /// Patch kubernetes object.
+ ///
+ /// the name
+ /// the string patch content
+ /// the token
+ /// the object type
+ /// the kubernetes object
+ public Task PatchAsync(string name, object patch, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return PatchAsync(name, patch, new PatchOptions(), cancellationToken);
+ }
+
+ ///
+ /// Patch kubernetes object under the namespaceProperty.
+ ///
+ /// the namespaceProperty
+ /// the name
+ /// the string patch content
+ /// the token
+ /// the object type
+ /// the kubernetes object
+ public Task PatchAsync(string namespaceProperty, string name, object patch, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return PatchAsync(namespaceProperty, name, patch, new PatchOptions(), cancellationToken);
+ }
+
+ ///
+ /// Delete kubernetes object.
+ ///
+ /// the name
+ /// the token
+ /// the object type
+ /// the kubernetes object
+ public Task DeleteAsync(string name, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return DeleteAsync(name, new V1DeleteOptions(), cancellationToken);
+ }
+
+ ///
+ /// Delete kubernetes object under the namespaceProperty.
+ ///
+ /// the namespaceProperty
+ /// the name
+ /// the token
+ /// the object type
+ /// the kubernetes object
+ public Task DeleteAsync(string namespaceProperty, string name, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return DeleteAsync(namespaceProperty, name, new V1DeleteOptions(), cancellationToken);
+ }
+
+ ///
+ /// Creates a cluster-scoped Watch on the resource.
+ ///
+ /// action on event
+ /// action on error
+ /// action on closed
+ /// the token
+ /// the object type
+ /// the watchable
+ public Watcher Watch(Action onEvent, Action onError = default, Action onClosed = default, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return Watch(new ListOptions(), onEvent, onError, onClosed, cancellationToken);
+ }
+
+ ///
+ /// Creates a namespaceProperty-scoped Watch on the resource.
+ ///
+ /// the object type
+ /// the namespaceProperty
+ /// action on event
+ /// action on error
+ /// action on closed
+ /// the token
+ /// the watchable
+ public Watcher Watch(string namespaceProperty, Action onEvent, Action onError = default, Action onClosed = default,
+ CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ return Watch(namespaceProperty, new ListOptions(), onEvent, onError, onClosed, cancellationToken);
+ }
+
+ // TODO(yue9944882): watch one resource?
+
+ ///
+ /// Get kubernetes object.
+ ///
+ /// the object type
+ /// the name
+ /// the get options
+ /// the token
+ /// the kubernetes object
+ public async Task GetAsync(string name, GetOptions getOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var resp = await _client.GetClusterCustomObjectWithHttpMessagesAsync(group: _apiGroup, plural: _resourcePlural, version: _apiVersion, name: name, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// Get kubernetes object.
+ ///
+ /// the object type
+ /// the namespaceProperty
+ /// the name
+ /// the get options
+ /// the token
+ /// the kubernetes object
+ public async Task GetAsync(string namespaceProperty, string name, GetOptions getOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (string.IsNullOrEmpty(namespaceProperty))
+ {
+ throw new ArgumentNullException(nameof(namespaceProperty));
+ }
+
+ var resp = await _client.GetNamespacedCustomObjectWithHttpMessagesAsync(group: _apiGroup, plural: _resourcePlural, version: _apiVersion, name: name, namespaceParameter: namespaceProperty,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// List kubernetes object.
+ ///
+ /// the object type
+ /// the list options
+ /// the token
+ /// the kubernetes object
+ public async Task ListAsync(ListOptions listOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (listOptions == null)
+ {
+ throw new ArgumentNullException(nameof(listOptions));
+ }
+
+ var resp = await _client.ListClusterCustomObjectWithHttpMessagesAsync(group: _apiGroup, plural: _resourcePlural, version: _apiVersion, resourceVersion: listOptions.ResourceVersion,
+ continueParameter: listOptions.Continue, fieldSelector: listOptions.FieldSelector, labelSelector: listOptions.LabelSelector, limit: listOptions.Limit,
+ timeoutSeconds: listOptions.TimeoutSeconds, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// List kubernetes object.
+ ///
+ /// the object type
+ /// the namespaceProperty
+ /// the list options
+ /// the token
+ /// the kubernetes object
+ public async Task ListAsync(string namespaceProperty, ListOptions listOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (listOptions == null)
+ {
+ throw new ArgumentNullException(nameof(listOptions));
+ }
+
+ if (string.IsNullOrEmpty(namespaceProperty))
+ {
+ throw new ArgumentNullException(nameof(namespaceProperty));
+ }
+
+ var resp = await _client.ListNamespacedCustomObjectWithHttpMessagesAsync(group: _apiGroup, plural: _resourcePlural, version: _apiVersion, resourceVersion: listOptions.ResourceVersion,
+ continueParameter: listOptions.Continue, fieldSelector: listOptions.FieldSelector, labelSelector: listOptions.LabelSelector, limit: listOptions.Limit,
+ timeoutSeconds: listOptions.TimeoutSeconds, namespaceParameter: namespaceProperty, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// Create kubernetes object.
+ ///
+ /// the object type
+ /// the object
+ /// the create options
+ /// the token
+ /// the kubernetes object
+ public async Task CreateAsync(T obj, CreateOptions createOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (createOptions == null)
+ {
+ throw new ArgumentNullException(nameof(createOptions));
+ }
+
+ V1ObjectMeta objectMeta = obj.Metadata;
+
+ var isNamespaced = !string.IsNullOrEmpty(objectMeta.NamespaceProperty);
+ if (isNamespaced)
+ {
+ return await CreateAsync(objectMeta.NamespaceProperty, obj, createOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ var resp = await _client.CreateClusterCustomObjectWithHttpMessagesAsync(body: obj, group: _apiGroup, plural: _resourcePlural, version: _apiVersion, dryRun: createOptions.DryRun,
+ fieldManager: createOptions.FieldManager, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// Create namespaced kubernetes object.
+ ///
+ /// the object type
+ /// the namespace
+ /// the object
+ /// the create options
+ /// the token
+ /// the kubernetes object
+ public async Task CreateAsync(string namespaceProperty, T obj, CreateOptions createOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (createOptions == null)
+ {
+ throw new ArgumentNullException(nameof(createOptions));
+ }
+
+ var resp = await _client.CreateNamespacedCustomObjectWithHttpMessagesAsync(body: obj, group: _apiGroup, plural: _resourcePlural, version: _apiVersion,
+ namespaceParameter: namespaceProperty, dryRun: createOptions.DryRun, fieldManager: createOptions.FieldManager, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return SafeJsonConvert.DeserializeObject(resp.Body.ToString());
+ }
+
+ ///
+ /// Update kubernetes object.
+ ///
+ /// the object type
+ /// the object
+ /// the update options
+ /// the token
+ /// the kubernetes object
+ public async Task UpdateAsync(T obj, UpdateOptions updateOptions, CancellationToken cancellationToken = default)
+ where T : class, IKubernetesObject
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (updateOptions == null)
+ {
+ throw new ArgumentNullException(nameof(updateOptions));
+ }
+
+ V1ObjectMeta objectMeta = obj.Metadata;
+
+ var isNamespaced = !string.IsNullOrEmpty(objectMeta.NamespaceProperty);
+ HttpOperationResponse