diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index eeb4378fed2..ccd490e9b0b 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -135,6 +135,8 @@ const ( useRestrictedPodSecurityLabel = "io.openshift.hypershift.restricted-psa" defaultToControlPlaneV2Label = "io.openshift.hypershift.control-plane-operator.v2-isdefault" + apiOpenShiftComLabelPrefix = "api.openshift.com/" + etcdEncKeyPostfix = "-etcd-encryption-key" jobHostedClusterNameLabel = "hypershift.openshift.io/cluster-name" @@ -2423,10 +2425,17 @@ func reconcileHostedControlPlane(hcp *hyperv1.HostedControlPlane, hcluster *hype if hcp.Labels == nil { hcp.Labels = make(map[string]string) } - // All labels on the HostedCluster with this special prefix are copied - // Those are labels set by OCM + // These labels are managed by OCM. Delete-then-copy ensures removals + // on the HostedCluster (e.g., clearing limited-support) propagate to the HCP. + for key := range hcp.Labels { + if strings.HasPrefix(key, apiOpenShiftComLabelPrefix) { + if _, exists := hcluster.Labels[key]; !exists { + delete(hcp.Labels, key) + } + } + } for key, val := range hcluster.Labels { - if strings.HasPrefix(key, "api.openshift.com") { + if strings.HasPrefix(key, apiOpenShiftComLabelPrefix) { hcp.Labels[key] = val } } diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go index f289f3dd122..073897aa5d5 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go @@ -310,6 +310,64 @@ func TestReconcileHostedControlPlaneAdditionalTrustBundle(t *testing.T) { } } +func TestReconcileHostedControlPlaneLabelSync(t *testing.T) { + t.Parallel() + tests := []struct { + name string + hcLabels map[string]string + hcpLabels map[string]string + expectedLabels map[string]string + }{ + { + name: "When HC has api.openshift.com labels, it should copy them to HCP", + hcLabels: map[string]string{"api.openshift.com/limited-support": "true", "api.openshift.com/name": "test"}, + hcpLabels: map[string]string{}, + expectedLabels: map[string]string{"api.openshift.com/limited-support": "true", "api.openshift.com/name": "test"}, + }, + { + name: "When HC removes an api.openshift.com label, it should remove the stale label from HCP", + hcLabels: map[string]string{"api.openshift.com/name": "test"}, + hcpLabels: map[string]string{"api.openshift.com/limited-support": "true", "api.openshift.com/name": "old"}, + expectedLabels: map[string]string{"api.openshift.com/name": "test"}, + }, + { + name: "When HC has no api.openshift.com labels, it should preserve non-api.openshift.com labels on HCP", + hcLabels: map[string]string{}, + hcpLabels: map[string]string{"api.openshift.com/limited-support": "true", "cluster.x-k8s.io/cluster-name": "keep-me"}, + expectedLabels: map[string]string{"cluster.x-k8s.io/cluster-name": "keep-me"}, + }, + { + name: "When HC labels are nil, it should remove all api.openshift.com labels from HCP", + hcLabels: nil, + hcpLabels: map[string]string{"api.openshift.com/limited-support": "true"}, + expectedLabels: map[string]string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + hc := &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{Labels: test.hcLabels}, + } + hcp := &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{Labels: test.hcpLabels}, + } + err := reconcileHostedControlPlane(hcp, hc, false, false, func() (map[string]string, error) { return nil, nil }) + g.Expect(err).ToNot(HaveOccurred()) + + for key, val := range test.expectedLabels { + g.Expect(hcp.Labels).To(HaveKeyWithValue(key, val)) + } + for key := range hcp.Labels { + if strings.HasPrefix(key, apiOpenShiftComLabelPrefix) { + g.Expect(test.expectedLabels).To(HaveKey(key), "unexpected label %s=%s still on HCP", key, hcp.Labels[key]) + } + } + }) + } +} + func TestReconcileHostedControlPlaneUpgrades(t *testing.T) { t.Parallel() // TODO: the spec/status comparison of control plane is a weak check; the