diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Patch.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Patch.cs new file mode 100644 index 00000000..d6f8b24d --- /dev/null +++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Patch.cs @@ -0,0 +1,41 @@ +using k8s.Models; + +namespace k8s.kubectl.beta; + +public partial class AsyncKubectl +{ + /// + /// Patch a cluster-scoped Kubernetes resource. + /// + /// The type of Kubernetes resource to patch. + /// The patch to apply. + /// The name of the resource. + /// Cancellation token. + /// The patched resource. + public async Task PatchAsync(V1Patch patch, string name, CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + var metadata = typeof(T).GetKubernetesTypeMetadata(); + using var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false); + + return await genericClient.PatchAsync(patch, name, cancellationToken).ConfigureAwait(false); + } + + /// + /// Patch a namespaced Kubernetes resource. + /// + /// The type of Kubernetes resource to patch. + /// The patch to apply. + /// The namespace of the resource. + /// The name of the resource. + /// Cancellation token. + /// The patched resource. + public async Task PatchNamespacedAsync(V1Patch patch, string @namespace, string name, CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + var metadata = typeof(T).GetKubernetesTypeMetadata(); + using var genericClient = new GenericClient(client, metadata.Group, metadata.ApiVersion, metadata.PluralName, disposeClient: false); + + return await genericClient.PatchNamespacedAsync(patch, @namespace, name, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Patch.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Patch.cs new file mode 100644 index 00000000..de12a9f2 --- /dev/null +++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Patch.cs @@ -0,0 +1,33 @@ +using k8s.Models; + +namespace k8s.kubectl.beta; + +public partial class Kubectl +{ + /// + /// Patch a cluster-scoped Kubernetes resource. + /// + /// The type of Kubernetes resource to patch. + /// The patch to apply. + /// The name of the resource. + /// The patched resource. + public T Patch(V1Patch patch, string name) + where T : IKubernetesObject + { + return client.PatchAsync(patch, name).GetAwaiter().GetResult(); + } + + /// + /// Patch a namespaced Kubernetes resource. + /// + /// The type of Kubernetes resource to patch. + /// The patch to apply. + /// The namespace of the resource. + /// The name of the resource. + /// The patched resource. + public T PatchNamespaced(V1Patch patch, string @namespace, string name) + where T : IKubernetesObject + { + return client.PatchNamespacedAsync(patch, @namespace, name).GetAwaiter().GetResult(); + } +} diff --git a/tests/Kubectl.Tests/KubectlTests.Patch.cs b/tests/Kubectl.Tests/KubectlTests.Patch.cs new file mode 100644 index 00000000..729e70ca --- /dev/null +++ b/tests/Kubectl.Tests/KubectlTests.Patch.cs @@ -0,0 +1,310 @@ +using k8s.Autorest; +using k8s.E2E; +using k8s.kubectl.beta; +using k8s.Models; +using Xunit; + +namespace k8s.kubectl.Tests; + +public partial class KubectlTests +{ + [MinikubeFact] + public void PatchConfigMapWithStrategicMergePatch() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var configMapName = "k8scsharp-e2e-patch-strategic"; + + // Create a test ConfigMap + var configMap = new V1ConfigMap + { + Metadata = new V1ObjectMeta + { + Name = configMapName, + NamespaceProperty = namespaceParameter, + }, + Data = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedConfigMap(configMap, namespaceParameter); + + // Patch the ConfigMap using strategic merge patch + var patchData = new + { + data = new Dictionary + { + { "key3", "value3" }, + }, + }; + + var patch = new V1Patch(patchData, V1Patch.PatchType.StrategicMergePatch); + var patchedConfigMap = client.PatchNamespaced(patch, namespaceParameter, configMapName); + + Assert.NotNull(patchedConfigMap); + Assert.Equal(configMapName, patchedConfigMap.Metadata.Name); + Assert.Equal(3, patchedConfigMap.Data.Count); + Assert.Equal("value1", patchedConfigMap.Data["key1"]); + Assert.Equal("value2", patchedConfigMap.Data["key2"]); + Assert.Equal("value3", patchedConfigMap.Data["key3"]); + + // Explicitly get the resource to validate it was correctly patched + var retrievedConfigMap = client.Get(configMapName, namespaceParameter); + + Assert.NotNull(retrievedConfigMap); + Assert.Equal(configMapName, retrievedConfigMap.Metadata.Name); + Assert.Equal(3, retrievedConfigMap.Data.Count); + Assert.Equal("value1", retrievedConfigMap.Data["key1"]); + Assert.Equal("value2", retrievedConfigMap.Data["key2"]); + Assert.Equal("value3", retrievedConfigMap.Data["key3"]); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedConfigMap(configMapName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void PatchConfigMapWithMergePatch() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var configMapName = "k8scsharp-e2e-patch-merge"; + + // Create a test ConfigMap + var configMap = new V1ConfigMap + { + Metadata = new V1ObjectMeta + { + Name = configMapName, + NamespaceProperty = namespaceParameter, + Labels = new Dictionary + { + { "app", "test" }, + }, + }, + Data = new Dictionary + { + { "key1", "value1" }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedConfigMap(configMap, namespaceParameter); + + // Patch the ConfigMap using merge patch + var patchData = new + { + metadata = new + { + labels = new Dictionary + { + { "app", "test" }, + { "environment", "testing" }, + }, + }, + data = new Dictionary + { + { "key1", "updatedValue1" }, + { "key2", "value2" }, + }, + }; + + var patch = new V1Patch(patchData, V1Patch.PatchType.MergePatch); + var patchedConfigMap = client.PatchNamespaced(patch, namespaceParameter, configMapName); + + Assert.NotNull(patchedConfigMap); + Assert.Equal(configMapName, patchedConfigMap.Metadata.Name); + Assert.Equal(2, patchedConfigMap.Metadata.Labels.Count); + Assert.Equal("test", patchedConfigMap.Metadata.Labels["app"]); + Assert.Equal("testing", patchedConfigMap.Metadata.Labels["environment"]); + Assert.Equal(2, patchedConfigMap.Data.Count); + Assert.Equal("updatedValue1", patchedConfigMap.Data["key1"]); + Assert.Equal("value2", patchedConfigMap.Data["key2"]); + + // Explicitly get the resource to validate it was correctly patched + var retrievedConfigMap = client.Get(configMapName, namespaceParameter); + + Assert.NotNull(retrievedConfigMap); + Assert.Equal(configMapName, retrievedConfigMap.Metadata.Name); + Assert.Equal(2, retrievedConfigMap.Metadata.Labels.Count); + Assert.Equal("test", retrievedConfigMap.Metadata.Labels["app"]); + Assert.Equal("testing", retrievedConfigMap.Metadata.Labels["environment"]); + Assert.Equal(2, retrievedConfigMap.Data.Count); + Assert.Equal("updatedValue1", retrievedConfigMap.Data["key1"]); + Assert.Equal("value2", retrievedConfigMap.Data["key2"]); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedConfigMap(configMapName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void PatchConfigMapWithJsonPatch() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var configMapName = "k8scsharp-e2e-patch-json"; + + // Create a test ConfigMap + var configMap = new V1ConfigMap + { + Metadata = new V1ObjectMeta + { + Name = configMapName, + NamespaceProperty = namespaceParameter, + }, + Data = new Dictionary + { + { "key1", "value1" }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespacedConfigMap(configMap, namespaceParameter); + + // Patch the ConfigMap using JSON patch + var patchData = new[] + { + new + { + op = "replace", + path = "/data/key1", + value = "updatedValue1", + }, + new + { + op = "add", + path = "/data/key2", + value = "value2", + }, + }; + + var patch = new V1Patch(patchData, V1Patch.PatchType.JsonPatch); + var patchedConfigMap = client.PatchNamespaced(patch, namespaceParameter, configMapName); + + Assert.NotNull(patchedConfigMap); + Assert.Equal(configMapName, patchedConfigMap.Metadata.Name); + Assert.Equal(2, patchedConfigMap.Data.Count); + Assert.Equal("updatedValue1", patchedConfigMap.Data["key1"]); + Assert.Equal("value2", patchedConfigMap.Data["key2"]); + + // Explicitly get the resource to validate it was correctly patched + var retrievedConfigMap = client.Get(configMapName, namespaceParameter); + + Assert.NotNull(retrievedConfigMap); + Assert.Equal(configMapName, retrievedConfigMap.Metadata.Name); + Assert.Equal(2, retrievedConfigMap.Data.Count); + Assert.Equal("updatedValue1", retrievedConfigMap.Data["key1"]); + Assert.Equal("value2", retrievedConfigMap.Data["key2"]); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespacedConfigMap(configMapName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void PatchNamespace() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceName = "k8scsharp-e2e-patch-ns"; + + // Create a test namespace + var ns = new V1Namespace + { + Metadata = new V1ObjectMeta + { + Name = namespaceName, + Labels = new Dictionary + { + { "app", "test" }, + }, + }, + }; + + try + { + kubernetes.CoreV1.CreateNamespace(ns); + + // Patch the namespace (cluster-scoped resource) + var patchData = new + { + metadata = new + { + labels = new Dictionary + { + { "app", "test" }, + { "patched", "true" }, + }, + }, + }; + + var patch = new V1Patch(patchData, V1Patch.PatchType.MergePatch); + var patchedNamespace = client.Patch(patch, namespaceName); + + Assert.NotNull(patchedNamespace); + Assert.Equal(namespaceName, patchedNamespace.Metadata.Name); + Assert.Equal(2, patchedNamespace.Metadata.Labels.Count); + Assert.Equal("test", patchedNamespace.Metadata.Labels["app"]); + Assert.Equal("true", patchedNamespace.Metadata.Labels["patched"]); + + // Explicitly get the resource to validate it was correctly patched + var retrievedNamespace = client.Get(namespaceName); + + Assert.NotNull(retrievedNamespace); + Assert.Equal(namespaceName, retrievedNamespace.Metadata.Name); + Assert.Equal(2, retrievedNamespace.Metadata.Labels.Count); + Assert.Equal("test", retrievedNamespace.Metadata.Labels["app"]); + Assert.Equal("true", retrievedNamespace.Metadata.Labels["patched"]); + } + finally + { + // Cleanup + try + { + kubernetes.CoreV1.DeleteNamespace(namespaceName); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } +}