Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/controller/operconfig/operconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,15 @@ func (r *ReconcileOperConfig) Reconcile(ctx context.Context, request reconcile.R
r.status.UnsetProgressing(statusmanager.OperatorRender)
}

if hcp := bootstrapResult.Infra.HostedControlPlane; hcp != nil && hcp.RestartDate != "" {
if err := hypershift.SetRestartDateAnnotation(objs, hcp.RestartDate); err != nil {
log.Printf("Failed to set restart-date annotation: %v", err)
r.status.MaybeSetDegraded(statusmanager.OperatorConfig, "RenderError",
fmt.Sprintf("Internal error while setting restart-date annotation: %v", err))
return reconcile.Result{}, err
}
}

// The first object we create should be the record of our applied configuration. The last object we create is config.openshift.io/v1/Network.Status
app, err := AppliedConfiguration(operConfig)
if err != nil {
Expand Down
36 changes: 36 additions & 0 deletions pkg/hypershift/hypershift.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const (
ClusterIDLabel = "_id"
// HyperShiftConditionTypePrefix is a cluster network operator condition type prefix in hostedControlPlane status
HyperShiftConditionTypePrefix = "network.operator.openshift.io/"
// RestartDateAnnotation is used to trigger rolling restarts of control plane operands
RestartDateAnnotation = "hypershift.openshift.io/restart-date"
)

type RelatedObject struct {
Expand All @@ -63,6 +65,7 @@ type HostedControlPlane struct {
AdvertiseAddress string
AdvertisePort int
PriorityClass string
RestartDate string
}

// AvailabilityPolicy specifies a high level availability policy for components.
Expand Down Expand Up @@ -151,6 +154,11 @@ func ParseHostedControlPlane(hcp *unstructured.Unstructured) (*HostedControlPlan
return nil, fmt.Errorf("failed to extract control plane priority class annotation: %v", err)
}

restartDate, _, err := unstructured.NestedString(hcp.UnstructuredContent(), "metadata", "annotations", RestartDateAnnotation)
if err != nil {
return nil, fmt.Errorf("failed to extract restart date annotation: %v", err)

@mgencur mgencur Jun 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: It would be better to use %w to preserve the error chain but since all other calls in this file use %v let's keep it.

}

nodeSelector, _, err := unstructured.NestedStringMap(hcp.UnstructuredContent(), "spec", "nodeSelector")
if err != nil {
return nil, fmt.Errorf("failed extract nodeSelector: %v", err)
Expand Down Expand Up @@ -264,9 +272,37 @@ func ParseHostedControlPlane(hcp *unstructured.Unstructured) (*HostedControlPlan
AdvertiseAddress: advertiseAddress,
AdvertisePort: int(advertisePort),
PriorityClass: controlPlanePriorityClassAnnotation,
RestartDate: restartDate,
}, nil
}

// SetRestartDateAnnotation sets the restart-date annotation on pod templates of all
// Deployment, DaemonSet, and StatefulSet objects to trigger a rolling restart.
func SetRestartDateAnnotation(objs []*unstructured.Unstructured, restartDate string) error {
for _, obj := range objs {
if obj.GetAPIVersion() != "apps/v1" {
continue
}
kind := obj.GetKind()
if kind != "Deployment" && kind != "DaemonSet" && kind != "StatefulSet" {
continue
}

anno, _, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
if err != nil {
return fmt.Errorf("failed to get pod template annotations from %s/%s: %v", kind, obj.GetName(), err)
}
if anno == nil {
anno = map[string]string{}
}
anno[RestartDateAnnotation] = restartDate
if err := unstructured.SetNestedStringMap(obj.Object, anno, "spec", "template", "metadata", "annotations"); err != nil {
return fmt.Errorf("failed to set restart-date annotation on %s/%s: %v", kind, obj.GetName(), err)
}
}
return nil
}

// SetHostedControlPlaneConditions updates the hcp status.conditions based on the provided operStatus
// Returns an updated list of conditions and an error. If there are no changes, the returned list is empty.
func SetHostedControlPlaneConditions(hcp *unstructured.Unstructured, operStatus *operv1.NetworkStatus) ([]metav1.Condition, error) {
Expand Down
113 changes: 113 additions & 0 deletions pkg/hypershift/hypershift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,116 @@ spec:
g.Expect(actualOutput).To(Equal(tc.expectedOutput))
}
}

func TestParseHostedControlPlaneRestartDate(t *testing.T) {
g := NewGomegaWithT(t)
input := `
apiVersion: hypershift.openshift.io/v1beta1
kind: HostedControlPlane
metadata:
annotations:
hypershift.openshift.io/restart-date: "2024-01-15T10:30:00Z"
spec:
clusterID: test-cluster-id
controllerAvailabilityPolicy: SingleReplica
`
rawHCP, err := yaml.ToJSON([]byte(input))
g.Expect(err).NotTo(HaveOccurred())
object, err := runtime.Decode(unstructured.UnstructuredJSONScheme, rawHCP)
g.Expect(err).NotTo(HaveOccurred())
hcpUnstructured, ok := object.(*unstructured.Unstructured)
g.Expect(ok).To(BeTrue())
result, err := ParseHostedControlPlane(hcpUnstructured)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(result.RestartDate).To(Equal("2024-01-15T10:30:00Z"))
}

func TestSetRestartDateAnnotation(t *testing.T) {
g := NewGomegaWithT(t)

makeObj := func(apiVersion, kind, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"name": name,
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{},
},
},
},
}
}

t.Run("sets annotation on Deployment pod template", func(t *testing.T) {
obj := makeObj("apps/v1", "Deployment", "cloud-network-config-controller")
err := SetRestartDateAnnotation([]*unstructured.Unstructured{obj}, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())
anno, _, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))
})

t.Run("sets annotation on DaemonSet pod template", func(t *testing.T) {
obj := makeObj("apps/v1", "DaemonSet", "ovnkube-node")
err := SetRestartDateAnnotation([]*unstructured.Unstructured{obj}, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())
anno, _, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))
})

t.Run("sets annotation on StatefulSet pod template", func(t *testing.T) {
obj := makeObj("apps/v1", "StatefulSet", "test-statefulset")
err := SetRestartDateAnnotation([]*unstructured.Unstructured{obj}, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())
anno, _, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))
})

t.Run("skips non-apps/v1 objects", func(t *testing.T) {
obj := makeObj("v1", "ConfigMap", "test-cm")
err := SetRestartDateAnnotation([]*unstructured.Unstructured{obj}, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())
_, found, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(found).To(BeFalse())
})

t.Run("preserves existing pod template annotations", func(t *testing.T) {
obj := makeObj("apps/v1", "Deployment", "test-deploy")
err := unstructured.SetNestedStringMap(obj.Object, map[string]string{"existing": "value"}, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
err = SetRestartDateAnnotation([]*unstructured.Unstructured{obj}, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())
anno, _, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue("existing", "value"))
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))
})

t.Run("handles multiple objects", func(t *testing.T) {
deploy := makeObj("apps/v1", "Deployment", "cncc")
ds := makeObj("apps/v1", "DaemonSet", "multus")
cm := makeObj("v1", "ConfigMap", "config")
objs := []*unstructured.Unstructured{deploy, ds, cm}
err := SetRestartDateAnnotation(objs, "2024-01-15T10:30:00Z")
g.Expect(err).NotTo(HaveOccurred())

anno, _, err := unstructured.NestedStringMap(deploy.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))

anno, _, err = unstructured.NestedStringMap(ds.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(anno).To(HaveKeyWithValue(RestartDateAnnotation, "2024-01-15T10:30:00Z"))

_, found, err := unstructured.NestedStringMap(cm.Object, "spec", "template", "metadata", "annotations")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(found).To(BeFalse())
})
}