From fdcabafd6eccfb0f55c5eaa87fb9dc51c9f2614f Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Mon, 15 Jun 2026 11:56:43 -0400 Subject: [PATCH 1/2] fix(cpo): restart cloud-network-config-controller on restart-date annotation cloud-network-config-controller is a CNO operand deployed on cloud platforms (AWS/Azure/GCP/OpenStack) that uses rotatable cloud API credentials and a kubeconfig. It was omitted from the restart-date annotation handling in cleanupClusterNetworkOperatorResources, leaving it running with stale credentials after rotation. This follows the same pattern as the ovnkube-control-plane fix in 9e1e73eeac. SetRestartAnnotationAndPatch returns nil for not-found deployments, so no platform-specific conditional is needed. Also fixes a pre-existing value-copy bug in SetRestartAnnotationAndPatch where ObjectMeta was copied by value, causing the annotation assignment to be lost when pod template annotations were nil. Co-Authored-By: Claude Opus 4.6 --- .../hostedcontrolplane_controller.go | 6 ++ .../hostedcontrolplane/manifests/cno.go | 10 ++ .../hostedcontrolplane/v2/cno/component.go | 7 +- .../v2/cno/component_test.go | 92 +++++++++++++++++++ 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index 5861a1fda10d..0c25cd194954 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -2043,6 +2043,12 @@ func (r *HostedControlPlaneReconciler) cleanupClusterNetworkOperatorResources(ct if err := cnov2.SetRestartAnnotationAndPatch(ctx, r.Client, ovnKubeControlPlaneDeployment, restartAnnotation); err != nil { return fmt.Errorf("failed to restart ovnkube-control-plane: %w", err) } + + // CNO manages overall cloud-network-config-controller deployment on cloud platforms (AWS/Azure/GCP/OpenStack). CPO manages restarts. + cloudNetworkConfigControllerDeployment := manifests.CloudNetworkConfigControllerDeployment(hcp.Namespace) + if err := cnov2.SetRestartAnnotationAndPatch(ctx, r.Client, cloudNetworkConfigControllerDeployment, restartAnnotation); err != nil { + return fmt.Errorf("failed to restart cloud-network-config-controller: %w", err) + } } // Clean up ovnkube-sbdb Route if exists diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/cno.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/cno.go index b27d1911df3d..b48dc19826cd 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/cno.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/cno.go @@ -13,6 +13,7 @@ const clusterNetworkOperator = "cluster-network-operator" const multusAdmissionController = "multus-admission-controller" const networkNodeIdentity = "network-node-identity" const ovnKubeControlPlane = "ovnkube-control-plane" +const cloudNetworkConfigController = "cloud-network-config-controller" func ClusterNetworkOperatorDeployment(ns string) *appsv1.Deployment { return &appsv1.Deployment{ @@ -77,6 +78,15 @@ func OVNKubeControlPlaneDeployment(namespace string) *appsv1.Deployment { } } +func CloudNetworkConfigControllerDeployment(namespace string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: cloudNetworkConfigController, + }, + } +} + func OVNKubeSBDBRoute(namespace string) *routev1.Route { return &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component.go b/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component.go index 139e1163995f..5d3e2200f833 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component.go @@ -91,11 +91,10 @@ func SetRestartAnnotationAndPatch(ctx context.Context, crclient client.Client, d } patch := dep.DeepCopy() - podMeta := patch.Spec.Template.ObjectMeta - if podMeta.Annotations == nil { - podMeta.Annotations = map[string]string{} + if patch.Spec.Template.ObjectMeta.Annotations == nil { + patch.Spec.Template.ObjectMeta.Annotations = map[string]string{} } - podMeta.Annotations[hyperv1.RestartDateAnnotation] = restartAnnotation + patch.Spec.Template.ObjectMeta.Annotations[hyperv1.RestartDateAnnotation] = restartAnnotation if err := crclient.Patch(ctx, patch, client.MergeFrom(dep)); err != nil { return fmt.Errorf("failed to set restart annotation: %w", err) diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component_test.go index 355b439d0f64..f81246dc8b8e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/cno/component_test.go @@ -1,13 +1,105 @@ package cno import ( + "context" "testing" . "github.com/onsi/gomega" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) +func TestSetRestartAnnotationAndPatch(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + tests := []struct { + name string + existingDeploy *appsv1.Deployment + restartAnnotation string + expectError bool + expectAnnotation bool + }{ + { + name: "When deployment exists with nil annotations it should set the restart annotation", + existingDeploy: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-deploy", + }, + }, + restartAnnotation: "2026-06-15T12:00:00Z", + expectAnnotation: true, + }, + { + name: "When deployment exists with existing annotations it should set the restart annotation", + existingDeploy: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-deploy", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "existing-key": "existing-value", + }, + }, + }, + }, + }, + restartAnnotation: "2026-06-15T12:00:00Z", + expectAnnotation: true, + }, + { + name: "When deployment does not exist it should return nil", + existingDeploy: nil, + restartAnnotation: "2026-06-15T12:00:00Z", + expectAnnotation: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + builder := fake.NewClientBuilder().WithScheme(scheme) + if tt.existingDeploy != nil { + builder = builder.WithObjects(tt.existingDeploy) + } + cl := builder.Build() + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-deploy", + }, + } + + err := SetRestartAnnotationAndPatch(context.Background(), cl, dep, tt.restartAnnotation) + if tt.expectError { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + + if tt.expectAnnotation { + updated := &appsv1.Deployment{} + err = cl.Get(context.Background(), client.ObjectKeyFromObject(dep), updated) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(updated.Spec.Template.ObjectMeta.Annotations).To(HaveKeyWithValue(hyperv1.RestartDateAnnotation, tt.restartAnnotation)) + } + }) + } +} + func TestPlatformHasCloudNetworkConfigController(t *testing.T) { tests := []struct { name string From 1ac48ef4278012a798fed99ae948f860dd3a6d9c Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Mon, 15 Jun 2026 11:56:44 -0400 Subject: [PATCH 2/2] docs: add missing CNO-managed components to restart list Add cloud-network-config-controller, multus-admission-controller, network-node-identity, and ovnkube-control-plane to the documented list of components restarted by the restart-date annotation. These were already restarted but missing from the documentation. Co-Authored-By: Claude Opus 4.6 --- docs/content/how-to/restart-control-plane-components.md | 4 ++++ docs/content/reference/aggregated-docs.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/content/how-to/restart-control-plane-components.md b/docs/content/how-to/restart-control-plane-components.md index 26bff9bb1646..d419b8c6215c 100644 --- a/docs/content/how-to/restart-control-plane-components.md +++ b/docs/content/how-to/restart-control-plane-components.md @@ -14,6 +14,7 @@ The list of components restarted are listed below: * catalog-operator * certified-operators-catalog +* cloud-network-config-controller (cloud platforms only: AWS, Azure, GCP, OpenStack) * cluster-api * cluster-autoscaler * cluster-policy-controller @@ -29,11 +30,14 @@ The list of components restarted are listed below: * kube-controller-manager * kube-scheduler * machine-approver +* multus-admission-controller (if multi-network is enabled) +* network-node-identity * oauth-openshift * olm-operator * openshift-apiserver * openshift-controller-manager * openshift-oauth-apiserver +* ovnkube-control-plane * packageserver * redhat-marketplace-catalog * redhat-operators-catalog \ No newline at end of file diff --git a/docs/content/reference/aggregated-docs.md b/docs/content/reference/aggregated-docs.md index d70c1b359e8d..46f61f3717a9 100644 --- a/docs/content/reference/aggregated-docs.md +++ b/docs/content/reference/aggregated-docs.md @@ -28444,6 +28444,7 @@ The list of components restarted are listed below: * catalog-operator * certified-operators-catalog +* cloud-network-config-controller (cloud platforms only: AWS, Azure, GCP, OpenStack) * cluster-api * cluster-autoscaler * cluster-policy-controller @@ -28459,11 +28460,14 @@ The list of components restarted are listed below: * kube-controller-manager * kube-scheduler * machine-approver +* multus-admission-controller (if multi-network is enabled) +* network-node-identity * oauth-openshift * olm-operator * openshift-apiserver * openshift-controller-manager * openshift-oauth-apiserver +* ovnkube-control-plane * packageserver * redhat-marketplace-catalog * redhat-operators-catalog