diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go index 74b50f24ad3..c282b4904b1 100644 --- a/api/hypershift/v1beta1/hostedcluster_types.go +++ b/api/hypershift/v1beta1/hostedcluster_types.go @@ -78,6 +78,9 @@ const ( // AWSKMSProviderImage is an annotation that allows the specification of the AWS kms provider image. // Upstream code located at: https://github.com/kubernetes-sigs/aws-encryption-provider AWSKMSProviderImage = "hypershift.openshift.io/aws-kms-provider-image" + // ManagedAzureResourceIDAnnotation is an annotation set by Cluster Service on the HostedCluster CR + // containing the Azure resource ID. It is propagated to the hosted control plane namespace. + ManagedAzureResourceIDAnnotation = "azure.microsoft.com/hcp-cluster-azure-resource-id" // IBMCloudKMSProviderImage is an annotation that allows the specification of the IBM Cloud kms provider image. IBMCloudKMSProviderImage = "hypershift.openshift.io/ibmcloud-kms-provider-image" // PortierisImageAnnotation is an annotation that allows the specification of the portieries component diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index 403cbda28ec..e23eb0ecb71 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -1399,6 +1399,8 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques // Enable observability operator monitoring metrics.EnableOBOMonitoring(controlPlaneNamespace) + propagateAzureResourceIDAnnotation(hcluster, controlPlaneNamespace) + return nil }) if err != nil { @@ -5382,6 +5384,26 @@ func computeGCPPSCCondition(gcpPSCList hyperv1.GCPPrivateServiceConnectList, con return computeEndpointServiceCondition(resourceConditions, conditionType, hyperv1.GCPErrorReason, hyperv1.GCPSuccessReason, "GCPPrivateServiceConnect conditions not found") } +// propagateAzureResourceIDAnnotation copies the Azure resource ID annotation set by Cluster Service +// on the HostedCluster CR to the control plane namespace, or removes it if no longer present. +// This annotation is consumed by ARO-HCP logging and observability components to correlate the +// HostedCluster with the corresponding Azure resources. +func propagateAzureResourceIDAnnotation(hcluster *hyperv1.HostedCluster, ns *corev1.Namespace) { + if !azureutil.IsAroHCP() { + // If the platform is not ARO-HCP, ensure the annotation is not set on the namespace and return early + delete(ns.Annotations, hyperv1.ManagedAzureResourceIDAnnotation) + return + } + if resourceID, exists := hcluster.Annotations[hyperv1.ManagedAzureResourceIDAnnotation]; exists { + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[hyperv1.ManagedAzureResourceIDAnnotation] = resourceID + } else { + delete(ns.Annotations, hyperv1.ManagedAzureResourceIDAnnotation) + } +} + func computeAzurePLSCondition(azPLSList hyperv1.AzurePrivateLinkServiceList, conditionType hyperv1.ConditionType) metav1.Condition { resourceConditions := make([][]metav1.Condition, len(azPLSList.Items)) for i, pls := range azPLSList.Items { diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go index e86cc223640..daf4fe8995b 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go @@ -7141,3 +7141,107 @@ func TestKasServingCertHashFromEndpoint(t *testing.T) { }) } } + +func TestPropagateAzureResourceIDAnnotation(t *testing.T) { + const testResourceID = "/subscriptions/00000000/resourceGroups/rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/cluster1" + + tests := []struct { + name string + isAroHCP bool + hcluster *hyperv1.HostedCluster + nsAnnotations map[string]string + expectedAnnotation string + }{ + { + name: "When ARO-HCP cluster has resource ID annotation, it should propagate to namespace", + isAroHCP: true, + hcluster: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: testResourceID, + }, + }, + }, + nsAnnotations: nil, + expectedAnnotation: testResourceID, + }, + { + name: "When ARO-HCP cluster has no resource ID annotation, it should remove it from namespace", + isAroHCP: true, + hcluster: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{}, + }, + nsAnnotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: testResourceID, + }, + expectedAnnotation: "", + }, + { + name: "When non-ARO-HCP cluster has resource ID annotation, it should not propagate to namespace", + hcluster: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: testResourceID, + }, + }, + }, + nsAnnotations: nil, + expectedAnnotation: "", + }, + { + name: "When non-ARO-HCP cluster has resource ID on namespace, it should remove it", + hcluster: &hyperv1.HostedCluster{}, + nsAnnotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: testResourceID, + }, + expectedAnnotation: "", + }, + { + name: "When ARO-HCP cluster has resource ID annotation, it should update existing namespace annotation", + isAroHCP: true, + hcluster: &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: testResourceID, + }, + }, + }, + nsAnnotations: map[string]string{ + hyperv1.ManagedAzureResourceIDAnnotation: "old-value", + "some-other-annotation": "preserved", + }, + expectedAnnotation: testResourceID, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.isAroHCP { + azureutil.SetAsAroHCPTest(t) + } + g := NewGomegaWithT(t) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: tc.nsAnnotations, + }, + } + + propagateAzureResourceIDAnnotation(tc.hcluster, ns) + + if tc.expectedAnnotation != "" { + g.Expect(ns.Annotations).To(HaveKeyWithValue( + hyperv1.ManagedAzureResourceIDAnnotation, tc.expectedAnnotation, + )) + } else { + g.Expect(ns.Annotations).ToNot(HaveKey(hyperv1.ManagedAzureResourceIDAnnotation)) + } + + // Verify other annotations are not disturbed + for k, v := range tc.nsAnnotations { + if k != hyperv1.ManagedAzureResourceIDAnnotation { + g.Expect(ns.Annotations).To(HaveKeyWithValue(k, v)) + } + } + }) + } +} diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go index 74b50f24ad3..c282b4904b1 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go @@ -78,6 +78,9 @@ const ( // AWSKMSProviderImage is an annotation that allows the specification of the AWS kms provider image. // Upstream code located at: https://github.com/kubernetes-sigs/aws-encryption-provider AWSKMSProviderImage = "hypershift.openshift.io/aws-kms-provider-image" + // ManagedAzureResourceIDAnnotation is an annotation set by Cluster Service on the HostedCluster CR + // containing the Azure resource ID. It is propagated to the hosted control plane namespace. + ManagedAzureResourceIDAnnotation = "azure.microsoft.com/hcp-cluster-azure-resource-id" // IBMCloudKMSProviderImage is an annotation that allows the specification of the IBM Cloud kms provider image. IBMCloudKMSProviderImage = "hypershift.openshift.io/ibmcloud-kms-provider-image" // PortierisImageAnnotation is an annotation that allows the specification of the portieries component