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 resp; + if (isNamespaced) + { + resp = await _client.ReplaceNamespacedCustomObjectWithHttpMessagesAsync(body: obj, name: objectMeta.Name, group: _apiGroup, plural: _resourcePlural, version: _apiVersion, + namespaceParameter: objectMeta.NamespaceProperty, dryRun: updateOptions.DryRun, fieldManager: updateOptions.FieldManager, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + else + { + resp = await _client.ReplaceClusterCustomObjectWithHttpMessagesAsync(body: obj, name: objectMeta.Name, group: _apiGroup ?? obj.ApiGroup(), plural: _resourcePlural, + version: _apiVersion, dryRun: updateOptions.DryRun, fieldManager: updateOptions.FieldManager, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// 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 + /// function to extract the status from the object + /// the token + /// the kubernetes object + public Task UpdateStatusAsync(T obj, Func status, CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + return UpdateStatusAsync(obj, status, new UpdateOptions(), cancellationToken); + } + + /// + /// Update status of kubernetes object. + /// + /// the object type + /// the object + /// function to extract the status from the object + /// the update options + /// the token + /// the kubernetes object + public async Task UpdateStatusAsync(T obj, Func status, 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; + HttpOperationResponse resp; + var isNamespaced = !string.IsNullOrEmpty(objectMeta.NamespaceProperty); + if (isNamespaced) + { + resp = await _client.PatchNamespacedCustomObjectStatusWithHttpMessagesAsync(body: obj, group: _apiGroup, version: _apiVersion, namespaceParameter: objectMeta.NamespaceProperty, + plural: _resourcePlural, name: objectMeta.Name, dryRun: updateOptions.DryRun, fieldManager: updateOptions.FieldManager, force: updateOptions.Force, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + resp = await _client.PatchClusterCustomObjectStatusWithHttpMessagesAsync(body: obj, group: _apiGroup, version: _apiVersion, plural: _resourcePlural, name: objectMeta.Name, + dryRun: updateOptions.DryRun, fieldManager: updateOptions.FieldManager, force: updateOptions.Force, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// Patch kubernetes object. + /// + /// the object type + /// the name + /// the object + /// the patch options + /// the token + /// the kubernetes object + public async Task PatchAsync(string name, object obj, PatchOptions patchOptions, CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (patchOptions == null) + { + throw new ArgumentNullException(nameof(patchOptions)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + var resp = await _client.PatchClusterCustomObjectWithHttpMessagesAsync(body: obj, group: _apiGroup, version: _apiVersion, plural: _resourcePlural, name: name, dryRun: patchOptions.DryRun, + fieldManager: patchOptions.FieldManager, force: patchOptions.Force, cancellationToken: cancellationToken).ConfigureAwait(false); + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// Patch kubernetes object. + /// + /// the object type + /// the namespaceProperty + /// the name + /// the object + /// the patch options + /// the token + /// the kubernetes object + public async Task PatchAsync(string namespaceProperty, string name, object obj, PatchOptions patchOptions, CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + if (string.IsNullOrEmpty(namespaceProperty)) + { + throw new ArgumentNullException(nameof(namespaceProperty)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (patchOptions == null) + { + throw new ArgumentNullException(nameof(patchOptions)); + } + + var resp = await _client.PatchNamespacedCustomObjectWithHttpMessagesAsync(body: obj, group: _apiGroup, version: _apiVersion, namespaceParameter: namespaceProperty, plural: _resourcePlural, + name: name, dryRun: patchOptions.DryRun, fieldManager: patchOptions.FieldManager, force: patchOptions.Force, cancellationToken: cancellationToken).ConfigureAwait(false); + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// Delete kubernetes object. + /// + /// the object type + /// the name + /// the delete options + /// the token + /// the kubernetes object + public async Task DeleteAsync(string name, V1DeleteOptions deleteOptions, CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + var resp = await _client.DeleteClusterCustomObjectWithHttpMessagesAsync( + group: _apiGroup, version: _apiVersion, plural: _resourcePlural, name: name, body: deleteOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// Delete kubernetes object. + /// + /// the object type + /// the namespaceProperty + /// the name + /// the delete options + /// the token + /// the kubernetes object + public async Task DeleteAsync(string namespaceProperty, string name, V1DeleteOptions deleteOptions, CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + if (string.IsNullOrEmpty(namespaceProperty)) + { + throw new ArgumentNullException(nameof(namespaceProperty)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + var resp = await _client.DeleteNamespacedCustomObjectWithHttpMessagesAsync(group: _apiGroup, version: _apiVersion, namespaceParameter: namespaceProperty, plural: _resourcePlural, + name: name, body: deleteOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + return SafeJsonConvert.DeserializeObject(resp.Body.ToString()); + } + + /// + /// Watch watchable. + /// + /// the list options + /// action on event + /// action on error + /// action on closed + /// the token + /// the object type + /// the watchable + public Watcher Watch(ListOptions listOptions, Action onEvent, Action onError = default, Action onClosed = default, + CancellationToken cancellationToken = default) + where T : class, IKubernetesObject + { + if (listOptions == null) + { + throw new ArgumentNullException(nameof(listOptions)); + } + + var resp = _client.ListClusterCustomObjectWithHttpMessagesAsync(group: _apiGroup, version: _apiVersion, plural: _resourcePlural, continueParameter: listOptions.Continue, + fieldSelector: listOptions.FieldSelector, labelSelector: listOptions.LabelSelector, limit: listOptions.Limit, resourceVersion: listOptions.ResourceVersion, + timeoutSeconds: listOptions.TimeoutSeconds, watch: true, cancellationToken: cancellationToken); + + return resp.Watch(onEvent, onError, onClosed); + } + + /// + /// Watch watchable. + /// + /// the namespaceProperty + /// the list options + /// action on event + /// action on error + /// action on closed + /// the token + /// the object type + /// the watchable + public Watcher Watch(string namespaceProperty, ListOptions listOptions, Action onEvent, Action onError = default, Action onClosed = default, + 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 = _client.ListNamespacedCustomObjectWithHttpMessagesAsync(group: _apiGroup, version: _apiVersion, namespaceParameter: namespaceProperty, plural: _resourcePlural, + continueParameter: listOptions.Continue, fieldSelector: listOptions.FieldSelector, labelSelector: listOptions.LabelSelector, limit: listOptions.Limit, + resourceVersion: listOptions.ResourceVersion, timeoutSeconds: listOptions.TimeoutSeconds, watch: true, cancellationToken: cancellationToken); + + return resp.Watch(onEvent, onError, onClosed); + } + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/KubernetesApiResponse.cs b/src/KubernetesClient/Util/Common/Generic/KubernetesApiResponse.cs new file mode 100644 index 0000000..0868783 --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/KubernetesApiResponse.cs @@ -0,0 +1,65 @@ +using System.Net; +using k8s.Models; + +namespace k8s.Util.Common.Generic +{ + public class KubernetesApiResponse + where TDataType : IKubernetesObject + { + public KubernetesApiResponse(TDataType @object) + { + Object = @object; + Status = null; + HttpStatusCode = HttpStatusCode.OK; // 200 + } + + public KubernetesApiResponse(V1Status status, HttpStatusCode httpStatusCode) + { + Object = default(TDataType); + Status = status; + HttpStatusCode = httpStatusCode; + } + + public TDataType Object { get; } + + public V1Status Status { get; } + + public HttpStatusCode HttpStatusCode { get; } + + public bool IsSuccess => ((int)HttpStatusCode > 199 && (int)HttpStatusCode < 300); // 400 + + /// + /// Throws api exception kubernetes api response on failure. This is the recommended approach to + /// deal with errors returned from server. + /// + /// the kubernetes api response + /// the api exception + public KubernetesApiResponse ThrowsApiException() + { + return OnFailure(new ErrorStatusHandler()); + } + + /// + /// Calling errorStatusHandler upon errors from server + /// + /// the error status handler + /// the kubernetes api response + public KubernetesApiResponse OnFailure(ErrorStatusHandler errorStatusHandler) + { + if (!IsSuccess && errorStatusHandler != null) + { + errorStatusHandler.Handle((int)HttpStatusCode, Status); + } + + return this; + } + + public class ErrorStatusHandler + { + public void Handle(int code, V1Status errorStatus) + { + throw new HttpListenerException(code, errorStatus?.Message); + } + } + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/Options/CreateOptions.cs b/src/KubernetesClient/Util/Common/Generic/Options/CreateOptions.cs new file mode 100644 index 0000000..d688d53 --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/Options/CreateOptions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace k8s.Util.Common.Generic.Options +{ + public class CreateOptions + { + public string DryRun { get; private set; } + + public string FieldManager { get; private set; } + + public CreateOptions(string dryRun = default, string fieldManager = default) + { + DryRun = dryRun; + FieldManager = fieldManager; + } + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/Options/GetOptions.cs b/src/KubernetesClient/Util/Common/Generic/Options/GetOptions.cs new file mode 100644 index 0000000..7a1425f --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/Options/GetOptions.cs @@ -0,0 +1,6 @@ +namespace k8s.Util.Common.Generic.Options +{ + public class GetOptions + { + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/Options/ListOptions.cs b/src/KubernetesClient/Util/Common/Generic/Options/ListOptions.cs new file mode 100644 index 0000000..2181729 --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/Options/ListOptions.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace k8s.Util.Common.Generic.Options +{ + public class ListOptions + { + public int? TimeoutSeconds { get; private set; } + + public int Limit { get; private set; } + + public string FieldSelector { get; private set; } + + public string LabelSelector { get; private set; } + + public string ResourceVersion { get; private set; } + + public string Continue { get; private set; } + + public ListOptions(int? timeoutSeconds = default, int limit = default, string fieldSelector = default, string labelSelector = default, string resourceVersion = default, + string @continue = default) + { + TimeoutSeconds = timeoutSeconds; + Limit = limit; + FieldSelector = fieldSelector; + LabelSelector = labelSelector; + ResourceVersion = resourceVersion; + Continue = @continue; + } + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/Options/PatchOptions.cs b/src/KubernetesClient/Util/Common/Generic/Options/PatchOptions.cs new file mode 100644 index 0000000..98610b0 --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/Options/PatchOptions.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace k8s.Util.Common.Generic.Options +{ + public class PatchOptions + { + public string DryRun { get; private set; } + + public bool Force { get; private set; } + + public string FieldManager { get; private set; } + + public PatchOptions(string dryRun = default, bool force = false, string fieldManager = default) + { + DryRun = dryRun; + Force = force; + FieldManager = fieldManager; + } + } +} diff --git a/src/KubernetesClient/Util/Common/Generic/Options/UpdateOptions.cs b/src/KubernetesClient/Util/Common/Generic/Options/UpdateOptions.cs new file mode 100644 index 0000000..f378fe9 --- /dev/null +++ b/src/KubernetesClient/Util/Common/Generic/Options/UpdateOptions.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace k8s.Util.Common.Generic.Options +{ + public class UpdateOptions + { + public string DryRun { get; private set; } + + public bool Force { get; private set; } + + public string FieldManager { get; private set; } + + public UpdateOptions(string dryRun = default, bool force = false, string fieldManager = default) + { + DryRun = dryRun; + Force = force; + FieldManager = fieldManager; + } + } +} diff --git a/src/KubernetesClient/Util/Common/Namespaces.cs b/src/KubernetesClient/Util/Common/Namespaces.cs new file mode 100644 index 0000000..1bb705c --- /dev/null +++ b/src/KubernetesClient/Util/Common/Namespaces.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text; + +namespace k8s.Util.Common +{ + /// + /// Namespaces provides a set of helpers for operating namespaces. + /// + public class Namespaces + { + public const string NamespaceAll = ""; + + public const string NamespaceDefault = "default"; + + public const string NamespaceKubeSystem = "kube-system"; + + public static string GetPodNamespace() + { + return File.ReadAllText(Config.ServiceAccountNamespacePath, Encoding.UTF8); + } + } +} diff --git a/src/KubernetesClient/Util/Informer/Cache/Cache.cs b/src/KubernetesClient/Util/Informer/Cache/Cache.cs index b1ae42a..193ffc9 100644 --- a/src/KubernetesClient/Util/Informer/Cache/Cache.cs +++ b/src/KubernetesClient/Util/Informer/Cache/Cache.cs @@ -16,7 +16,7 @@ namespace k8s.Util.Informer.Cache /// /// keyFunc defines how to map index objects into indices /// - private Func _keyFunc; + private Func, string> _keyFunc; /// /// indexers stores index functions by their names @@ -51,6 +51,9 @@ namespace k8s.Util.Informer.Cache /// learn more: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement private readonly object _lock = new object(); + /// + /// Initializes a new instance of the class. Uses an object's namespace as the key. + /// public Cache() : this(Caches.NamespaceIndex, Caches.MetaNamespaceIndexFunc, Caches.DeletionHandlingMetaNamespaceKeyFunc) { @@ -63,13 +66,23 @@ namespace k8s.Util.Informer.Cache /// the index name, an unique name representing the index /// the index func by which we map multiple object to an index for querying /// the key func by which we map one object to an unique key for storing - public Cache(string indexName, Func> indexFunc, Func keyFunc) + public Cache(string indexName, Func> indexFunc, Func, string> keyFunc) { _indexers[indexName] = indexFunc; _keyFunc = keyFunc; _indices[indexName] = new Dictionary>(); } + public void Clear() + { + lock (_lock) + { + _items?.Clear(); + _indices?.Clear(); + _indexers?.Clear(); + } + } + /// /// Add objects. /// @@ -421,9 +434,9 @@ namespace k8s.Util.Informer.Cache _indexers[indexName] = indexFunc; } - public Func KeyFunc => _keyFunc; + public Func, string> KeyFunc => _keyFunc; - public void SetKeyFunc(Func keyFunc) + public void SetKeyFunc(Func, string> keyFunc) { _keyFunc = keyFunc; } diff --git a/src/KubernetesClient/Util/Informer/Cache/IStore.cs b/src/KubernetesClient/Util/Informer/Cache/IStore.cs index 8fb935a..bbc2618 100644 --- a/src/KubernetesClient/Util/Informer/Cache/IStore.cs +++ b/src/KubernetesClient/Util/Informer/Cache/IStore.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using k8s.Models; @@ -61,5 +62,10 @@ namespace k8s.Util.Informer.Cache /// /// list of all the items IEnumerable List(); + + /// + /// Empty the store + /// + void Clear(); } } diff --git a/tests/KubernetesClient.Tests/Util/Common/Generic/GenericKubernetesApiTest.cs b/tests/KubernetesClient.Tests/Util/Common/Generic/GenericKubernetesApiTest.cs new file mode 100644 index 0000000..3475de8 --- /dev/null +++ b/tests/KubernetesClient.Tests/Util/Common/Generic/GenericKubernetesApiTest.cs @@ -0,0 +1,109 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using k8s.Models; +using k8s.Tests.Mock; +using k8s.Util.Common; +using Xunit; +using Xunit.Abstractions; + +namespace k8s.Tests.Util.Common.Generic +{ + public class GenericKubernetesApiTest + { + private readonly ITestOutputHelper _outputHelper; + + public GenericKubernetesApiTest(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Fact(DisplayName = "Create constructor success")] + public void CreateConstSuccess() + { + using var server = new MockKubeApiServer(_outputHelper); + var genericApi = Helpers.BuildGenericApi(server.Uri); + genericApi.Should().NotBeNull(); + } + + [Fact(DisplayName = "Get namespaced object success")] + public async Task GetNamespacedObject() + { + var serverOptions = new MockKubeApiServerOptions(MockKubeServerFlags.GetPod); + using var server = new MockKubeApiServer(_outputHelper, serverOptions.ShouldNext); + var podName = "nginx-1493591563-xb2v4"; + var genericApi = Helpers.BuildGenericApi(server.Uri); + + var resp = await genericApi.GetAsync(Namespaces.NamespaceDefault, podName).ConfigureAwait(false); + + resp.Should().NotBeNull(); + resp.Metadata.Name.Should().Be(podName); + resp.Metadata.NamespaceProperty.Should().Be(Namespaces.NamespaceDefault); + } + + [Fact(DisplayName = "List namespaced object success")] + public async Task ListNamespacedObject() + { + var serverOptions = new MockKubeApiServerOptions(MockKubeServerFlags.ListPods); + using var server = new MockKubeApiServer(_outputHelper, serverOptions.ShouldNext); + var genericApi = Helpers.BuildGenericApi(server.Uri); + + var resp = await genericApi.ListAsync(Namespaces.NamespaceDefault).ConfigureAwait(false); + + resp.Should().NotBeNull(); + resp.Items.Should().NotBeNull(); + } + + [Fact(DisplayName = "Patch namespaced object success")] + public async Task PatchNamespacedObject() + { + using var server = new MockKubeApiServer(_outputHelper); + var podName = "nginx-1493591563-xb2v4"; + var genericApi = Helpers.BuildGenericApi(server.Uri); + + var resp = await genericApi.PatchAsync(Namespaces.NamespaceDefault, podName).ConfigureAwait(false); + + resp.Should().NotBeNull(); + } + + [Fact(DisplayName = "Update object success")] + public async Task UpdateObject() + { + using var server = new MockKubeApiServer(_outputHelper); + var pod = Helpers.CreatePods(1).First(); + var genericApi = Helpers.BuildGenericApi(server.Uri); + + var resp = await genericApi.UpdateAsync(pod).ConfigureAwait(false); + + resp.Should().NotBeNull(); + } + + [Fact(DisplayName = "Delete namespaced object success")] + public async Task DeleteNamespacedObject() + { + using var server = new MockKubeApiServer(_outputHelper); + var podName = "nginx-1493591563-xb2v4"; + var genericApi = Helpers.BuildGenericApi(server.Uri); + + var resp = await genericApi.DeleteAsync(Namespaces.NamespaceDefault, podName).ConfigureAwait(false); + + resp.Should().NotBeNull(); + } + + [Fact(DisplayName = "Watch namespaced object success")] + public void WatchNamespacedObject() + { + using var cts = new CancellationTokenSource(); + var serverOptions = new MockKubeApiServerOptions(MockKubeServerFlags.ModifiedPod); + using var server = new MockKubeApiServer(_outputHelper, serverOptions.ShouldNext); + var genericApi = Helpers.BuildGenericApi(server.Uri); + + using var resp = genericApi.Watch(Namespaces.NamespaceDefault, (actionType, pod) => { }, exception => { }, () => { }, cts.Token); + + resp.Should().NotBeNull(); + cts.CancelAfter(1000); + serverOptions.ServerShutdown?.Set(); + } + } +} diff --git a/tests/KubernetesClient.Tests/Util/Helpers.cs b/tests/KubernetesClient.Tests/Util/Helpers.cs new file mode 100644 index 0000000..7a2a096 --- /dev/null +++ b/tests/KubernetesClient.Tests/Util/Helpers.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using k8s.Models; +using k8s.Tests.Mock; +using k8s.Util.Common.Generic; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Nito.AsyncEx; +using Xunit.Abstractions; + +namespace k8s.Tests.Util +{ + internal static class Helpers + { + public static IEnumerable CreatePods(int cnt) + { + var pods = new List(); + for (var i = 0; i < cnt; i++) + { + pods.Add(new V1Pod() + { + ApiVersion = "Pod/V1", + Kind = "Pod", + Metadata = new V1ObjectMeta() + { + Name = Guid.NewGuid().ToString(), + NamespaceProperty = "the-namespace", + ResourceVersion = DateTime.Now.Ticks.ToString(), + }, + }); + } + + return pods; + } + + public static V1PodList CreatePodList(int cnt) + { + return new V1PodList() + { + ApiVersion = "Pod/V1", + Kind = "Pod", + Metadata = new V1ListMeta() + { + ResourceVersion = DateTime.Now.Ticks.ToString(), + }, + Items = CreatePods(cnt).ToList(), + }; + } + + public static Kubernetes BuildApiClient(Uri hostAddress) + { + return new Kubernetes(new KubernetesClientConfiguration { Host = hostAddress.ToString() }) + { + HttpClient = + { + Timeout = Timeout.InfiniteTimeSpan, + }, + }; + } + + public static GenericKubernetesApi BuildGenericApi(Uri hostAddress) + { + return new GenericKubernetesApi( + apiGroup: "pod", + apiVersion: "v1", + resourcePlural: "pods", + apiClient: BuildApiClient(hostAddress)); + } + } +} diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs index 9eae0e9..a3f4094 100644 --- a/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs +++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/CacheTest.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; -using k8s.Util.Informer.Cache; using k8s.Models; +using k8s.Util.Informer.Cache; using Xunit; namespace k8s.Tests.Util.Informer.Cache @@ -16,13 +16,12 @@ namespace k8s.Tests.Util.Informer.Cache var cache = new Cache(); cache.Should().NotBeNull(); cache.GetIndexers().ContainsKey(Caches.NamespaceIndex).Should().BeTrue(); - // Todo: validate all defaults gor set up } [Fact(DisplayName = "Add cache item success")] private void AddCacheItemSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); cache.Add(aPod); @@ -33,7 +32,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Update cache item success")] private void UpdateCacheItemSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -47,7 +46,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Delete cache item success")] private void DeleteCacheItemSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -61,7 +60,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Replace cache items success")] private void ReplaceCacheItemsSuccess() { - var pods = Util.CreatePods(3); + var pods = Helpers.CreatePods(3); var aPod = pods.First(); var anotherPod = pods.Skip(1).First(); var yetAnotherPod = pods.Skip(2).First(); @@ -79,7 +78,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "List item keys success")] public void ListItemKeysSuccess() { - var pods = Util.CreatePods(3); + var pods = Helpers.CreatePods(3); var aPod = pods.First(); var anotherPod = pods.Skip(1).First(); var cache = new Cache(); @@ -96,7 +95,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get item doesn't exist")] public void GetItemNotExist() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var item = cache.Get(aPod); @@ -106,7 +105,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get item success")] public void GetItemSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); cache.Add(aPod); @@ -117,7 +116,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "List items success")] public void ListItemSuccess() { - var pods = Util.CreatePods(3); + var pods = Helpers.CreatePods(3); var aPod = pods.First(); var anotherPod = pods.Skip(1).First(); var yetAnotherPod = pods.Skip(2).First(); @@ -138,7 +137,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get item by key success")] public void GetItemByKeySuccess() { - var pod = Util.CreatePods(1).First(); + var pod = Helpers.CreatePods(1).First(); var cache = new Cache(); cache.Add(pod); @@ -149,7 +148,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Index items no index")] public void IndexItemsNoIndex() { - var pod = Util.CreatePods(1).First(); + var pod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -161,7 +160,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Index items success")] public void IndexItemsSuccess() { - var pod = Util.CreatePods(1).First(); + var pod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -191,7 +190,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get index keys success")] public void GetIndexKeysSuccess() { - var pod = Util.CreatePods(1).First(); + var pod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -221,7 +220,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "List by index success")] public void ListByIndexSuccess() { - var pod = Util.CreatePods(1).First(); + var pod = Helpers.CreatePods(1).First(); var cache = new Cache(); @@ -323,7 +322,7 @@ namespace k8s.Tests.Util.Informer.Cache }, }; var cache = new Cache(); - var newFunc = new Func((pod) => pod.Kind); + var newFunc = new Func, string>((pod) => pod.Kind); var defaultReturnValue = newFunc(aPod); cache.SetKeyFunc(newFunc); diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs index dd2cfb7..312a75b 100644 --- a/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs +++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/CachesTest.cs @@ -12,14 +12,14 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Check for default DeletedFinalStateUnknown")] public void CheckDefaultDeletedFinalStateUnknown() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); Caches.DeletionHandlingMetaNamespaceKeyFunc(aPod).Should().Be($"{aPod.Metadata.NamespaceProperty}/{aPod.Metadata.Name}"); } [Fact(DisplayName = "Check for obj DeletedFinalStateUnknown")] public void CheckObjDeletedFinalStateUnknown() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var key = "a-key"; var deletedPod = new DeletedFinalStateUnknown(key, aPod); @@ -37,7 +37,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get default namespace key success")] public void GetDefaultNamespaceKeySuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); Caches.MetaNamespaceKeyFunc(aPod).Should().Be($"{aPod.Metadata.NamespaceProperty}/{aPod.Metadata.Name}"); } @@ -50,7 +50,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get default namespace index success")] public void GetDefaultNamespaceIndexSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var indexes = Caches.MetaNamespaceIndexFunc(aPod); indexes.Should().NotBeNull(); diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs index 1d1860e..63e46b7 100644 --- a/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs +++ b/tests/KubernetesClient.Tests/Util/Informer/Cache/ListerTest.cs @@ -1,8 +1,8 @@ using System.Linq; using FluentAssertions; using k8s.Models; -using Xunit; using k8s.Util.Informer.Cache; +using Xunit; namespace k8s.Tests.Util.Informer.Cache { @@ -20,7 +20,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "List with null namespace success")] private void ListNullNamespaceSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var lister = new Lister(cache); @@ -35,7 +35,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "List with custom namespace success")] private void ListCustomNamespaceSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var lister = new Lister(cache, aPod.Metadata.NamespaceProperty); @@ -50,7 +50,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get with null namespace success")] private void GetNullNamespaceSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var lister = new Lister(cache); @@ -65,7 +65,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Get with custom namespace success")] private void GetCustomNamespaceSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var lister = new Lister(cache, aPod.Metadata.NamespaceProperty); @@ -78,7 +78,7 @@ namespace k8s.Tests.Util.Informer.Cache [Fact(DisplayName = "Set custom namespace success")] private void SetCustomNamespaceSuccess() { - var aPod = Util.CreatePods(1).First(); + var aPod = Helpers.CreatePods(1).First(); var cache = new Cache(); var lister = new Lister(cache); diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs deleted file mode 100644 index da7f187..0000000 --- a/tests/KubernetesClient.Tests/Util/Informer/Cache/ReflectorTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FluentAssertions; -using k8s.Models; -using k8s.Util.Informer.Cache; -using Microsoft.Extensions.Logging; -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace k8s.Tests.Util.Informer.Cache -{ - public class ReflectorTest - { - private readonly ITestOutputHelper _ouputHelper; - - public ReflectorTest(ITestOutputHelper outputHelper) - { - _ouputHelper = outputHelper; - } - - [Fact(DisplayName = "Create default reflector success")] - public void CreateReflectorSuccess() - { - /*using var apiClient = new Kubernetes(_clientConfiguration); - var cache = new Cache(); - var queue = new DeltaFifo(Caches.MetaNamespaceKeyFunc, cache, _deltasLogger); - var listerWatcher = new ListWatcher(apiClient, ListAllPods); - var logger = LoggerFactory.Create(builder => builder.AddXUnit(_ouputHelper).SetMinimumLevel(LogLevel.Trace)).CreateLogger(); - var reflector = new k8s.Util.Cache.Reflector(listerWatcher, queue, logger); - - reflector.Should().NotBeNull();*/ - } - } -} diff --git a/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs b/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs deleted file mode 100644 index 7b10c14..0000000 --- a/tests/KubernetesClient.Tests/Util/Informer/Cache/Util.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using k8s.Models; - -namespace k8s.Tests.Util.Informer.Cache -{ - internal static class Util - { - internal static IEnumerable CreatePods(int cnt) - { - var pods = new List(); - for (var i = 0; i < cnt; i++) - { - pods.Add(new V1Pod() - { - ApiVersion = "Pod/V1", - Kind = "Pod", - Metadata = new V1ObjectMeta() - { - Name = Guid.NewGuid().ToString(), - NamespaceProperty = "the-namespace", - ResourceVersion = "1", - }, - }); - } - - return pods; - } - - internal static V1PodList CreatePostList(int cnt) - { - return new V1PodList() - { - ApiVersion = "Pod/V1", - Kind = "Pod", - Metadata = new V1ListMeta() - { - ResourceVersion = "1", - }, - Items = CreatePods(cnt).ToList(), - }; - } - } -} diff --git a/tests/KubernetesClient.Tests/Util/MockKubeApiServerOptions.cs b/tests/KubernetesClient.Tests/Util/MockKubeApiServerOptions.cs new file mode 100644 index 0000000..b612c59 --- /dev/null +++ b/tests/KubernetesClient.Tests/Util/MockKubeApiServerOptions.cs @@ -0,0 +1,171 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using k8s.Models; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Nito.AsyncEx; + +namespace k8s.Tests.Util +{ + /// + /// Flags to configure how the server will respond to requests + /// + [Flags] + public enum MockKubeServerFlags + { + /// + /// No flag + /// + None = 0, + + /// + /// Include a response with malformed json + /// + BadJson = 1, + + /// + /// Include a pod added response + /// + AddedPod = 2, + + /// + /// Include a pod delete response + /// + DeletedPod = 4, + + /// + /// Include a pod modified response + /// + ModifiedPod = 8, + + /// + /// Include a pod error response + /// + ErrorPod = 16, + + /// + /// Include a response of pod list + /// + ListPods = 32, + + /// + /// Include a reponse of get pod + /// + GetPod = 64, + + /// + /// Throw a 500 Http status code on any request + /// + Throw500 = 128, + } + + internal class MockKubeApiServerOptions + { + // paste from minikube /api/v1/namespaces/default/pods + public const string MockPodResponse = + "{\r\n \"kind\": \"PodList\",\r\n \"apiVersion\": \"v1\",\r\n \"metadata\": {\r\n \"selfLink\": \"/api/v1/namespaces/default/pods\",\r\n \"resourceVersion\": \"1762810\"\r\n },\r\n \"items\": [\r\n {\r\n \"metadata\": {\r\n \"name\": \"nginx-1493591563-xb2v4\",\r\n \"generateName\": \"nginx-1493591563-\",\r\n \"namespace\": \"default\",\r\n \"selfLink\": \"/api/v1/namespaces/default/pods/nginx-1493591563-xb2v4\",\r\n \"uid\": \"ac1abb94-9c58-11e7-aaf5-00155d744505\",\r\n \"resourceVersion\": \"1737928\",\r\n \"creationTimestamp\": \"2017-09-18T10:03:51Z\",\r\n \"labels\": {\r\n \"app\": \"nginx\",\r\n \"pod-template-hash\": \"1493591563\"\r\n },\r\n \"annotations\": {\r\n \"kubernetes.io/created-by\": \"{\\\"kind\\\":\\\"SerializedReference\\\",\\\"apiVersion\\\":\\\"v1\\\",\\\"reference\\\":{\\\"kind\\\":\\\"ReplicaSet\\\",\\\"namespace\\\":\\\"default\\\",\\\"name\\\":\\\"nginx-1493591563\\\",\\\"uid\\\":\\\"ac013b63-9c58-11e7-aaf5-00155d744505\\\",\\\"apiVersion\\\":\\\"extensions\\\",\\\"resourceVersion\\\":\\\"5306\\\"}}\\n\"\r\n },\r\n \"ownerReferences\": [\r\n {\r\n \"apiVersion\": \"extensions/v1beta1\",\r\n \"kind\": \"ReplicaSet\",\r\n \"name\": \"nginx-1493591563\",\r\n \"uid\": \"ac013b63-9c58-11e7-aaf5-00155d744505\",\r\n \"controller\": true,\r\n \"blockOwnerDeletion\": true\r\n }\r\n ]\r\n },\r\n \"spec\": {\r\n \"volumes\": [\r\n {\r\n \"name\": \"default-token-3zzcj\",\r\n \"secret\": {\r\n \"secretName\": \"default-token-3zzcj\",\r\n \"defaultMode\": 420\r\n }\r\n }\r\n ],\r\n \"containers\": [\r\n {\r\n \"name\": \"nginx\",\r\n \"image\": \"nginx\",\r\n \"resources\": {},\r\n \"volumeMounts\": [\r\n {\r\n \"name\": \"default-token-3zzcj\",\r\n \"readOnly\": true,\r\n \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\r\n }\r\n ],\r\n \"terminationMessagePath\": \"/dev/termination-log\",\r\n \"terminationMessagePolicy\": \"File\",\r\n \"imagePullPolicy\": \"Always\"\r\n }\r\n ],\r\n \"restartPolicy\": \"Always\",\r\n \"terminationGracePeriodSeconds\": 30,\r\n \"dnsPolicy\": \"ClusterFirst\",\r\n \"serviceAccountName\": \"default\",\r\n \"serviceAccount\": \"default\",\r\n \"nodeName\": \"ubuntu\",\r\n \"securityContext\": {},\r\n \"schedulerName\": \"default-scheduler\"\r\n },\r\n \"status\": {\r\n \"phase\": \"Running\",\r\n \"conditions\": [\r\n {\r\n \"type\": \"Initialized\",\r\n \"status\": \"True\",\r\n \"lastProbeTime\": null,\r\n \"lastTransitionTime\": \"2017-09-18T10:03:51Z\"\r\n },\r\n {\r\n \"type\": \"Ready\",\r\n \"status\": \"True\",\r\n \"lastProbeTime\": null,\r\n \"lastTransitionTime\": \"2017-10-12T07:09:21Z\"\r\n },\r\n {\r\n \"type\": \"PodScheduled\",\r\n \"status\": \"True\",\r\n \"lastProbeTime\": null,\r\n \"lastTransitionTime\": \"2017-09-18T10:03:51Z\"\r\n }\r\n ],\r\n \"hostIP\": \"192.168.188.42\",\r\n \"podIP\": \"172.17.0.5\",\r\n \"startTime\": \"2017-09-18T10:03:51Z\",\r\n \"containerStatuses\": [\r\n {\r\n \"name\": \"nginx\",\r\n \"state\": {\r\n \"running\": {\r\n \"startedAt\": \"2017-10-12T07:09:20Z\"\r\n }\r\n },\r\n \"lastState\": {\r\n \"terminated\": {\r\n \"exitCode\": 0,\r\n \"reason\": \"Completed\",\r\n \"startedAt\": \"2017-10-10T21:35:51Z\",\r\n \"finishedAt\": \"2017-10-12T07:07:37Z\",\r\n \"containerID\": \"docker://94df3f3965807421ad6dc76618e00b76cb15d024919c4946f3eb46a92659c62a\"\r\n }\r\n },\r\n \"ready\": true,\r\n \"restartCount\": 7,\r\n \"image\": \"nginx:latest\",\r\n \"imageID\": \"docker-pullable://nginx@sha256:004ac1d5e791e705f12a17c80d7bb1e8f7f01aa7dca7deee6e65a03465392072\",\r\n \"containerID\": \"docker://fa11bdd48c9b7d3a6c4c3f9b6d7319743c3455ab8d00c57d59c083b319b88194\"\r\n }\r\n ],\r\n \"qosClass\": \"BestEffort\"\r\n }\r\n }\r\n ]\r\n}"; + + public AsyncManualResetEvent ServerShutdown { get; private set; } + + private readonly MockKubeServerFlags _serverFlags; + private readonly string _mockAddedEventStreamLine = BuildWatchEventStreamLine(WatchEventType.Added); + private readonly string _mockDeletedStreamLine = BuildWatchEventStreamLine(WatchEventType.Deleted); + private readonly string _mockModifiedStreamLine = BuildWatchEventStreamLine(WatchEventType.Modified); + private readonly string _mockErrorStreamLine = BuildWatchEventStreamLine(WatchEventType.Error); + private const string MockBadStreamLine = "bad json"; + + public MockKubeApiServerOptions(MockKubeServerFlags? serverFlags) + { + _serverFlags = serverFlags ?? MockKubeServerFlags.None; + } + + private static string BuildWatchEventStreamLine(WatchEventType eventType) + { + var corev1PodList = JsonConvert.DeserializeObject(MockPodResponse); + return JsonConvert.SerializeObject( + new Watcher.WatchEvent { Type = eventType, Object = corev1PodList.Items.First() }, + new StringEnumConverter()); + } + + private async Task WriteStreamLine(HttpContext httpContext, string reponseLine) + { + const string crlf = "\r\n"; + await httpContext.Response.WriteAsync(reponseLine.Replace(crlf, "")).ConfigureAwait(false); + await httpContext.Response.WriteAsync(crlf).ConfigureAwait(false); + await httpContext.Response.Body.FlushAsync().ConfigureAwait(false); + } + + public async Task ShouldNext(HttpContext httpContext) + { + var isWatch = (httpContext.Request.Query.ContainsKey("watch") && httpContext.Request.Query["watch"] == "true"); + var returnStatusCode = (_serverFlags.HasFlag(MockKubeServerFlags.Throw500) ? HttpStatusCode.InternalServerError : HttpStatusCode.OK); + + httpContext.Response.StatusCode = (int)returnStatusCode; + httpContext.Response.ContentLength = null; + + if (isWatch) + { + ServerShutdown = new AsyncManualResetEvent(); + + foreach (Enum flag in Enum.GetValues(_serverFlags.GetType())) + { + if (!_serverFlags.HasFlag(flag)) + { + continue; + } + + switch (flag) + { + case MockKubeServerFlags.AddedPod: + await WriteStreamLine(httpContext, _mockAddedEventStreamLine).ConfigureAwait(false); + break; + case MockKubeServerFlags.ErrorPod: + await WriteStreamLine(httpContext, _mockErrorStreamLine).ConfigureAwait(false); + break; + case MockKubeServerFlags.DeletedPod: + await WriteStreamLine(httpContext, _mockDeletedStreamLine).ConfigureAwait(false); + break; + case MockKubeServerFlags.ModifiedPod: + await WriteStreamLine(httpContext, _mockModifiedStreamLine).ConfigureAwait(false); + break; + case MockKubeServerFlags.BadJson: + await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(false); + break; + case MockKubeServerFlags.Throw500: + return false; + } + } + + // keep server connection open + await ServerShutdown.WaitAsync().ConfigureAwait(false); + return false; + } + + foreach (Enum flag in Enum.GetValues(_serverFlags.GetType())) + { + if (!_serverFlags.HasFlag(flag)) + { + continue; + } + + switch (flag) + { + case MockKubeServerFlags.ListPods: + await WriteStreamLine(httpContext, MockPodResponse).ConfigureAwait(false); + break; + case MockKubeServerFlags.GetPod: + var corev1PodList = JsonConvert.DeserializeObject(MockPodResponse); + await WriteStreamLine(httpContext, JsonConvert.SerializeObject(corev1PodList.Items.First())).ConfigureAwait(false); + break; + case MockKubeServerFlags.Throw500: + return false; + } + } + + return false; + } + } +}