From 0e3cb942d1f6c171f552c11f0ca1bba6ca4f5335 Mon Sep 17 00:00:00 2001 From: Ali Kanso Date: Sat, 19 Dec 2020 11:26:25 -0500 Subject: [PATCH] Custom Resource example (#540) * CR example * minor changes --- .../CustomResourceDefinition.cs | 45 ++++++++ examples/customResource/Program.cs | 103 ++++++++++++++++++ examples/customResource/README.md | 53 +++++++++ examples/customResource/Utils.cs | 48 ++++++++ examples/customResource/cResource.cs | 35 ++++++ examples/customResource/config/crd.yaml | 11 ++ .../config/yaml-cr-instance.yaml | 8 ++ examples/customResource/customResource.csproj | 15 +++ kubernetes-client.sln | 15 +++ 9 files changed, 333 insertions(+) create mode 100644 examples/customResource/CustomResourceDefinition.cs create mode 100644 examples/customResource/Program.cs create mode 100644 examples/customResource/README.md create mode 100644 examples/customResource/Utils.cs create mode 100644 examples/customResource/cResource.cs create mode 100644 examples/customResource/config/crd.yaml create mode 100644 examples/customResource/config/yaml-cr-instance.yaml create mode 100644 examples/customResource/customResource.csproj diff --git a/examples/customResource/CustomResourceDefinition.cs b/examples/customResource/CustomResourceDefinition.cs new file mode 100644 index 0000000..a83d027 --- /dev/null +++ b/examples/customResource/CustomResourceDefinition.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using k8s; +using k8s.Models; +using Newtonsoft.Json; + +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "This is just an example.")] +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "This is just an example.")] + +namespace customResource +{ + public class CustomResourceDefinition + { + public string Version { get; set; } + + public string Group { get; set; } + + public string PluralName { get; set; } + + public string Kind { get; set; } + + public string Namespace { get; set; } + } + + public abstract class CustomResource : KubernetesObject + { + [JsonProperty(PropertyName = "metadata")] + public V1ObjectMeta Metadata { get; set; } + } + + public abstract class CustomResource : CustomResource + { + [JsonProperty(PropertyName = "spec")] + public TSpec Spec { get; set; } + + [JsonProperty(PropertyName = "CStatus")] + public TStatus CStatus { get; set; } + } + + public class CustomResourceList : KubernetesObject + where T : CustomResource + { + public V1ListMeta Metadata { get; set; } + public List Items { get; set; } + } +} diff --git a/examples/customResource/Program.cs b/examples/customResource/Program.cs new file mode 100644 index 0000000..9ecbbd7 --- /dev/null +++ b/examples/customResource/Program.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using k8s; +using k8s.Models; +using System.Threading.Tasks; +using Microsoft.AspNetCore.JsonPatch; + + +namespace customResource +{ + public class Program + { + private static async Task Main(string[] args) + { + Console.WriteLine("strating main()..."); + + // creating the k8s client + var k8SClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile(); + IKubernetes client = new Kubernetes(k8SClientConfig); + + // creating a K8s client for the CRD + var myCRD = Utils.MakeCRD(); + Console.WriteLine("working with CRD: {0}.{1}", myCRD.PluralName, myCRD.Group); + var generic = new GenericClient(k8SClientConfig, myCRD.Group, myCRD.Version, myCRD.PluralName); + + // creating a sample custom resource content + var myCr = Utils.MakeCResource(); + + try + { + Console.WriteLine("creating CR {0}", myCr.Metadata.Name); + var response = await client.CreateNamespacedCustomObjectWithHttpMessagesAsync( + myCr, + myCRD.Group, myCRD.Version, + myCr.Metadata.NamespaceProperty ?? "default", + myCRD.PluralName).ConfigureAwait(false); + } + catch (Microsoft.Rest.HttpOperationException httpOperationException) when (httpOperationException.Message.Contains("422")) + { + var phase = httpOperationException.Response.ReasonPhrase; + var content = httpOperationException.Response.Content; + Console.WriteLine("response content: {0}", content); + Console.WriteLine("response phase: {0}", phase); + } + catch (Microsoft.Rest.HttpOperationException) + { + } + + // listing the cr instances + Console.WriteLine("CR list:"); + var crs = await generic.ListNamespacedAsync>(myCr.Metadata.NamespaceProperty ?? "default").ConfigureAwait(false); + foreach (var cr in crs.Items) + { + Console.WriteLine("- CR Item {0} = {1}", crs.Items.IndexOf(cr), cr.Metadata.Name); + } + + // updating the custom resource + myCr.Metadata.Labels.TryAdd("newKey", "newValue"); + var patch = new JsonPatchDocument(); + patch.Replace(x => x.Metadata.Labels, myCr.Metadata.Labels); + patch.Operations.ForEach(x => x.path = x.path.ToLower()); + var crPatch = new V1Patch(patch, V1Patch.PatchType.JsonPatch); + try + { + var patchResponse = await client.PatchNamespacedCustomObjectAsync( + crPatch, + myCRD.Group, + myCRD.Version, + myCr.Metadata.NamespaceProperty ?? "default", + myCRD.PluralName, + myCr.Metadata.Name).ConfigureAwait(false); + } + catch (Microsoft.Rest.HttpOperationException httpOperationException) + { + var phase = httpOperationException.Response.ReasonPhrase; + var content = httpOperationException.Response.Content; + Console.WriteLine("response content: {0}", content); + Console.WriteLine("response phase: {0}", phase); + } + + // getting the updated custom resource + var fetchedCR = await generic.ReadNamespacedAsync( + myCr.Metadata.NamespaceProperty ?? "default", + myCr.Metadata.Name).ConfigureAwait(false); + + Console.WriteLine("fetchedCR = {0}", fetchedCR.ToString()); + + // deleting the custom resource + try + { + myCr = await generic.DeleteNamespacedAsync( + myCr.Metadata.NamespaceProperty ?? "default", + myCr.Metadata.Name).ConfigureAwait(false); + + Console.WriteLine("Deleted the CR"); + } + catch (Exception exception) + { + Console.WriteLine("Exception type {0}", exception); + } + } + } +} diff --git a/examples/customResource/README.md b/examples/customResource/README.md new file mode 100644 index 0000000..8ea9084 --- /dev/null +++ b/examples/customResource/README.md @@ -0,0 +1,53 @@ +# Custom Resource Client Example + +This example demonstrates how to use the C# Kubernetes Client library to create, get and list custom resources. + +## Pre-requisits + +Make sure your have added the library package + +```shell +dotnet add package KubernetesClient +``` + +## Create Custom Resource Definition (CRD) + +Make sure the [CRD](./config/crd.yaml) is created, in order to create an instance of it after. + +```shell +kubectl create -f ./crd.yaml +``` + +You can test that the CRD is successfully added, by creating an [instance](./config/yaml-cr-instance.yaml) of it using kubectl: + +```shell +kubectl create -f ./config/yaml-cr-instance.yaml +``` + +```shell +kubectl get customresources.csharp.com +``` + +## Execute the code + +The client uses the `BuildConfigFromConfigFile()` function. If the KUBECONFIG environment variable is set, then that path to the k8s config file will be used. + +`dotnet run` + +Expected output: + +``` +strating main()... +working with CRD: customresources.csharp.com +creating CR cr-instance-london +CR list: +- CR Item 0 = cr-instance-london +- CR Item 1 = cr-instance-paris +fetchedCR = cr-instance-london (Labels: {identifier : city, newKey : newValue}), Spec: London +Deleted the CR +``` + +## Under the hood + +For more details, you can look at the Generic client [implementation](https://github.com/kubernetes-client/csharp/blob/master/src/KubernetesClient/GenericClient.cs) + diff --git a/examples/customResource/Utils.cs b/examples/customResource/Utils.cs new file mode 100644 index 0000000..8101c1d --- /dev/null +++ b/examples/customResource/Utils.cs @@ -0,0 +1,48 @@ +using k8s.Models; +using System.Collections.Generic; +namespace customResource +{ + public class Utils + { + // creats a CRD definition + public static CustomResourceDefinition MakeCRD() + { + var myCRD = new CustomResourceDefinition() + { + Kind = "CResource", + Group = "csharp.com", + Version = "v1alpha1", + PluralName = "customresources", + }; + + return myCRD; + } + + // creats a CR instance + public static CResource MakeCResource() + { + var myCResource = new CResource() + { + Kind = "CResource", + ApiVersion = "csharp.com/v1alpha1", + Metadata = new V1ObjectMeta + { + Name = "cr-instance-london", + NamespaceProperty = "default", + Labels = new Dictionary + { + { + "identifier", "city" + }, + }, + }, + // spec + Spec = new CResourceSpec + { + CityName = "London", + }, + }; + return myCResource; + } + } +} diff --git a/examples/customResource/cResource.cs b/examples/customResource/cResource.cs new file mode 100644 index 0000000..a1e1414 --- /dev/null +++ b/examples/customResource/cResource.cs @@ -0,0 +1,35 @@ +using k8s.Models; +using System.Collections.Generic; +using k8s; +using Newtonsoft.Json; + +namespace customResource +{ + public class CResource : CustomResource + { + public override string ToString() + { + var labels = "{"; + foreach (var kvp in Metadata.Labels) + { + labels += kvp.Key + " : " + kvp.Value + ", "; + } + + labels = labels.TrimEnd(',', ' ') + "}"; + + return $"{Metadata.Name} (Labels: {labels}), Spec: {Spec.CityName}"; + } + } + + public class CResourceSpec + { + [JsonProperty(PropertyName = "cityName")] + public string CityName { get; set; } + } + + public class CResourceStatus : V1Status + { + [JsonProperty(PropertyName = "temperature", NullValueHandling = NullValueHandling.Ignore)] + public string Temperature { get; set; } + } +} diff --git a/examples/customResource/config/crd.yaml b/examples/customResource/config/crd.yaml new file mode 100644 index 0000000..23e27b6 --- /dev/null +++ b/examples/customResource/config/crd.yaml @@ -0,0 +1,11 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: customresources.csharp.com +spec: + group: csharp.com + version: v1alpha1 + names: + kind: CResource + plural: customresources + scope: Namespaced \ No newline at end of file diff --git a/examples/customResource/config/yaml-cr-instance.yaml b/examples/customResource/config/yaml-cr-instance.yaml new file mode 100644 index 0000000..482dd48 --- /dev/null +++ b/examples/customResource/config/yaml-cr-instance.yaml @@ -0,0 +1,8 @@ +apiVersion: csharp.com/v1alpha1 +kind: CResource +metadata: + name: cr-instance-paris + namespace: default +spec: + cityName: Paris + \ No newline at end of file diff --git a/examples/customResource/customResource.csproj b/examples/customResource/customResource.csproj new file mode 100644 index 0000000..da54913 --- /dev/null +++ b/examples/customResource/customResource.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + + + + + + + + + + diff --git a/kubernetes-client.sln b/kubernetes-client.sln index b087b68..750286f 100644 --- a/kubernetes-client.sln +++ b/kubernetes-client.sln @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2E.Tests", "tests\E2E.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkipTestLogger", "tests\SkipTestLogger\SkipTestLogger.csproj", "{4D2AE427-F856-49E5-B61D-EA6B17D89051}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "customResource", "examples\customResource\customResource.csproj", "{95672061-5799-4454-ACDB-D6D330DB1EC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -231,6 +233,18 @@ Global {4D2AE427-F856-49E5-B61D-EA6B17D89051}.Release|x64.Build.0 = Release|Any CPU {4D2AE427-F856-49E5-B61D-EA6B17D89051}.Release|x86.ActiveCfg = Release|Any CPU {4D2AE427-F856-49E5-B61D-EA6B17D89051}.Release|x86.Build.0 = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|x64.Build.0 = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Debug|x86.Build.0 = Debug|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|Any CPU.Build.0 = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|x64.ActiveCfg = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|x64.Build.0 = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|x86.ActiveCfg = Release|Any CPU + {95672061-5799-4454-ACDB-D6D330DB1EC4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -251,6 +265,7 @@ Global {B9647AD4-F6B0-406F-8B79-6781E31600EC} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40} {5056C4A2-5E12-4C16-8DA7-8835DA58BFF2} = {8AF4A5C2-F0CE-47D5-A4C5-FE4AB83CA509} {4D2AE427-F856-49E5-B61D-EA6B17D89051} = {8AF4A5C2-F0CE-47D5-A4C5-FE4AB83CA509} + {95672061-5799-4454-ACDB-D6D330DB1EC4} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {049A763A-C891-4E8D-80CF-89DD3E22ADC7}