Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 10, 2025

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 types
  • RolloutHistoryAsync<T> - retrieves revision history with change-cause annotations
  • RolloutUndoAsync<T> - rolls back to specified or previous revision (Deployment only)
  • RolloutPauseAsync<T> / RolloutResumeAsync<T> - controls deployment rollouts (Deployment only)

Synchronous wrappers (Kubectl.Rollout.cs):

  • Generic sync wrappers following existing patterns (RolloutRestart, RolloutStatus, etc.)

Tests (KubectlTests.Rollout.cs):

  • Coverage for all operations across resource types

API Design

The API uses generic type parameters following the same pattern as existing Get<T> and List<T> methods, providing a clean and consistent interface:

using var kubernetes = new Kubernetes(config);
var kubectl = new Kubectl(kubernetes);

// Restart workloads
kubectl.RolloutRestart<V1Deployment>("my-app", "production");
kubectl.RolloutRestart<V1DaemonSet>("my-daemon", "default");
kubectl.RolloutRestart<V1StatefulSet>("my-sts", "default");

// Check status
var status = kubectl.RolloutStatus<V1Deployment>("my-app", "production");
// Returns: "deployment \"my-app\" successfully rolled out"

// View history
var history = kubectl.RolloutHistory<V1Deployment>("my-app", "production");

// Deployment-specific operations
kubectl.RolloutPause<V1Deployment>("my-app", "production");
kubectl.RolloutResume<V1Deployment>("my-app", "production");
kubectl.RolloutUndo<V1Deployment>("my-app", "production");

Technical Details

  • Uses JSON patch operations for resource updates
  • Generic type parameters with runtime type checking for resource-specific operations
  • Extracts label selectors via shared helper method
  • Defines annotation keys as constants (RestartedAtAnnotation, RevisionAnnotation, ChangeCauseAnnotation)
  • Handles ReplicaSets for deployments, ControllerRevisions for daemonsets/statefulsets
Original prompt

This section details on the original issue you should resolve

<issue_title>Implement KubectlRollout</issue_title>
<issue_description>Similar to this code in the Java client:
https://github.com/kubernetes-client/java/blob/master/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlRollout.java

And this cli command:
https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/

We should support rollout in the Kubectl Library.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@k8s-ci-robot k8s-ci-robot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Dec 10, 2025
…, pause, and resume methods

Co-authored-by: brendandburns <[email protected]>
@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Dec 10, 2025
@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Dec 10, 2025
Copilot AI changed the title [WIP] Add support for KubectlRollout in Kubectl Library Implement KubectlRollout for deployment lifecycle management Dec 10, 2025
Copilot AI requested a review from brendandburns December 10, 2025 20:49
@brendandburns
Copy link
Contributor

@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]>
Copy link
Contributor Author

Copilot AI commented Dec 10, 2025

@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.

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.

Copilot AI changed the title Implement KubectlRollout for deployment lifecycle management Implement KubectlRollout with generic type parameters for deployment lifecycle management Dec 10, 2025
@brendandburns brendandburns marked this pull request as ready for review December 12, 2025 16:53
Copilot AI review requested due to automatic review settings December 12, 2025 16:53
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Dec 12, 2025
@k8s-ci-robot k8s-ci-robot requested a review from tg123 December 12, 2025 16:53
Copy link
Contributor

@brendandburns brendandburns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Dec 12, 2025
@k8s-ci-robot
Copy link
Contributor

[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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Dec 12, 2025
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +1 to +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<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
}
}
}
}
Copy link

Copilot AI Dec 12, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +261
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";
Copy link

Copilot AI Dec 12, 2025

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".

Suggested change
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";

Copilot uses AI. Check for mistakes.
Comment on lines +364 to +366
deployment.Metadata.Annotations[RevisionAnnotation] =
targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0";

Copy link

Copilot AI Dec 12, 2025

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.

Suggested change
deployment.Metadata.Annotations[RevisionAnnotation] =
targetReplicaSet.Metadata.Annotations?[RevisionAnnotation] ?? "0";

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +69
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));
}
}
Copy link

Copilot AI Dec 12, 2025

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.

Copilot uses AI. Check for mistakes.
/// <returns>A task representing the asynchronous operation.</returns>
public async Task RolloutRestartAsync<T>(string name, string @namespace, CancellationToken cancellationToken = default)
where T : IKubernetesObject
{
Copy link

Copilot AI Dec 12, 2025

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.

Suggested change
{
{
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));

Copilot uses AI. Check for mistakes.
}

var changeCause = "<none>";
if (rs.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause))
Copy link

Copilot AI Dec 12, 2025

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'.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 12, 2025

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'.

Copilot uses AI. Check for mistakes.
.Select(cr =>
{
var changeCause = "<none>";
if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause))
Copy link

Copilot AI Dec 12, 2025

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'.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 12, 2025

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'.

Copilot uses AI. Check for mistakes.
.Select(cr =>
{
var changeCause = "<none>";
if (cr.Metadata.Annotations?.TryGetValue(ChangeCauseAnnotation, out var cause) == true && !string.IsNullOrEmpty(cause))
Copy link

Copilot AI Dec 12, 2025

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'.

Copilot uses AI. Check for mistakes.
@k8s-ci-robot k8s-ci-robot merged commit 31df7ed into master Dec 12, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement KubectlRollout

3 participants