-
Notifications
You must be signed in to change notification settings - Fork 307
Implement KubectlRollout with generic type parameters for deployment lifecycle management #1700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…, pause, and resume methods Co-authored-by: brendandburns <[email protected]>
Co-authored-by: brendandburns <[email protected]>
Co-authored-by: brendandburns <[email protected]>
|
@copilot please update this PR to use a single method signature with different typed parameters instead of a separate method name for Deployments, StatefulSets, etc. |
Changed API from separate methods per resource type to generic methods:
- RolloutRestart<T>, RolloutStatus<T>, RolloutHistory<T>
- RolloutPause<T>, RolloutResume<T>, RolloutUndo<T>
Usage example:
kubectl.RolloutRestart<V1Deployment>("my-app", "production");
kubectl.RolloutStatus<V1DaemonSet>("my-daemon", "default");
This provides a cleaner API that follows the pattern used by Get<T> and List<T>.
Co-authored-by: brendandburns <[email protected]>
Done! Refactored to use generic type parameters instead of separate method names. The new API follows the same pattern as Get and List: kubectl.RolloutRestart<V1Deployment>("my-app", "production");
kubectl.RolloutStatus<V1DaemonSet>("my-daemon", "default");
kubectl.RolloutHistory<V1StatefulSet>("my-sts", "default");Changes in commit f820e76. |
brendandburns
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/lgtm
/approve
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: brendandburns, Copilot The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements kubectl rollout operations for the C# Kubernetes client library, adding deployment lifecycle management capabilities equivalent to the kubectl CLI and Java client. The implementation uses generic type parameters to provide a clean, consistent API that matches the existing Get<T> and List<T> patterns in the codebase.
- Adds six core rollout operations (restart, status, pause, resume, undo, history) with async implementations
- Supports three workload types: V1Deployment, V1DaemonSet, and V1StatefulSet
- Provides both async and synchronous API wrappers following existing codebase patterns
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 31 comments.
| File | Description |
|---|---|
src/KubernetesClient.Kubectl/Beta/AsyncKubectl.Rollout.cs |
Core async implementation of rollout operations with type-specific handlers and status checking logic |
src/KubernetesClient.Kubectl/Beta/Kubectl.Rollout.cs |
Synchronous wrappers for rollout operations using GetAwaiter().GetResult() pattern |
tests/Kubectl.Tests/KubectlTests.Rollout.cs |
Integration tests covering restart, status, pause/resume, and history operations across all supported resource types |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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<string, string> | ||
| { | ||
| { "app", "test-rollout" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutRestart<V1Deployment>(deploymentName, namespaceParameter); | ||
|
|
||
| // Verify the restart annotation was added | ||
| var updatedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); | ||
| Assert.NotNull(updatedDeployment.Spec.Template.Metadata.Annotations); | ||
| Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedDeployment.Spec.Template.Metadata.Annotations.Keys); | ||
| } | ||
| 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<string, string> | ||
| { | ||
| { "app", "test-status" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutStatus<V1Deployment>(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<string, string> | ||
| { | ||
| { "app", "test-pause" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutPause<V1Deployment>(deploymentName, namespaceParameter); | ||
|
|
||
| // Verify the deployment is paused | ||
| var pausedDeployment = kubernetes.AppsV1.ReadNamespacedDeployment(deploymentName, namespaceParameter); | ||
| Assert.True(pausedDeployment.Spec.Paused); | ||
|
|
||
| // Resume the deployment | ||
| client.RolloutResume<V1Deployment>(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<string, string> | ||
| { | ||
| { "kubernetes.io/change-cause", "Initial deployment" }, | ||
| }, | ||
| }, | ||
| Spec = new V1DeploymentSpec | ||
| { | ||
| Replicas = 1, | ||
| Selector = new V1LabelSelector | ||
| { | ||
| MatchLabels = new Dictionary<string, string> | ||
| { | ||
| { "app", "test-history" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutHistory<V1Deployment>(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<string, string> | ||
| { | ||
| { "app", "test-daemonset" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutRestart<V1DaemonSet>(daemonSetName, namespaceParameter); | ||
|
|
||
| // Verify the restart annotation was added | ||
| var updatedDaemonSet = kubernetes.AppsV1.ReadNamespacedDaemonSet(daemonSetName, namespaceParameter); | ||
| Assert.NotNull(updatedDaemonSet.Spec.Template.Metadata.Annotations); | ||
| Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedDaemonSet.Spec.Template.Metadata.Annotations.Keys); | ||
| } | ||
| 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<string, string> | ||
| { | ||
| { "app", "test-ds-status" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutStatus<V1DaemonSet>(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<string, string> | ||
| { | ||
| { "app", "test-statefulset" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutRestart<V1StatefulSet>(statefulSetName, namespaceParameter); | ||
|
|
||
| // Verify the restart annotation was added | ||
| var updatedStatefulSet = kubernetes.AppsV1.ReadNamespacedStatefulSet(statefulSetName, namespaceParameter); | ||
| Assert.NotNull(updatedStatefulSet.Spec.Template.Metadata.Annotations); | ||
| Assert.Contains("kubectl.kubernetes.io/restartedAt", updatedStatefulSet.Spec.Template.Metadata.Annotations.Keys); | ||
| } | ||
| 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<string, string> | ||
| { | ||
| { "app", "test-sts-status" }, | ||
| }, | ||
| }, | ||
| Template = new V1PodTemplateSpec | ||
| { | ||
| Metadata = new V1ObjectMeta | ||
| { | ||
| Labels = new Dictionary<string, string> | ||
| { | ||
| { "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.RolloutStatus<V1StatefulSet>(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 | ||
| } | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for RolloutUndo operation. The test file includes tests for RolloutRestart, RolloutStatus, RolloutPause/Resume, and RolloutHistory, but does not test the RolloutUndo functionality which is a critical operation for rolling back deployments.
| 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"; |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent capitalization in status messages. The messages use "daemon set" (two words, line 243, 248, 253, 258) which is inconsistent with how "statefulset" (one word) is used on lines 273, 278, 299, 302. For consistency, either use "DaemonSet" as one word like "StatefulSet", or split both as "daemon set" and "stateful set".
| 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"; | |
| return "Waiting for DaemonSet spec update to be observed..."; | |
| } | |
| if (status.ObservedGeneration < daemonSet.Metadata.Generation) | |
| { | |
| return "Waiting for DaemonSet spec update to be observed..."; | |
| } | |
| if (status.UpdatedNumberScheduled < status.DesiredNumberScheduled) | |
| { | |
| return $"Waiting for DaemonSet \"{name}\" rollout to finish: {status.UpdatedNumberScheduled} out of {status.DesiredNumberScheduled} new pods have been updated..."; | |
| } | |
| if (status.NumberAvailable < status.DesiredNumberScheduled) | |
| { | |
| return $"Waiting for DaemonSet \"{name}\" rollout to finish: {status.NumberAvailable ?? 0} of {status.DesiredNumberScheduled} updated pods are available..."; | |
| } | |
| return $"DaemonSet \"{name}\" successfully rolled out"; |
| deployment.Metadata.Annotations[RevisionAnnotation] = | ||
| targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0"; | ||
|
|
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issue with setting revision annotation during undo operation. The code sets the revision annotation on the deployment's metadata using the target ReplicaSet's revision, but this doesn't match kubectl's behavior. According to Kubernetes documentation, the revision annotation is managed by the deployment controller and should not be manually set. This line should likely be removed, as the controller will assign the next revision number automatically when the deployment spec is updated.
| deployment.Metadata.Annotations[RevisionAnnotation] = | |
| targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0"; |
| public async Task RolloutRestartAsync<T>(string name, string @namespace, CancellationToken cancellationToken = default) | ||
| where T : IKubernetesObject | ||
| { | ||
| if (typeof(T) == typeof(V1Deployment)) | ||
| { | ||
| var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
| var old = JsonSerializer.SerializeToDocument(deployment); | ||
|
|
||
| deployment.Spec.Template.Metadata ??= new V1ObjectMeta(); | ||
| deployment.Spec.Template.Metadata.Annotations ??= new Dictionary<string, string>(); | ||
| 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); | ||
|
|
||
| daemonSet.Spec.Template.Metadata ??= new V1ObjectMeta(); | ||
| daemonSet.Spec.Template.Metadata.Annotations ??= new Dictionary<string, string>(); | ||
| daemonSet.Spec.Template.Metadata.Annotations[RestartedAtAnnotation] = 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); | ||
| } | ||
| 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<string, string>(); | ||
| 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)); | ||
| } | ||
| } |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Significant code duplication across the three resource type branches. The logic for restarting Deployment, DaemonSet, and StatefulSet is nearly identical, with only the resource type and API method calls differing. Consider refactoring to use a common helper method or introduce an interface/abstraction to reduce duplication and improve maintainability.
| /// <returns>A task representing the asynchronous operation.</returns> | ||
| public async Task RolloutRestartAsync<T>(string name, string @namespace, CancellationToken cancellationToken = default) | ||
| where T : IKubernetesObject | ||
| { |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for required parameters. The name and namespace parameters should be validated for null or empty values before making API calls. This would provide clearer error messages to users rather than allowing null reference exceptions or API errors.
| { | |
| { | |
| if (string.IsNullOrWhiteSpace(name)) | |
| throw new ArgumentException("Parameter 'name' must not be null or empty.", nameof(name)); | |
| if (string.IsNullOrWhiteSpace(@namespace)) | |
| throw new ArgumentException("Parameter 'namespace' must not be null or empty.", nameof(@namespace)); |
| } | ||
|
|
||
| var changeCause = "<none>"; | ||
| if (rs.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == true' can be simplified to 'A'.
| var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var history = controllerRevisions.Items | ||
| .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == daemonSet.Metadata.Uid) == true) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == true' can be simplified to 'A'.
| .Select(cr => | ||
| { | ||
| var changeCause = "<none>"; | ||
| if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == true' can be simplified to 'A'.
| var controllerRevisions = await client.AppsV1.ListNamespacedControllerRevisionAsync(@namespace, labelSelector: labelSelector, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var history = controllerRevisions.Items | ||
| .Where(cr => cr.Metadata.OwnerReferences?.Any(or => or.Uid == statefulSet.Metadata.Uid) == true) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == true' can be simplified to 'A'.
| .Select(cr => | ||
| { | ||
| var changeCause = "<none>"; | ||
| if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause)) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == true' can be simplified to 'A'.
Adds kubectl rollout operations to match Java client and kubectl CLI functionality for managing deployment, daemonset, and statefulset rollouts.
Implementation
Core operations (
AsyncKubectl.Rollout.cs):RolloutRestartAsync<T>- triggers rollout via restart annotation (supports V1Deployment, V1DaemonSet, V1StatefulSet)RolloutStatusAsync<T>- returns detailed rollout progress status for all workload typesRolloutHistoryAsync<T>- retrieves revision history with change-cause annotationsRolloutUndoAsync<T>- rolls back to specified or previous revision (Deployment only)RolloutPauseAsync<T>/RolloutResumeAsync<T>- controls deployment rollouts (Deployment only)Synchronous wrappers (
Kubectl.Rollout.cs):Tests (
KubectlTests.Rollout.cs):API Design
The API uses generic type parameters following the same pattern as existing
Get<T>andList<T>methods, providing a clean and consistent interface:Technical Details
RestartedAtAnnotation,RevisionAnnotation,ChangeCauseAnnotation)Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.