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);