diff --git a/src/KubernetesClient/ModelExtensions.cs b/src/KubernetesClient/ModelExtensions.cs index c559ebaf1..5b2f33d1f 100644 --- a/src/KubernetesClient/ModelExtensions.cs +++ b/src/KubernetesClient/ModelExtensions.cs @@ -1,7 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Net; namespace k8s.Models { + /// Adds convenient extensions for Kubernetes objects. + public static class ModelExtensions + { + /// Adds the given finalizer to a Kubernetes object if it doesn't already exist. + /// Returns true if the finalizer was added and false if it already existed. + public static bool AddFinalizer(this IMetadata obj, string finalizer) + { + if (string.IsNullOrEmpty(finalizer)) + { + throw new ArgumentNullException(nameof(finalizer)); + } + if (EnsureMetadata(obj).Finalizers == null) + { + obj.Metadata.Finalizers = new List(); + } + if (!obj.Metadata.Finalizers.Contains(finalizer)) + { + obj.Metadata.Finalizers.Add(finalizer); + return true; + } + return false; + } + + /// Extracts the Kubernetes API group from the . + public static string ApiGroup(this IKubernetesObject obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj.ApiVersion != null) + { + int slash = obj.ApiVersion.IndexOf('/'); + return slash < 0 ? string.Empty : obj.ApiVersion.Substring(0, slash); + } + return null; + } + + /// Extracts the Kubernetes API version (excluding the group) from the . + public static string ApiGroupVersion(this IKubernetesObject obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj.ApiVersion != null) + { + int slash = obj.ApiVersion.IndexOf('/'); + return slash < 0 ? obj.ApiVersion : obj.ApiVersion.Substring(slash+1); + } + return null; + } + + /// Splits the Kubernetes API version into the group and version. + public static (string, string) ApiGroupAndVersion(this IKubernetesObject obj) + { + string group, version; + GetApiGroupAndVersion(obj, out group, out version); + return (group, version); + } + + /// Splits the Kubernetes API version into the group and version. + public static void GetApiGroupAndVersion(this IKubernetesObject obj, out string group, out string version) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj.ApiVersion == null) + { + group = version = null; + } + else + { + int slash = obj.ApiVersion.IndexOf('/'); + if (slash < 0) (group, version) = (string.Empty, obj.ApiVersion); + else (group, version) = (obj.ApiVersion.Substring(0, slash), obj.ApiVersion.Substring(slash+1)); + } + } + + /// Gets the continuation token version of a Kubernetes list. + public static string Continue(this IMetadata list) => list.Metadata?.ContinueProperty; + + /// Ensures that the metadata field is set, and returns it. + public static V1ListMeta EnsureMetadata(this IMetadata obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj.Metadata == null) + { + obj.Metadata = new V1ListMeta(); + } + return obj.Metadata; + } + + /// Gets the resource version of a Kubernetes list. + public static string ResourceVersion(this IMetadata list) => list.Metadata?.ResourceVersion; + + /// Adds an owner reference to the object. No attempt is made to ensure the reference is correct or fits with the + /// other references. + /// + public static void AddOwnerReference(this IMetadata obj, V1OwnerReference ownerRef) + { + if (ownerRef == null) + { + throw new ArgumentNullException(nameof(ownerRef)); + } + if (EnsureMetadata(obj).OwnerReferences == null) + { + obj.Metadata.OwnerReferences = new List(); + } + obj.Metadata.OwnerReferences.Add(ownerRef); + } + + /// Gets the annotations of a Kubernetes object. + public static IDictionary Annotations(this IMetadata obj) => obj.Metadata?.Annotations; + + /// Gets the creation time of a Kubernetes object, or null if it hasn't been created yet. + public static DateTime? CreationTimestamp(this IMetadata obj) => obj.Metadata?.CreationTimestamp; + + /// Gets the deletion time of a Kubernetes object, or null if it hasn't been scheduled for deletion. + public static DateTime? DeletionTimestamp(this IMetadata obj) => obj.Metadata?.DeletionTimestamp; + + /// Ensures that the metadata field is set, and returns it. + public static V1ObjectMeta EnsureMetadata(this IMetadata obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj.Metadata == null) + { + obj.Metadata = new V1ObjectMeta(); + } + return obj.Metadata; + } + + /// Gets the of a Kubernetes object. + public static IList Finalizers(this IMetadata obj) => obj.Metadata?.Finalizers; + + /// Gets the index of the that matches the given object, or -1 if no such + /// reference could be found. + /// + public static int FindOwnerReference(this IMetadata obj, IKubernetesObject owner) => + FindOwnerReference(obj, r => r.Matches(owner)); + + /// Gets the index of the that matches the given predicate, or -1 if no such + /// reference could be found. + /// + public static int FindOwnerReference(this IMetadata obj, Predicate predicate) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + var ownerRefs = obj.OwnerReferences(); + if (ownerRefs != null) + { + for (int i = 0; i < ownerRefs.Count; i++) + { + if (predicate(ownerRefs[i])) return i; + } + } + return -1; + } + + /// Gets the generation a Kubernetes object. + public static long? Generation(this IMetadata obj) => obj.Metadata?.Generation; + + /// Returns the given annotation from a Kubernetes object or null if the annotation was not found. + public static string GetAnnotation(this IMetadata obj, string key) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + IDictionary annotations = obj.Annotations(); + return annotations != null && annotations.TryGetValue(key, out string value) ? value : null; + } + + /// Gets the for the controller of this object, or null if it couldn't be found. + public static V1OwnerReference GetController(this IMetadata obj) => + obj.OwnerReferences()?.FirstOrDefault(r => r.Controller.GetValueOrDefault()); + + /// Returns the given label from a Kubernetes object or null if the label was not found. + public static string GetLabel(this IMetadata obj, string key) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + IDictionary labels = obj.Labels(); + return labels != null && labels.TryGetValue(key, out string value) ? value : null; + } + + /// Gets that matches the given object, or null if no matching reference exists. + public static V1OwnerReference GetOwnerReference(this IMetadata obj, IKubernetesObject owner) => + GetOwnerReference(obj, r => r.Matches(owner)); + + /// Gets the that matches the given predicate, or null if no matching reference exists. + public static V1OwnerReference GetOwnerReference(this IMetadata obj, Predicate predicate) + { + int index = FindOwnerReference(obj, predicate); + return index >= 0 ? obj.Metadata.OwnerReferences[index] : null; + } + + /// Determines whether the Kubernetes object has the given finalizer. + public static bool HasFinalizer(this IMetadata obj, string finalizer) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (string.IsNullOrEmpty(finalizer)) + { + throw new ArgumentNullException(nameof(finalizer)); + } + + return obj.Finalizers() != null && obj.Metadata.Finalizers.Contains(finalizer); + } + + /// Determines whether one object is owned by another. + public static bool IsOwnedBy(this IMetadata obj, IKubernetesObject owner) => + FindOwnerReference(obj, owner) >= 0; + + /// Gets the labels of a Kubernetes object. + public static IDictionary Labels(this IMetadata obj) => obj.Metadata?.Labels; + + /// Gets the name of a Kubernetes object. + public static string Name(this IMetadata obj) => obj.Metadata?.Name; + + /// Gets the namespace of a Kubernetes object. + public static string Namespace(this IMetadata obj) => obj.Metadata?.NamespaceProperty; + + /// Gets the owner references of a Kubernetes object. + public static IList OwnerReferences(this IMetadata obj) => obj.Metadata?.OwnerReferences; + + /// Removes the given finalizer from a Kubernetes object if it exists. + /// Returns true if the finalizer was removed and false if it didn't exist. + public static bool RemoveFinalizer(this IMetadata obj, string finalizer) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (string.IsNullOrEmpty(finalizer)) + { + throw new ArgumentNullException(nameof(finalizer)); + } + + return obj.Finalizers() != null && obj.Metadata.Finalizers.Remove(finalizer); + } + + /// Removes the first that matches the given object and returns it, or returns null if no + /// matching reference could be found. + /// + public static V1OwnerReference RemoveOwnerReference(this IMetadata obj, IKubernetesObject owner) + { + int index = FindOwnerReference(obj, owner); + V1OwnerReference ownerRef = index >= 0 ? obj.Metadata.OwnerReferences[index] : null; + if (index >= 0) + { + obj.Metadata.OwnerReferences.RemoveAt(index); + } + return ownerRef; + } + + /// Removes all owner references that match the given predicate, and returns true if + /// any were removed. + /// + public static bool RemoveOwnerReferences(this IMetadata obj, Predicate predicate) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + bool removed = false; + IList refs = obj.Metadata?.OwnerReferences; + if (refs != null) + { + for (int i = refs.Count-1; i >= 0; i--) + { + if (predicate(refs[i])) + { + refs.RemoveAt(i); + removed = true; + } + } + } + return removed; + } + + /// Removes all owner references that match the given object, and returns true if + /// any were removed. + /// + public static bool RemoveOwnerReferences(this IMetadata obj, IKubernetesObject owner) => + RemoveOwnerReferences(obj, r => r.Matches(owner)); + + /// Gets the resource version of a Kubernetes object. + public static string ResourceVersion(this IMetadata obj) => obj.Metadata?.ResourceVersion; + + /// Sets or removes an annotation on a Kubernetes object. + public static void SetAnnotation(this IMetadata obj, string key, string value) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value != null) + { + obj.EnsureMetadata().EnsureAnnotations()[key] = value; + } + else + { + obj.Metadata?.Annotations?.Remove(key); + } + } + + /// Sets or removes a label on a Kubernetes object. + public static void SetLabel(this IMetadata obj, string key, string value) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value != null) + { + obj.EnsureMetadata().EnsureLabels()[key] = value; + } + else + { + obj.Metadata?.Labels?.Remove(key); + } + } + + /// Gets the unique ID of a Kubernetes object. + public static string Uid(this IMetadata obj) => obj.Metadata?.Uid; + + /// Ensures that the field is not null, and returns it. + public static IDictionary EnsureAnnotations(this V1ObjectMeta meta) + { + if (meta == null) + { + throw new ArgumentNullException(nameof(meta)); + } + if (meta.Annotations == null) + { + meta.Annotations = new Dictionary(); + } + return meta.Annotations; + } + + /// Ensures that the field is not null, and returns it. + public static IList EnsureFinalizers(this V1ObjectMeta meta) + { + if (meta == null) + { + throw new ArgumentNullException(nameof(meta)); + } + if (meta.Finalizers == null) + { + meta.Finalizers = new List(); + } + return meta.Finalizers; + } + + /// Ensures that the field is not null, and returns it. + public static IDictionary EnsureLabels(this V1ObjectMeta meta) + { + if (meta == null) + { + throw new ArgumentNullException(nameof(meta)); + } + if (meta.Labels == null) + { + meta.Labels = new Dictionary(); + } + return meta.Labels; + } + + /// Gets the namespace from Kubernetes metadata. + public static string Namespace(this V1ObjectMeta meta) => meta.NamespaceProperty; + + /// Sets the namespace from Kubernetes metadata. + public static void SetNamespace(this V1ObjectMeta meta, string ns) => meta.NamespaceProperty = ns; + + /// Determines whether an object reference references the given object. + public static bool Matches(this V1ObjectReference objref, IKubernetesObject obj) + { + if (objref == null) + { + throw new ArgumentNullException(nameof(objref)); + } + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + return objref.ApiVersion == obj.ApiVersion && objref.Kind == obj.Kind && objref.Name == obj.Name() && objref.Uid == obj.Uid() && + objref.NamespaceProperty == obj.Namespace(); + } + + /// Determines whether an owner reference references the given object. + public static bool Matches(this V1OwnerReference owner, IKubernetesObject obj) + { + if (owner == null) + { + throw new ArgumentNullException(nameof(owner)); + } + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + return owner.ApiVersion == obj.ApiVersion && owner.Kind == obj.Kind && owner.Name == obj.Name() && owner.Uid == obj.Uid(); + } + } + public partial class V1Status { /// Converts a object into a short description of the status. diff --git a/tests/KubernetesClient.Tests/ModelExtensionTests.cs b/tests/KubernetesClient.Tests/ModelExtensionTests.cs index e3bcf9910..9ea153528 100644 --- a/tests/KubernetesClient.Tests/ModelExtensionTests.cs +++ b/tests/KubernetesClient.Tests/ModelExtensionTests.cs @@ -1,3 +1,4 @@ +using System; using k8s.Models; using Xunit; @@ -5,6 +6,154 @@ namespace k8s.Tests { public class ModelExtensionTests { + [Fact] + public void TestMetadata() + { + // test getters on null metadata + var pod = new V1Pod(); + Assert.Null(pod.Annotations()); + Assert.Null(pod.ApiGroup()); + var (g, v) = pod.ApiGroupAndVersion(); + Assert.Null(g); + Assert.Null(v); + Assert.Null(pod.ApiGroupVersion()); + Assert.Null(pod.CreationTimestamp()); + Assert.Null(pod.DeletionTimestamp()); + Assert.Null(pod.Finalizers()); + Assert.Equal(-1, pod.FindOwnerReference(r => true)); + Assert.Null(pod.Generation()); + Assert.Null(pod.GetAnnotation("x")); + Assert.Null(pod.GetController()); + Assert.Null(pod.GetLabel("x")); + Assert.Null(pod.GetOwnerReference(r => true)); + Assert.False(pod.HasFinalizer("x")); + Assert.Null(pod.Labels()); + Assert.Null(pod.Name()); + Assert.Null(pod.Namespace()); + Assert.Null(pod.OwnerReferences()); + Assert.Null(pod.ResourceVersion()); + Assert.Null(pod.Uid()); + Assert.Null(pod.Metadata); + + // test API version stuff + pod = new V1Pod() { ApiVersion = "v1" }; + Assert.Equal("", pod.ApiGroup()); + (g, v) = pod.ApiGroupAndVersion(); + Assert.Equal("", g); + Assert.Equal("v1", v); + Assert.Equal("v1", pod.ApiGroupVersion()); + pod.ApiVersion = "abc/v2"; + Assert.Equal("abc", pod.ApiGroup()); + (g, v) = pod.ApiGroupAndVersion(); + Assert.Equal("abc", g); + Assert.Equal("v2", v); + Assert.Equal("v2", pod.ApiGroupVersion()); + + // test the Ensure*() functions + Assert.NotNull(pod.EnsureMetadata()); + Assert.NotNull(pod.Metadata); + Assert.NotNull(pod.Metadata.EnsureAnnotations()); + Assert.NotNull(pod.Metadata.Annotations); + Assert.NotNull(pod.Metadata.EnsureFinalizers()); + Assert.NotNull(pod.Metadata.Finalizers); + Assert.NotNull(pod.Metadata.EnsureLabels()); + Assert.NotNull(pod.Metadata.Labels); + + // test getters with non-null values + DateTime ts = DateTime.UtcNow, ts2 = DateTime.Now; + pod.Metadata = new V1ObjectMeta() + { + CreationTimestamp = ts, DeletionTimestamp = ts2, Generation = 1, Name = "name", NamespaceProperty = "ns", ResourceVersion = "42", Uid = "id" + }; + Assert.Equal(ts, pod.CreationTimestamp().Value); + Assert.Equal(ts2, pod.DeletionTimestamp().Value); + Assert.Equal(1, pod.Generation().Value); + Assert.Equal("name", pod.Name()); + Assert.Equal("ns", pod.Namespace()); + Assert.Equal("42", pod.ResourceVersion()); + Assert.Equal("id", pod.Uid()); + + // test annotations and labels + pod.SetAnnotation("x", "y"); + pod.SetLabel("a", "b"); + Assert.Equal(1, pod.Annotations().Count); + Assert.Equal(1, pod.Labels().Count); + Assert.Equal("y", pod.GetAnnotation("x")); + Assert.Equal("y", pod.Metadata.Annotations["x"]); + Assert.Null(pod.GetAnnotation("a")); + Assert.Equal("b", pod.GetLabel("a")); + Assert.Equal("b", pod.Metadata.Labels["a"]); + Assert.Null(pod.GetLabel("x")); + pod.SetAnnotation("x", null); + Assert.Equal(0, pod.Annotations().Count); + pod.SetLabel("a", null); + Assert.Equal(0, pod.Labels().Count); + + // test finalizers + Assert.False(pod.HasFinalizer("abc")); + Assert.True(pod.AddFinalizer("abc")); + Assert.True(pod.HasFinalizer("abc")); + Assert.False(pod.AddFinalizer("abc")); + Assert.False(pod.HasFinalizer("xyz")); + Assert.False(pod.RemoveFinalizer("xyz")); + Assert.True(pod.RemoveFinalizer("abc")); + Assert.False(pod.HasFinalizer("abc")); + Assert.False(pod.RemoveFinalizer("abc")); + } + + [Fact] + public void TestReferences() + { + // test object references + var pod = new V1Pod() { ApiVersion = "v1", Kind = "Pod" }; + pod.Metadata = new V1ObjectMeta() { Name = "name", NamespaceProperty = "ns", ResourceVersion = "ver", Uid = "id" }; + + var objr = new V1ObjectReference() { ApiVersion = pod.ApiVersion, Kind = pod.Kind, Name = pod.Name(), NamespaceProperty = pod.Namespace(), Uid = pod.Uid() }; + Assert.True(objr.Matches(pod)); + + (pod.ApiVersion, pod.Kind) = (null, null); + Assert.False(objr.Matches(pod)); + (pod.ApiVersion, pod.Kind) = ("v1", "Pod"); + Assert.True(objr.Matches(pod)); + pod.Metadata.Name = "nome"; + Assert.False(objr.Matches(pod)); + + // test owner references + (pod.ApiVersion, pod.Kind) = ("abc/xyz", "sometimes"); + var ownr = new V1OwnerReference() { ApiVersion = "abc/xyz", Kind = "sometimes", Name = pod.Name(), Uid = pod.Uid() }; + Assert.True(ownr.Matches(pod)); + + (pod.ApiVersion, pod.Kind) = (null, null); + Assert.False(ownr.Matches(pod)); + (ownr.ApiVersion, ownr.Kind) = ("v1", "Pod"); + Assert.False(ownr.Matches(pod)); + (pod.ApiVersion, pod.Kind) = (ownr.ApiVersion, ownr.Kind); + Assert.True(ownr.Matches(pod)); + ownr.Name = "nim"; + Assert.False(ownr.Matches(pod)); + ownr.Name = pod.Name(); + + var svc = new V1Service(); + svc.AddOwnerReference(ownr); + Assert.Equal(0, svc.FindOwnerReference(pod)); + Assert.Equal(-1, svc.FindOwnerReference(svc)); + Assert.Same(ownr, svc.GetOwnerReference(pod)); + Assert.Null(svc.GetOwnerReference(svc)); + Assert.Null(svc.GetController()); + svc.OwnerReferences()[0].Controller = true; + Assert.Same(ownr, svc.GetController()); + Assert.Same(ownr, svc.RemoveOwnerReference(pod)); + Assert.Equal(0, svc.OwnerReferences().Count); + svc.AddOwnerReference(new V1OwnerReference() { ApiVersion = pod.ApiVersion, Kind = pod.Kind, Name = pod.Name(), Uid = pod.Uid(), Controller = true }); + svc.AddOwnerReference(new V1OwnerReference() { ApiVersion = pod.ApiVersion, Kind = pod.Kind, Name = pod.Name(), Uid = pod.Uid(), Controller = false }); + svc.AddOwnerReference(new V1OwnerReference() { ApiVersion = pod.ApiVersion, Kind = pod.Kind, Name = pod.Name(), Uid = pod.Uid() }); + Assert.Equal(3, svc.OwnerReferences().Count); + Assert.NotNull(svc.RemoveOwnerReference(pod)); + Assert.Equal(2, svc.OwnerReferences().Count); + Assert.True(svc.RemoveOwnerReferences(pod)); + Assert.Equal(0, svc.OwnerReferences().Count); + } + [Fact] public void TestV1Status() {