From e4325a57507f5ca69ddfc9be1079e03dad1130c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:14:18 +0000 Subject: [PATCH 1/5] Initial plan From 6d4973251caa15061b373c84d1dea07c0c56eb92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:31:19 +0000 Subject: [PATCH 2/5] Add KubectlRollout implementation with restart, status, history, undo, pause, and resume methods Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- .../Beta/AsyncKubectl.Rollout.cs | 461 ++++++++++++++++++ .../Beta/Kubectl.Rollout.cs | 131 +++++ 2 files changed, 592 insertions(+) create mode 100644 src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs create mode 100644 src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs new file mode 100644 index 00000000..27addee0 --- /dev/null +++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs @@ -0,0 +1,461 @@ +using Json.Patch; +using k8s.Models; +using System.Text.Json; + +namespace k8s.kubectl.beta; + +public partial class AsyncKubectl +{ + /// + /// Restart a Deployment by adding a restart annotation to trigger a rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutRestartDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var old = JsonSerializer.SerializeToDocument(deployment); + + // Add or update the restart annotation to trigger a rollout + deployment.Spec.Template.Metadata ??= new V1ObjectMeta(); + deployment.Spec.Template.Metadata.Annotations ??= new Dictionary(); + deployment.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + + var patch = old.CreatePatch(deployment); + + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Restart a DaemonSet by adding a restart annotation to trigger a rollout. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutRestartDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var old = JsonSerializer.SerializeToDocument(daemonSet); + + // Add or update the restart annotation to trigger a rollout + daemonSet.Spec.Template.Metadata ??= new V1ObjectMeta(); + daemonSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); + daemonSet.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + + var patch = old.CreatePatch(daemonSet); + + await client.AppsV1.PatchNamespacedDaemonSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Restart a StatefulSet by adding a restart annotation to trigger a rollout. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutRestartStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var old = JsonSerializer.SerializeToDocument(statefulSet); + + // Add or update the restart annotation to trigger a rollout + statefulSet.Spec.Template.Metadata ??= new V1ObjectMeta(); + statefulSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); + statefulSet.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + + var patch = old.CreatePatch(statefulSet); + + await client.AppsV1.PatchNamespacedStatefulSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Get the rollout status of a Deployment. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A string describing the rollout status. + public async Task RolloutStatusDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var status = deployment.Status; + var spec = deployment.Spec; + + if (status == null) + { + return "Waiting for deployment spec update to be observed..."; + } + + if (status.ObservedGeneration < deployment.Metadata.Generation) + { + return "Waiting for deployment spec update to be observed..."; + } + + if (status.UpdatedReplicas < spec.Replicas) + { + return $"Waiting for deployment \"{name}\" rollout to finish: {status.UpdatedReplicas ?? 0} out of {spec.Replicas ?? 0} new replicas have been updated..."; + } + + if (status.Replicas > status.UpdatedReplicas) + { + return $"Waiting for deployment \"{name}\" rollout to finish: {status.Replicas - status.UpdatedReplicas} old replicas are pending termination..."; + } + + if (status.AvailableReplicas < status.UpdatedReplicas) + { + return $"Waiting for deployment \"{name}\" rollout to finish: {status.AvailableReplicas ?? 0} of {status.UpdatedReplicas ?? 0} updated replicas are available..."; + } + + return $"deployment \"{name}\" successfully rolled out"; + } + + /// + /// Get the rollout status of a DaemonSet. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + /// Cancellation token. + /// A string describing the rollout status. + public async Task RolloutStatusDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var status = daemonSet.Status; + + if (status == null) + { + return "Waiting for daemon set spec update to be observed..."; + } + + if (status.ObservedGeneration < daemonSet.Metadata.Generation) + { + return "Waiting for daemon set spec update to be observed..."; + } + + if (status.UpdatedNumberScheduled < status.DesiredNumberScheduled) + { + return $"Waiting for daemon set \"{name}\" rollout to finish: {status.UpdatedNumberScheduled} out of {status.DesiredNumberScheduled} new pods have been updated..."; + } + + if (status.NumberAvailable < status.DesiredNumberScheduled) + { + return $"Waiting for daemon set \"{name}\" rollout to finish: {status.NumberAvailable ?? 0} of {status.DesiredNumberScheduled} updated pods are available..."; + } + + return $"daemon set \"{name}\" successfully rolled out"; + } + + /// + /// Get the rollout status of a StatefulSet. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + /// Cancellation token. + /// A string describing the rollout status. + public async Task RolloutStatusStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var status = statefulSet.Status; + var spec = statefulSet.Spec; + + if (status == null) + { + return "Waiting for statefulset spec update to be observed..."; + } + + if (status.ObservedGeneration < statefulSet.Metadata.Generation) + { + return "Waiting for statefulset spec update to be observed..."; + } + + if (spec.Replicas != null && status.ReadyReplicas < spec.Replicas) + { + return $"Waiting for {spec.Replicas - status.ReadyReplicas} pods to be ready..."; + } + + if (spec.UpdateStrategy?.Type == "RollingUpdate" && spec.UpdateStrategy.RollingUpdate != null) + { + if (spec.Replicas != null && spec.UpdateStrategy.RollingUpdate.Partition != null) + { + if (status.UpdatedReplicas < (spec.Replicas - spec.UpdateStrategy.RollingUpdate.Partition)) + { + return $"Waiting for partitioned roll out to finish: {status.UpdatedReplicas} out of {spec.Replicas - spec.UpdateStrategy.RollingUpdate.Partition} new pods have been updated..."; + } + } + } + + if (status.UpdateRevision != status.CurrentRevision) + { + return $"waiting for statefulset rolling update to complete {status.UpdatedReplicas} pods at revision {status.UpdateRevision}..."; + } + + return $"statefulset rolling update complete {status.CurrentReplicas} pods at revision {status.CurrentRevision}..."; + } + + /// + /// Pause a Deployment rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutPauseDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var old = JsonSerializer.SerializeToDocument(deployment); + + deployment.Spec.Paused = true; + + var patch = old.CreatePatch(deployment); + + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Resume a paused Deployment rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutResumeDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + var old = JsonSerializer.SerializeToDocument(deployment); + + deployment.Spec.Paused = false; + + var patch = old.CreatePatch(deployment); + + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Undo a Deployment rollout to a previous revision. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// The revision to roll back to. If 0 or not specified, rolls back to the previous revision. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutUndoDeploymentAsync(string name, string @namespace, long? toRevision = null, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Get all ReplicaSets for this deployment + var labelSelector = string.Join(",", deployment.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Filter ReplicaSets owned by this deployment + var ownedReplicaSets = replicaSets.Items + .Where(rs => rs.Metadata.OwnerReferences?.Any(or => or.Uid == deployment.Metadata.Uid) == true) + .OrderByDescending(rs => + { + if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + { + return long.TryParse(revisionStr, out var revision) ? revision : 0; + } + + return 0; + }) + .ToList(); + + if (ownedReplicaSets.Count == 0) + { + throw new InvalidOperationException($"No ReplicaSets found for deployment {name}"); + } + + V1ReplicaSet? targetReplicaSet; + + if (toRevision.HasValue && toRevision.Value > 0) + { + // Find specific revision + targetReplicaSet = ownedReplicaSets.FirstOrDefault(rs => + { + if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + { + return long.TryParse(revisionStr, out var revision) && revision == toRevision.Value; + } + + return false; + }); + + if (targetReplicaSet == null) + { + throw new InvalidOperationException($"Revision {toRevision} not found for deployment {name}"); + } + } + else + { + // Use previous revision (second in the list) + if (ownedReplicaSets.Count < 2) + { + throw new InvalidOperationException($"No previous revision found for deployment {name}"); + } + + targetReplicaSet = ownedReplicaSets[1]; + } + + // Update deployment with the template from the target ReplicaSet + var old = JsonSerializer.SerializeToDocument(deployment); + + deployment.Spec.Template = targetReplicaSet.Spec.Template; + + // Add annotation to record the rollback + deployment.Metadata.Annotations ??= new Dictionary(); + deployment.Metadata.Annotations["deployment.kubernetes.io/revision"] = + targetReplicaSet.Metadata.Annotations?["deployment.kubernetes.io/revision"] ?? "0"; + + var patch = old.CreatePatch(deployment); + + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Get the rollout history of a Deployment. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A list of revision history entries. + public async Task> RolloutHistoryDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Get all ReplicaSets for this deployment + var labelSelector = string.Join(",", deployment.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Filter and process ReplicaSets owned by this deployment + var history = replicaSets.Items + .Where(rs => rs.Metadata.OwnerReferences?.Any(or => or.Uid == deployment.Metadata.Uid) == true) + .Select(rs => + { + var revision = 0L; + if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + { + long.TryParse(revisionStr, out revision); + } + + var changeCause = ""; + if (rs.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + { + changeCause = cause; + } + + return new RolloutHistoryEntry + { + Revision = revision, + ChangeCause = changeCause, + }; + }) + .Where(entry => entry.Revision > 0) + .OrderBy(entry => entry.Revision) + .ToList(); + + return history; + } + + /// + /// Get the rollout history of a DaemonSet. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + /// Cancellation token. + /// A list of revision history entries. + public async Task> RolloutHistoryDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Get ControllerRevisions for this DaemonSet + var labelSelector = string.Join(",", daemonSet.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Filter and process ControllerRevisions owned by this DaemonSet + var history = controllerRevisions.Items + .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == daemonSet.Metadata.Uid) == true) + .Select(cr => + { + var changeCause = ""; + if (cr.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + { + changeCause = cause; + } + + return new RolloutHistoryEntry + { + Revision = cr.Revision, + ChangeCause = changeCause, + }; + }) + .OrderBy(entry => entry.Revision) + .ToList(); + + return history; + } + + /// + /// Get the rollout history of a StatefulSet. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + /// Cancellation token. + /// A list of revision history entries. + public async Task> RolloutHistoryStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + { + var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Get ControllerRevisions for this StatefulSet + var labelSelector = string.Join(",", statefulSet.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Filter and process ControllerRevisions owned by this StatefulSet + var history = controllerRevisions.Items + .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == statefulSet.Metadata.Uid) == true) + .Select(cr => + { + var changeCause = ""; + if (cr.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + { + changeCause = cause; + } + + return new RolloutHistoryEntry + { + Revision = cr.Revision, + ChangeCause = changeCause, + }; + }) + .OrderBy(entry => entry.Revision) + .ToList(); + + return history; + } +} + +/// +/// Represents a single entry in the rollout history. +/// +public class RolloutHistoryEntry +{ + /// + /// The revision number. + /// + public long Revision { get; set; } + + /// + /// The change cause annotation for this revision. + /// + public string ChangeCause { get; set; } = ""; +} diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs new file mode 100644 index 00000000..849ae890 --- /dev/null +++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs @@ -0,0 +1,131 @@ +namespace k8s.kubectl.beta; + +public partial class Kubectl +{ + /// + /// Restart a Deployment by adding a restart annotation to trigger a rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + public void RolloutRestartDeployment(string name, string @namespace) + { + client.RolloutRestartDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Restart a DaemonSet by adding a restart annotation to trigger a rollout. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + public void RolloutRestartDaemonSet(string name, string @namespace) + { + client.RolloutRestartDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Restart a StatefulSet by adding a restart annotation to trigger a rollout. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + public void RolloutRestartStatefulSet(string name, string @namespace) + { + client.RolloutRestartStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout status of a Deployment. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// A string describing the rollout status. + public string RolloutStatusDeployment(string name, string @namespace) + { + return client.RolloutStatusDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout status of a DaemonSet. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + /// A string describing the rollout status. + public string RolloutStatusDaemonSet(string name, string @namespace) + { + return client.RolloutStatusDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout status of a StatefulSet. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + /// A string describing the rollout status. + public string RolloutStatusStatefulSet(string name, string @namespace) + { + return client.RolloutStatusStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Pause a Deployment rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + public void RolloutPauseDeployment(string name, string @namespace) + { + client.RolloutPauseDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Resume a paused Deployment rollout. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + public void RolloutResumeDeployment(string name, string @namespace) + { + client.RolloutResumeDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Undo a Deployment rollout to a previous revision. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// The revision to roll back to. If 0 or not specified, rolls back to the previous revision. + public void RolloutUndoDeployment(string name, string @namespace, long? toRevision = null) + { + client.RolloutUndoDeploymentAsync(name, @namespace, toRevision).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout history of a Deployment. + /// + /// The name of the Deployment. + /// The namespace of the Deployment. + /// A list of revision history entries. + public IList RolloutHistoryDeployment(string name, string @namespace) + { + return client.RolloutHistoryDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout history of a DaemonSet. + /// + /// The name of the DaemonSet. + /// The namespace of the DaemonSet. + /// A list of revision history entries. + public IList RolloutHistoryDaemonSet(string name, string @namespace) + { + return client.RolloutHistoryDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); + } + + /// + /// Get the rollout history of a StatefulSet. + /// + /// The name of the StatefulSet. + /// The namespace of the StatefulSet. + /// A list of revision history entries. + public IList RolloutHistoryStatefulSet(string name, string @namespace) + { + return client.RolloutHistoryStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); + } +} From b814eaa357431fb3815b164097c74186225ba084 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:35:12 +0000 Subject: [PATCH 3/5] Add comprehensive tests for KubectlRollout functionality Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- tests/Kubectl.Tests/KubectlTests.Rollout.cs | 648 ++++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 tests/Kubectl.Tests/KubectlTests.Rollout.cs diff --git a/tests/Kubectl.Tests/KubectlTests.Rollout.cs b/tests/Kubectl.Tests/KubectlTests.Rollout.cs new file mode 100644 index 00000000..de96fee1 --- /dev/null +++ b/tests/Kubectl.Tests/KubectlTests.Rollout.cs @@ -0,0 +1,648 @@ +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 RolloutRestartDeployment() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var deploymentName = "k8scsharp-e2e-rollout-deployment"; + + // Create a test deployment + var deployment = new V1Deployment + { + Metadata = new V1ObjectMeta + { + Name = deploymentName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-rollout" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-rollout" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); + + // Wait a moment for the deployment to stabilize + System.Threading.Thread.Sleep(2000); + + // Restart the deployment + client.RolloutRestartDeployment(deploymentName, namespaceParameter); + + // Verify the restart annotation was added + var updatedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); + Assert.NotNull(updatedDeployment.Spec.Template.Metadata.Annotations); + Assert.True(updatedDeployment.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutStatusDeployment() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var deploymentName = "k8scsharp-e2e-rollout-status"; + + // Create a test deployment + var deployment = new V1Deployment + { + Metadata = new V1ObjectMeta + { + Name = deploymentName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-status" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-status" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); + + // Get rollout status + var status = client.RolloutStatusDeployment(deploymentName, namespaceParameter); + + // Status should contain the deployment name + Assert.Contains(deploymentName, status); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutPauseAndResumeDeployment() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var deploymentName = "k8scsharp-e2e-rollout-pause"; + + // Create a test deployment + var deployment = new V1Deployment + { + Metadata = new V1ObjectMeta + { + Name = deploymentName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-pause" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-pause" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); + + // Pause the deployment + client.RolloutPauseDeployment(deploymentName, namespaceParameter); + + // Verify the deployment is paused + var pausedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); + Assert.True(pausedDeployment.Spec.Paused); + + // Resume the deployment + client.RolloutResumeDeployment(deploymentName, namespaceParameter); + + // Verify the deployment is resumed + var resumedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); + Assert.False(resumedDeployment.Spec.Paused); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutHistoryDeployment() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var deploymentName = "k8scsharp-e2e-rollout-history"; + + // Create a test deployment with change-cause annotation + var deployment = new V1Deployment + { + Metadata = new V1ObjectMeta + { + Name = deploymentName, + NamespaceProperty = namespaceParameter, + Annotations = new Dictionary + { + { "kubernetes.io/change-cause", "Initial deployment" }, + }, + }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-history" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-history" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); + + // Wait for deployment to create ReplicaSets + System.Threading.Thread.Sleep(3000); + + // Get rollout history + var history = client.RolloutHistoryDeployment(deploymentName, namespaceParameter); + + // Should have at least one revision + Assert.NotNull(history); + Assert.NotEmpty(history); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDeployment(deploymentName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutRestartDaemonSet() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var daemonSetName = "k8scsharp-e2e-rollout-daemonset"; + + // Create a test daemonset + var daemonSet = new V1DaemonSet + { + Metadata = new V1ObjectMeta + { + Name = daemonSetName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1DaemonSetSpec + { + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-daemonset" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-daemonset" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + Tolerations = new[] + { + new V1Toleration + { + OperatorProperty = "Exists", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDaemonSet(daemonSet, namespaceParameter); + + // Wait a moment for the daemonset to stabilize + System.Threading.Thread.Sleep(2000); + + // Restart the daemonset + client.RolloutRestartDaemonSet(daemonSetName, namespaceParameter); + + // Verify the restart annotation was added + var updatedDaemonSet = kubernetes.AppsV1.ReadNamespacedDaemonSet(daemonSetName, namespaceParameter); + Assert.NotNull(updatedDaemonSet.Spec.Template.Metadata.Annotations); + Assert.True(updatedDaemonSet.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDaemonSet(daemonSetName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutStatusDaemonSet() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var daemonSetName = "k8scsharp-e2e-rollout-ds-status"; + + // Create a test daemonset + var daemonSet = new V1DaemonSet + { + Metadata = new V1ObjectMeta + { + Name = daemonSetName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1DaemonSetSpec + { + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-ds-status" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-ds-status" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + Tolerations = new[] + { + new V1Toleration + { + OperatorProperty = "Exists", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedDaemonSet(daemonSet, namespaceParameter); + + // Get rollout status + var status = client.RolloutStatusDaemonSet(daemonSetName, namespaceParameter); + + // Status should contain the daemonset name + Assert.Contains(daemonSetName, status); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedDaemonSet(daemonSetName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutRestartStatefulSet() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var statefulSetName = "k8scsharp-e2e-rollout-statefulset"; + + // Create a test statefulset + var statefulSet = new V1StatefulSet + { + Metadata = new V1ObjectMeta + { + Name = statefulSetName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1StatefulSetSpec + { + ServiceName = "test-service", + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-statefulset" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-statefulset" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedStatefulSet(statefulSet, namespaceParameter); + + // Wait a moment for the statefulset to stabilize + System.Threading.Thread.Sleep(2000); + + // Restart the statefulset + client.RolloutRestartStatefulSet(statefulSetName, namespaceParameter); + + // Verify the restart annotation was added + var updatedStatefulSet = kubernetes.AppsV1.ReadNamespacedStatefulSet(statefulSetName, namespaceParameter); + Assert.NotNull(updatedStatefulSet.Spec.Template.Metadata.Annotations); + Assert.True(updatedStatefulSet.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedStatefulSet(statefulSetName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } + + [MinikubeFact] + public void RolloutStatusStatefulSet() + { + using var kubernetes = MinikubeTests.CreateClient(); + var client = new Kubectl(kubernetes); + var namespaceParameter = "default"; + var statefulSetName = "k8scsharp-e2e-rollout-sts-status"; + + // Create a test statefulset + var statefulSet = new V1StatefulSet + { + Metadata = new V1ObjectMeta + { + Name = statefulSetName, + NamespaceProperty = namespaceParameter, + }, + Spec = new V1StatefulSetSpec + { + ServiceName = "test-service", + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary + { + { "app", "test-sts-status" }, + }, + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary + { + { "app", "test-sts-status" }, + }, + }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container + { + Name = "test", + Image = "nginx:latest", + }, + }, + }, + }, + }, + }; + + try + { + kubernetes.AppsV1.CreateNamespacedStatefulSet(statefulSet, namespaceParameter); + + // Get rollout status + var status = client.RolloutStatusStatefulSet(statefulSetName, namespaceParameter); + + // Status should contain the statefulset name or status information + Assert.NotNull(status); + Assert.NotEmpty(status); + } + finally + { + // Cleanup + try + { + kubernetes.AppsV1.DeleteNamespacedStatefulSet(statefulSetName, namespaceParameter); + } + catch (HttpOperationException) + { + // Ignore cleanup errors + } + } + } +} From a6da3f6e69adfe447ed18dbe583fd8e9a93c03f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:42:19 +0000 Subject: [PATCH 4/5] Address code review feedback - extract constants and shared methods Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- .../Beta/AsyncKubectl.Rollout.cs | 39 ++++++++++++------- tests/Kubectl.Tests/KubectlTests.Rollout.cs | 6 +-- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs index 27addee0..5591603b 100644 --- a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs +++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs @@ -6,6 +6,15 @@ namespace k8s.kubectl.beta; public partial class AsyncKubectl { + private const string RestartedAtAnnotation = "kubectl.kubernetes.io/restartedAt"; + private const string RevisionAnnotation = "deployment.kubernetes.io/revision"; + private const string ChangeCauseAnnotation = "kubernetes.io/change-cause"; + + private static string BuildLabelSelector(IDictionary matchLabels) + { + return string.Join(",", matchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + } + /// /// Restart a Deployment by adding a restart annotation to trigger a rollout. /// @@ -22,7 +31,7 @@ public async Task RolloutRestartDeploymentAsync(string name, string @namespace, // Add or update the restart annotation to trigger a rollout deployment.Spec.Template.Metadata ??= new V1ObjectMeta(); deployment.Spec.Template.Metadata.Annotations ??= new Dictionary(); - deployment.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + deployment.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); var patch = old.CreatePatch(deployment); @@ -45,7 +54,7 @@ public async Task RolloutRestartDaemonSetAsync(string name, string @namespace, C // Add or update the restart annotation to trigger a rollout daemonSet.Spec.Template.Metadata ??= new V1ObjectMeta(); daemonSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); - daemonSet.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + daemonSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); var patch = old.CreatePatch(daemonSet); @@ -68,7 +77,7 @@ public async Task RolloutRestartStatefulSetAsync(string name, string @namespace, // Add or update the restart annotation to trigger a rollout statefulSet.Spec.Template.Metadata ??= new V1ObjectMeta(); statefulSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); - statefulSet.Spec.Template.Metadata.Annotations["kubectl.kubernetes.io/restartedAt"] = DateTime.UtcNow.ToString("o"); + statefulSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); var patch = old.CreatePatch(statefulSet); @@ -254,7 +263,7 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); // Get all ReplicaSets for this deployment - var labelSelector = string.Join(",", deployment.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var labelSelector = BuildLabelSelector(deployment.Spec.Selector.MatchLabels); var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); // Filter ReplicaSets owned by this deployment @@ -262,7 +271,7 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon .Where(rs => rs.Metadata.OwnerReferences?.Any(or => or.Uid == deployment.Metadata.Uid) == true) .OrderByDescending(rs => { - if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + if (rs.Metadata.Annotations?.TryGetValue(RevisionAnnotation, out var revisionStr) == true) { return long.TryParse(revisionStr, out var revision) ? revision : 0; } @@ -283,7 +292,7 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon // Find specific revision targetReplicaSet = ownedReplicaSets.FirstOrDefault(rs => { - if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + if (rs.Metadata.Annotations?.TryGetValue(RevisionAnnotation, out var revisionStr) == true) { return long.TryParse(revisionStr, out var revision) && revision == toRevision.Value; } @@ -314,8 +323,8 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon // Add annotation to record the rollback deployment.Metadata.Annotations ??= new Dictionary(); - deployment.Metadata.Annotations["deployment.kubernetes.io/revision"] = - targetReplicaSet.Metadata.Annotations?["deployment.kubernetes.io/revision"] ?? "0"; + deployment.Metadata.Annotations[RevisionAnnotation] = + targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0"; var patch = old.CreatePatch(deployment); @@ -334,7 +343,7 @@ public async Task> RolloutHistoryDeploymentAsync(stri var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); // Get all ReplicaSets for this deployment - var labelSelector = string.Join(",", deployment.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var labelSelector = BuildLabelSelector(deployment.Spec.Selector.MatchLabels); var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); // Filter and process ReplicaSets owned by this deployment @@ -343,13 +352,13 @@ public async Task> RolloutHistoryDeploymentAsync(stri .Select(rs => { var revision = 0L; - if (rs.Metadata.Annotations?.TryGetValue("deployment.kubernetes.io/revision", out var revisionStr) == true) + if (rs.Metadata.Annotations?.TryGetValue(RevisionAnnotation, out var revisionStr) == true) { long.TryParse(revisionStr, out revision); } var changeCause = ""; - if (rs.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + if (rs.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) { changeCause = cause; } @@ -379,7 +388,7 @@ public async Task> RolloutHistoryDaemonSetAsync(strin var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); // Get ControllerRevisions for this DaemonSet - var labelSelector = string.Join(",", daemonSet.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var labelSelector = BuildLabelSelector(daemonSet.Spec.Selector.MatchLabels); var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); // Filter and process ControllerRevisions owned by this DaemonSet @@ -388,7 +397,7 @@ public async Task> RolloutHistoryDaemonSetAsync(strin .Select(cr => { var changeCause = ""; - if (cr.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) { changeCause = cause; } @@ -417,7 +426,7 @@ public async Task> RolloutHistoryStatefulSetAsync(str var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); // Get ControllerRevisions for this StatefulSet - var labelSelector = string.Join(",", statefulSet.Spec.Selector.MatchLabels.Select(kvp => $"{kvp.Key}={kvp.Value}")); + var labelSelector = BuildLabelSelector(statefulSet.Spec.Selector.MatchLabels); var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); // Filter and process ControllerRevisions owned by this StatefulSet @@ -426,7 +435,7 @@ public async Task> RolloutHistoryStatefulSetAsync(str .Select(cr => { var changeCause = ""; - if (cr.Metadata.Annotations?.TryGetValue("kubernetes.io/change-cause", out var cause) == true && !string.IsNullOrEmpty(cause)) + if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) { changeCause = cause; } diff --git a/tests/Kubectl.Tests/KubectlTests.Rollout.cs b/tests/Kubectl.Tests/KubectlTests.Rollout.cs index de96fee1..d5ba2c7e 100644 --- a/tests/Kubectl.Tests/KubectlTests.Rollout.cs +++ b/tests/Kubectl.Tests/KubectlTests.Rollout.cs @@ -71,7 +71,7 @@ public void RolloutRestartDeployment() // Verify the restart annotation was added var updatedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); Assert.NotNull(updatedDeployment.Spec.Template.Metadata.Annotations); - Assert.True(updatedDeployment.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedDeployment.Spec.Template.Metadata.Annotations.Keys); } finally { @@ -394,7 +394,7 @@ public void RolloutRestartDaemonSet() // Verify the restart annotation was added var updatedDaemonSet = kubernetes.AppsV1.ReadNamespacedDaemonSet(daemonSetName, namespaceParameter); Assert.NotNull(updatedDaemonSet.Spec.Template.Metadata.Annotations); - Assert.True(updatedDaemonSet.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedDaemonSet.Spec.Template.Metadata.Annotations.Keys); } finally { @@ -554,7 +554,7 @@ public void RolloutRestartStatefulSet() // Verify the restart annotation was added var updatedStatefulSet = kubernetes.AppsV1.ReadNamespacedStatefulSet(statefulSetName, namespaceParameter); Assert.NotNull(updatedStatefulSet.Spec.Template.Metadata.Annotations); - Assert.True(updatedStatefulSet.Spec.Template.Metadata.Annotations.ContainsKey("kubectl.kubernetes.io/restartedAt")); + Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedStatefulSet.Spec.Template.Metadata.Annotations.Keys); } finally { From f820e768e3edcacf68e2e883ff59fda2e6987082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 23:33:30 +0000 Subject: [PATCH 5/5] Refactor to use generic type parameters for rollout methods Changed API from separate methods per resource type to generic methods: - RolloutRestart, RolloutStatus, RolloutHistory - RolloutPause, RolloutResume, RolloutUndo Usage example: kubectl.RolloutRestart("my-app", "production"); kubectl.RolloutStatus("my-daemon", "default"); This provides a cleaner API that follows the pattern used by Get and List. Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- .../Beta/AsyncKubectl.Rollout.cs | 295 +++++++++--------- .../Beta/Kubectl.Rollout.cs | 118 ++----- tests/Kubectl.Tests/KubectlTests.Rollout.cs | 18 +- 3 files changed, 195 insertions(+), 236 deletions(-) diff --git a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs index 5591603b..d5cc63d3 100644 --- a/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs +++ b/src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs @@ -16,82 +16,188 @@ private static string BuildLabelSelector(IDictionary matchLabels } /// - /// Restart a Deployment by adding a restart annotation to trigger a rollout. + /// Restart a workload resource by adding a restart annotation to trigger a rollout. /// - /// The name of the Deployment. - /// The namespace of the Deployment. + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. /// Cancellation token. /// A task representing the asynchronous operation. - public async Task RolloutRestartDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + public async Task RolloutRestartAsync(string name, string @namespace, CancellationToken cancellationToken = default) + where T : IKubernetesObject { - var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + if (typeof(T) == typeof(V1Deployment)) + { + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var old = JsonSerializer.SerializeToDocument(deployment); - var old = JsonSerializer.SerializeToDocument(deployment); + deployment.Spec.Template.Metadata ??= new V1ObjectMeta(); + deployment.Spec.Template.Metadata.Annotations ??= new Dictionary(); + deployment.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); - // Add or update the restart annotation to trigger a rollout - deployment.Spec.Template.Metadata ??= new V1ObjectMeta(); - deployment.Spec.Template.Metadata.Annotations ??= new Dictionary(); - deployment.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); + var patch = old.CreatePatch(deployment); + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1DaemonSet)) + { + var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var old = JsonSerializer.SerializeToDocument(daemonSet); - var patch = old.CreatePatch(deployment); + daemonSet.Spec.Template.Metadata ??= new V1ObjectMeta(); + daemonSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); + daemonSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); - await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var patch = old.CreatePatch(daemonSet); + await client.AppsV1.PatchNamespacedDaemonSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1StatefulSet)) + { + var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var old = JsonSerializer.SerializeToDocument(statefulSet); + + statefulSet.Spec.Template.Metadata ??= new V1ObjectMeta(); + statefulSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); + statefulSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); + + var patch = old.CreatePatch(statefulSet); + await client.AppsV1.PatchNamespacedStatefulSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + throw new ArgumentException($"Unsupported resource type: {typeof(T).Name}. Only V1Deployment, V1DaemonSet, and V1StatefulSet are supported.", nameof(T)); + } } /// - /// Restart a DaemonSet by adding a restart annotation to trigger a rollout. + /// Get the rollout status of a workload resource. /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. /// Cancellation token. - /// A task representing the asynchronous operation. - public async Task RolloutRestartDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + /// A string describing the rollout status. + public async Task RolloutStatusAsync(string name, string @namespace, CancellationToken cancellationToken = default) + where T : IKubernetesObject { - var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + if (typeof(T) == typeof(V1Deployment)) + { + return await RolloutStatusDeploymentInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1DaemonSet)) + { + return await RolloutStatusDaemonSetInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1StatefulSet)) + { + return await RolloutStatusStatefulSetInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else + { + throw new ArgumentException($"Unsupported resource type: {typeof(T).Name}. Only V1Deployment, V1DaemonSet, and V1StatefulSet are supported.", nameof(T)); + } + } - var old = JsonSerializer.SerializeToDocument(daemonSet); + /// + /// Pause a Deployment rollout. + /// + /// The type of resource (must be V1Deployment). + /// The name of the Deployment. + /// The namespace of the Deployment. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task RolloutPauseAsync(string name, string @namespace, CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + if (typeof(T) != typeof(V1Deployment)) + { + throw new ArgumentException($"Pause is only supported for V1Deployment, not {typeof(T).Name}.", nameof(T)); + } - // Add or update the restart annotation to trigger a rollout - daemonSet.Spec.Template.Metadata ??= new V1ObjectMeta(); - daemonSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); - daemonSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var old = JsonSerializer.SerializeToDocument(deployment); - var patch = old.CreatePatch(daemonSet); + deployment.Spec.Paused = true; - await client.AppsV1.PatchNamespacedDaemonSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var patch = old.CreatePatch(deployment); + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); } /// - /// Restart a StatefulSet by adding a restart annotation to trigger a rollout. + /// Resume a paused Deployment rollout. /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. + /// The type of resource (must be V1Deployment). + /// The name of the Deployment. + /// The namespace of the Deployment. /// Cancellation token. /// A task representing the asynchronous operation. - public async Task RolloutRestartStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + public async Task RolloutResumeAsync(string name, string @namespace, CancellationToken cancellationToken = default) + where T : IKubernetesObject { - var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - - var old = JsonSerializer.SerializeToDocument(statefulSet); + if (typeof(T) != typeof(V1Deployment)) + { + throw new ArgumentException($"Resume is only supported for V1Deployment, not {typeof(T).Name}.", nameof(T)); + } - // Add or update the restart annotation to trigger a rollout - statefulSet.Spec.Template.Metadata ??= new V1ObjectMeta(); - statefulSet.Spec.Template.Metadata.Annotations ??= new Dictionary(); - statefulSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = DateTime.UtcNow.ToString("o"); + var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var old = JsonSerializer.SerializeToDocument(deployment); - var patch = old.CreatePatch(statefulSet); + deployment.Spec.Paused = false; - await client.AppsV1.PatchNamespacedStatefulSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); + var patch = old.CreatePatch(deployment); + await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); } /// - /// Get the rollout status of a Deployment. + /// Undo a Deployment rollout to a previous revision. /// + /// The type of resource (must be V1Deployment). /// The name of the Deployment. /// The namespace of the Deployment. + /// The revision to roll back to. If 0 or not specified, rolls back to the previous revision. /// Cancellation token. - /// A string describing the rollout status. - public async Task RolloutStatusDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + /// A task representing the asynchronous operation. + public async Task RolloutUndoAsync(string name, string @namespace, long? toRevision = null, CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + if (typeof(T) != typeof(V1Deployment)) + { + throw new ArgumentException($"Undo is only supported for V1Deployment, not {typeof(T).Name}.", nameof(T)); + } + + await RolloutUndoDeploymentInternalAsync(name, @namespace, toRevision, cancellationToken).ConfigureAwait(false); + } + + /// + /// Get the rollout history of a workload resource. + /// + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. + /// Cancellation token. + /// A list of revision history entries. + public async Task> RolloutHistoryAsync(string name, string @namespace, CancellationToken cancellationToken = default) + where T : IKubernetesObject + { + if (typeof(T) == typeof(V1Deployment)) + { + return await RolloutHistoryDeploymentInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1DaemonSet)) + { + return await RolloutHistoryDaemonSetInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else if (typeof(T) == typeof(V1StatefulSet)) + { + return await RolloutHistoryStatefulSetInternalAsync(name, @namespace, cancellationToken).ConfigureAwait(false); + } + else + { + throw new ArgumentException($"Unsupported resource type: {typeof(T).Name}. Only V1Deployment, V1DaemonSet, and V1StatefulSet are supported.", nameof(T)); + } + } + + // Internal implementation methods + private async Task RolloutStatusDeploymentInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -126,14 +232,7 @@ public async Task RolloutStatusDeploymentAsync(string name, string @name return $"deployment \"{name}\" successfully rolled out"; } - /// - /// Get the rollout status of a DaemonSet. - /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. - /// Cancellation token. - /// A string describing the rollout status. - public async Task RolloutStatusDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task RolloutStatusDaemonSetInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -162,14 +261,7 @@ public async Task RolloutStatusDaemonSetAsync(string name, string @names return $"daemon set \"{name}\" successfully rolled out"; } - /// - /// Get the rollout status of a StatefulSet. - /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. - /// Cancellation token. - /// A string describing the rollout status. - public async Task RolloutStatusStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task RolloutStatusStatefulSetInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -210,63 +302,13 @@ public async Task RolloutStatusStatefulSetAsync(string name, string @nam return $"statefulset rolling update complete {status.CurrentReplicas} pods at revision {status.CurrentRevision}..."; } - /// - /// Pause a Deployment rollout. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// Cancellation token. - /// A task representing the asynchronous operation. - public async Task RolloutPauseDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task RolloutUndoDeploymentInternalAsync(string name, string @namespace, long? toRevision, CancellationToken cancellationToken) { var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - var old = JsonSerializer.SerializeToDocument(deployment); - - deployment.Spec.Paused = true; - - var patch = old.CreatePatch(deployment); - - await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Resume a paused Deployment rollout. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// Cancellation token. - /// A task representing the asynchronous operation. - public async Task RolloutResumeDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) - { - var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - - var old = JsonSerializer.SerializeToDocument(deployment); - - deployment.Spec.Paused = false; - - var patch = old.CreatePatch(deployment); - - await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Undo a Deployment rollout to a previous revision. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// The revision to roll back to. If 0 or not specified, rolls back to the previous revision. - /// Cancellation token. - /// A task representing the asynchronous operation. - public async Task RolloutUndoDeploymentAsync(string name, string @namespace, long? toRevision = null, CancellationToken cancellationToken = default) - { - var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - - // Get all ReplicaSets for this deployment var labelSelector = BuildLabelSelector(deployment.Spec.Selector.MatchLabels); var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); - // Filter ReplicaSets owned by this deployment var ownedReplicaSets = replicaSets.Items .Where(rs => rs.Metadata.OwnerReferences?.Any(or => or.Uid == deployment.Metadata.Uid) == true) .OrderByDescending(rs => @@ -289,7 +331,6 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon if (toRevision.HasValue && toRevision.Value > 0) { - // Find specific revision targetReplicaSet = ownedReplicaSets.FirstOrDefault(rs => { if (rs.Metadata.Annotations?.TryGetValue(RevisionAnnotation, out var revisionStr) == true) @@ -307,7 +348,6 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon } else { - // Use previous revision (second in the list) if (ownedReplicaSets.Count < 2) { throw new InvalidOperationException($"No previous revision found for deployment {name}"); @@ -316,12 +356,10 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon targetReplicaSet = ownedReplicaSets[1]; } - // Update deployment with the template from the target ReplicaSet var old = JsonSerializer.SerializeToDocument(deployment); deployment.Spec.Template = targetReplicaSet.Spec.Template; - // Add annotation to record the rollback deployment.Metadata.Annotations ??= new Dictionary(); deployment.Metadata.Annotations[RevisionAnnotation] = targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0"; @@ -331,22 +369,13 @@ public async Task RolloutUndoDeploymentAsync(string name, string @namespace, lon await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); } - /// - /// Get the rollout history of a Deployment. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// Cancellation token. - /// A list of revision history entries. - public async Task> RolloutHistoryDeploymentAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task> RolloutHistoryDeploymentInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - // Get all ReplicaSets for this deployment var labelSelector = BuildLabelSelector(deployment.Spec.Selector.MatchLabels); var replicaSets = await client.AppsV1.ListNamespacedReplicaSetAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); - // Filter and process ReplicaSets owned by this deployment var history = replicaSets.Items .Where(rs => rs.Metadata.OwnerReferences?.Any(or => or.Uid == deployment.Metadata.Uid) == true) .Select(rs => @@ -376,22 +405,13 @@ public async Task> RolloutHistoryDeploymentAsync(stri return history; } - /// - /// Get the rollout history of a DaemonSet. - /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. - /// Cancellation token. - /// A list of revision history entries. - public async Task> RolloutHistoryDaemonSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task> RolloutHistoryDaemonSetInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - // Get ControllerRevisions for this DaemonSet var labelSelector = BuildLabelSelector(daemonSet.Spec.Selector.MatchLabels); var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); - // Filter and process ControllerRevisions owned by this DaemonSet var history = controllerRevisions.Items .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == daemonSet.Metadata.Uid) == true) .Select(cr => @@ -414,22 +434,13 @@ public async Task> RolloutHistoryDaemonSetAsync(strin return history; } - /// - /// Get the rollout history of a StatefulSet. - /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. - /// Cancellation token. - /// A list of revision history entries. - public async Task> RolloutHistoryStatefulSetAsync(string name, string @namespace, CancellationToken cancellationToken = default) + private async Task> RolloutHistoryStatefulSetInternalAsync(string name, string @namespace, CancellationToken cancellationToken) { var statefulSet = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); - // Get ControllerRevisions for this StatefulSet var labelSelector = BuildLabelSelector(statefulSet.Spec.Selector.MatchLabels); var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); - // Filter and process ControllerRevisions owned by this StatefulSet var history = controllerRevisions.Items .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == statefulSet.Metadata.Uid) == true) .Select(cr => diff --git a/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs b/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs index 849ae890..d195349c 100644 --- a/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs +++ b/src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs @@ -3,129 +3,77 @@ namespace k8s.kubectl.beta; public partial class Kubectl { /// - /// Restart a Deployment by adding a restart annotation to trigger a rollout. + /// Restart a workload resource by adding a restart annotation to trigger a rollout. /// - /// The name of the Deployment. - /// The namespace of the Deployment. - public void RolloutRestartDeployment(string name, string @namespace) - { - client.RolloutRestartDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); - } - - /// - /// Restart a DaemonSet by adding a restart annotation to trigger a rollout. - /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. - public void RolloutRestartDaemonSet(string name, string @namespace) - { - client.RolloutRestartDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); - } - - /// - /// Restart a StatefulSet by adding a restart annotation to trigger a rollout. - /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. - public void RolloutRestartStatefulSet(string name, string @namespace) - { - client.RolloutRestartStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); - } - - /// - /// Get the rollout status of a Deployment. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// A string describing the rollout status. - public string RolloutStatusDeployment(string name, string @namespace) + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. + public void RolloutRestart(string name, string @namespace) + where T : IKubernetesObject { - return client.RolloutStatusDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + client.RolloutRestartAsync(name, @namespace).GetAwaiter().GetResult(); } /// - /// Get the rollout status of a DaemonSet. + /// Get the rollout status of a workload resource. /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. /// A string describing the rollout status. - public string RolloutStatusDaemonSet(string name, string @namespace) + public string RolloutStatus(string name, string @namespace) + where T : IKubernetesObject { - return client.RolloutStatusDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); - } - - /// - /// Get the rollout status of a StatefulSet. - /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. - /// A string describing the rollout status. - public string RolloutStatusStatefulSet(string name, string @namespace) - { - return client.RolloutStatusStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); + return client.RolloutStatusAsync(name, @namespace).GetAwaiter().GetResult(); } /// /// Pause a Deployment rollout. /// + /// The type of resource (must be V1Deployment). /// The name of the Deployment. /// The namespace of the Deployment. - public void RolloutPauseDeployment(string name, string @namespace) + public void RolloutPause(string name, string @namespace) + where T : IKubernetesObject { - client.RolloutPauseDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + client.RolloutPauseAsync(name, @namespace).GetAwaiter().GetResult(); } /// /// Resume a paused Deployment rollout. /// + /// The type of resource (must be V1Deployment). /// The name of the Deployment. /// The namespace of the Deployment. - public void RolloutResumeDeployment(string name, string @namespace) + public void RolloutResume(string name, string @namespace) + where T : IKubernetesObject { - client.RolloutResumeDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); + client.RolloutResumeAsync(name, @namespace).GetAwaiter().GetResult(); } /// /// Undo a Deployment rollout to a previous revision. /// + /// The type of resource (must be V1Deployment). /// The name of the Deployment. /// The namespace of the Deployment. /// The revision to roll back to. If 0 or not specified, rolls back to the previous revision. - public void RolloutUndoDeployment(string name, string @namespace, long? toRevision = null) - { - client.RolloutUndoDeploymentAsync(name, @namespace, toRevision).GetAwaiter().GetResult(); - } - - /// - /// Get the rollout history of a Deployment. - /// - /// The name of the Deployment. - /// The namespace of the Deployment. - /// A list of revision history entries. - public IList RolloutHistoryDeployment(string name, string @namespace) - { - return client.RolloutHistoryDeploymentAsync(name, @namespace).GetAwaiter().GetResult(); - } - - /// - /// Get the rollout history of a DaemonSet. - /// - /// The name of the DaemonSet. - /// The namespace of the DaemonSet. - /// A list of revision history entries. - public IList RolloutHistoryDaemonSet(string name, string @namespace) + public void RolloutUndo(string name, string @namespace, long? toRevision = null) + where T : IKubernetesObject { - return client.RolloutHistoryDaemonSetAsync(name, @namespace).GetAwaiter().GetResult(); + client.RolloutUndoAsync(name, @namespace, toRevision).GetAwaiter().GetResult(); } /// - /// Get the rollout history of a StatefulSet. + /// Get the rollout history of a workload resource. /// - /// The name of the StatefulSet. - /// The namespace of the StatefulSet. + /// The type of workload resource (V1Deployment, V1DaemonSet, or V1StatefulSet). + /// The name of the resource. + /// The namespace of the resource. /// A list of revision history entries. - public IList RolloutHistoryStatefulSet(string name, string @namespace) + public IList RolloutHistory(string name, string @namespace) + where T : IKubernetesObject { - return client.RolloutHistoryStatefulSetAsync(name, @namespace).GetAwaiter().GetResult(); + return client.RolloutHistoryAsync(name, @namespace).GetAwaiter().GetResult(); } } diff --git a/tests/Kubectl.Tests/KubectlTests.Rollout.cs b/tests/Kubectl.Tests/KubectlTests.Rollout.cs index d5ba2c7e..aa5c6adc 100644 --- a/tests/Kubectl.Tests/KubectlTests.Rollout.cs +++ b/tests/Kubectl.Tests/KubectlTests.Rollout.cs @@ -66,7 +66,7 @@ public void RolloutRestartDeployment() System.Threading.Thread.Sleep(2000); // Restart the deployment - client.RolloutRestartDeployment(deploymentName, namespaceParameter); + client.RolloutRestart(deploymentName, namespaceParameter); // Verify the restart annotation was added var updatedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); @@ -142,7 +142,7 @@ public void RolloutStatusDeployment() kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); // Get rollout status - var status = client.RolloutStatusDeployment(deploymentName, namespaceParameter); + var status = client.RolloutStatus(deploymentName, namespaceParameter); // Status should contain the deployment name Assert.Contains(deploymentName, status); @@ -216,14 +216,14 @@ public void RolloutPauseAndResumeDeployment() kubernetes.AppsV1.CreateNamespacedDeployment(deployment, namespaceParameter); // Pause the deployment - client.RolloutPauseDeployment(deploymentName, namespaceParameter); + client.RolloutPause(deploymentName, namespaceParameter); // Verify the deployment is paused var pausedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); Assert.True(pausedDeployment.Spec.Paused); // Resume the deployment - client.RolloutResumeDeployment(deploymentName, namespaceParameter); + client.RolloutResume(deploymentName, namespaceParameter); // Verify the deployment is resumed var resumedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); @@ -305,7 +305,7 @@ public void RolloutHistoryDeployment() System.Threading.Thread.Sleep(3000); // Get rollout history - var history = client.RolloutHistoryDeployment(deploymentName, namespaceParameter); + var history = client.RolloutHistory(deploymentName, namespaceParameter); // Should have at least one revision Assert.NotNull(history); @@ -389,7 +389,7 @@ public void RolloutRestartDaemonSet() System.Threading.Thread.Sleep(2000); // Restart the daemonset - client.RolloutRestartDaemonSet(daemonSetName, namespaceParameter); + client.RolloutRestart(daemonSetName, namespaceParameter); // Verify the restart annotation was added var updatedDaemonSet = kubernetes.AppsV1.ReadNamespacedDaemonSet(daemonSetName, namespaceParameter); @@ -471,7 +471,7 @@ public void RolloutStatusDaemonSet() kubernetes.AppsV1.CreateNamespacedDaemonSet(daemonSet, namespaceParameter); // Get rollout status - var status = client.RolloutStatusDaemonSet(daemonSetName, namespaceParameter); + var status = client.RolloutStatus(daemonSetName, namespaceParameter); // Status should contain the daemonset name Assert.Contains(daemonSetName, status); @@ -549,7 +549,7 @@ public void RolloutRestartStatefulSet() System.Threading.Thread.Sleep(2000); // Restart the statefulset - client.RolloutRestartStatefulSet(statefulSetName, namespaceParameter); + client.RolloutRestart(statefulSetName, namespaceParameter); // Verify the restart annotation was added var updatedStatefulSet = kubernetes.AppsV1.ReadNamespacedStatefulSet(statefulSetName, namespaceParameter); @@ -626,7 +626,7 @@ public void RolloutStatusStatefulSet() kubernetes.AppsV1.CreateNamespacedStatefulSet(statefulSet, namespaceParameter); // Get rollout status - var status = client.RolloutStatusStatefulSet(statefulSetName, namespaceParameter); + var status = client.RolloutStatus(statefulSetName, namespaceParameter); // Status should contain the statefulset name or status information Assert.NotNull(status);