From b4a423e2e207794d19308e8cf96b35ea1318c0c7 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Mon, 20 Apr 2026 09:02:24 +0000 Subject: [PATCH 01/10] feat(operator): sync additional trust bundle to cloud-provider-config for AWS Guest cluster operators (e.g. cloud-network-config-controller, cluster-ingress-operator) need the additional trust bundle to communicate with AWS API endpoints that use custom certificates, such as when using a proxy or private AWS endpoints. Previously, the trust bundle was only synced to the user-ca-bundle ConfigMap in the guest cluster's openshift-config namespace. With this change, we also sync it to the cloud-provider-config ConfigMap in the guest cluster, and set infra.Spec.CloudConfig on the Infrastructure resource to reference it, following the same pattern used by Azure and OpenStack platforms. The AWS case in reconcileCloudConfig now: - Always creates a cloud-provider-config ConfigMap with the AWS cloud config (aws.conf) - Includes the CA bundle (ca-bundle.pem) when additionalTrustBundle is set - Removes the CA bundle key when additionalTrustBundle is unset Ref: CNTRLPLANE-625 Co-Authored-By: Claude Opus 4.6 --- .../cloud/aws/providerconfig.go | 1 + .../controllers/resources/resources.go | 31 +++++++++++++++++++ support/globalconfig/infrastructure.go | 3 ++ 3 files changed, 35 insertions(+) diff --git a/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go b/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go index 5705ef7d4485..24963b78a57f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go @@ -10,6 +10,7 @@ import ( const ( Provider = podspec.AWSCloudProviderName ProviderConfigKey = "aws.conf" + CABundleKey = "ca-bundle.pem" ) func AWSKMSCredsSecret(controlPlaneNamespace string) *corev1.Secret { diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 22b24db8f99d..573218e5939d 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -14,6 +14,7 @@ import ( hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane" + "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/azure" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" kubevirtcsi "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/csi/kubevirt" @@ -2528,6 +2529,36 @@ func (r *reconciler) reconcileObservedConfiguration(ctx context.Context, hcp *hy func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.HostedControlPlane) error { switch hcp.Spec.Platform.Type { + case hyperv1.AWSPlatform: + reference := cpomanifests.AWSProviderConfig(hcp.Namespace) + if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(reference), reference); err != nil { + return fmt.Errorf("failed to fetch %s/%s configmap from management cluster: %w", reference.Namespace, reference.Name, err) + } + + var caBundle string + if hcp.Spec.AdditionalTrustBundle != nil { + cpUserCAConfigMap := cpomanifests.UserCAConfigMap(hcp.Namespace) + if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(cpUserCAConfigMap), cpUserCAConfigMap); err != nil { + return fmt.Errorf("failed to fetch user CA bundle from management cluster: %w", err) + } + caBundle = cpUserCAConfigMap.Data["ca-bundle.crt"] + } + + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} + if _, err := r.CreateOrUpdate(ctx, r.client, cm, func() error { + if cm.Data == nil { + cm.Data = map[string]string{} + } + cm.Data[aws.ProviderConfigKey] = reference.Data[aws.ProviderConfigKey] + if caBundle != "" { + cm.Data[aws.CABundleKey] = caBundle + } else { + delete(cm.Data, aws.CABundleKey) + } + return nil + }); err != nil { + return fmt.Errorf("failed to reconcile the %s/%s configmap: %w", cm.Namespace, cm.Name, err) + } case hyperv1.AzurePlatform: // This is needed for the e2e tests and only for Azure: https://github.com/openshift/origin/blob/625733dd1ce7ebf40c3dd0abd693f7bb54f2d580/test/extended/util/cluster/cluster.go#L186 reference := cpomanifests.AzureProviderConfig(hcp.Namespace) diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 7c892d7e5f76..1c8453d48331 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -5,6 +5,7 @@ import ( "strings" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" "github.com/openshift/hypershift/support/netutil" @@ -57,6 +58,8 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted switch platformType { case hyperv1.AWSPlatform: + infra.Spec.CloudConfig.Name = "cloud-provider-config" + infra.Spec.CloudConfig.Key = aws.ProviderConfigKey if infra.Spec.PlatformSpec.AWS == nil { infra.Spec.PlatformSpec.AWS = &configv1.AWSPlatformSpec{} } From 79d9b1e778444e8229d6c9ffee8c07fade00ff32 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Mon, 20 Apr 2026 09:02:33 +0000 Subject: [PATCH 02/10] test: add tests for AWS cloud-provider-config trust bundle sync Ensure the new AWS cloud config reconciliation behavior is covered by unit tests: - Verify cloud-provider-config is created with CA bundle when additionalTrustBundle is set - Verify cloud-provider-config is created without CA bundle when additionalTrustBundle is not set - Verify CA bundle key is removed from cloud-provider-config when trust bundle is removed - Verify Infrastructure CloudConfig is set correctly for AWS platform Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 169 ++++++++++++++++++ support/globalconfig/infrastructure_test.go | 16 ++ 2 files changed, 185 insertions(+) create mode 100644 control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go new file mode 100644 index 000000000000..3f6f3b3915a7 --- /dev/null +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -0,0 +1,169 @@ +package resources + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" + cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestReconcileCloudConfig_AWS(t *testing.T) { + const ( + hcpNamespace = "test-hcp-ns" + ) + + fakeAWSCloudConfig := func() *corev1.ConfigMap { + cm := cpomanifests.AWSProviderConfig(hcpNamespace) + cm.Data = map[string]string{ + aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + } + return cm + } + + fakeUserCABundle := func() *corev1.ConfigMap { + cm := cpomanifests.UserCAConfigMap(hcpNamespace) + cm.Data = map[string]string{ + "ca-bundle.crt": "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n", + } + return cm + } + + testCases := []struct { + name string + hcp *hyperv1.HostedControlPlane + cpObjects []client.Object + guestObjects []client.Object + verify func(g Gomega, guestClient client.Client) + }{ + { + name: "When AWS platform with additionalTrustBundle, it should create cloud-provider-config with CA bundle", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + fakeUserCABundle(), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) + }, + }, + { + name: "When AWS platform without additionalTrustBundle, it should create cloud-provider-config without CA bundle", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + }, + }, + { + name: "When trust bundle is removed, it should remove CA bundle key from cloud-provider-config", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + }, + guestObjects: []client.Object{ + // Simulate a previously existing cloud-provider-config with CA bundle + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, + Data: map[string]string{ + aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + aws.CABundleKey: "old-ca-bundle", + }, + }, + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + cpClient := fake.NewClientBuilder().WithObjects(tc.cpObjects...).Build() + + guestObjects := tc.guestObjects + guestClient := fake.NewClientBuilder().WithObjects(guestObjects...).Build() + + r := &reconciler{ + client: guestClient, + cpClient: cpClient, + CreateOrUpdateProvider: &simpleCreateOrUpdater{}, + } + + err := r.reconcileCloudConfig(context.Background(), tc.hcp) + g.Expect(err).ToNot(HaveOccurred()) + + tc.verify(g, guestClient) + }) + } +} diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go index f1a56f88a8db..0fac00108f0d 100644 --- a/support/globalconfig/infrastructure_test.go +++ b/support/globalconfig/infrastructure_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/gomega" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" configv1 "github.com/openshift/api/config/v1" @@ -193,6 +194,21 @@ func TestReconcileInfrastructure(t *testing.T) { g.Expect(infra.Status.PlatformStatus.AWS.Region).To(Equal("us-east-1")) }, }, + { + name: "When AWS platform is specified, it should set CloudConfig name and key", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.AWSPlatform) + hcp.Spec.Platform.AWS = &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + } + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Spec.CloudConfig.Name).To(Equal("cloud-provider-config")) + g.Expect(infra.Spec.CloudConfig.Key).To(Equal(aws.ProviderConfigKey)) + }, + }, { name: "When AWS platform has resource tags, it should copy tags filtering out kubernetes.io prefixed ones", inputInfra: InfrastructureConfig(), From 84d5eb57d01c3816bb5ed6d4168ef110bb164bf0 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Mon, 20 Apr 2026 09:19:54 +0000 Subject: [PATCH 03/10] fix(operator): use constant for CA bundle key and handle missing user-ca-bundle gracefully Replace magic string "ca-bundle.crt" with certs.UserCABundleMapKey constant in reconcileCloudConfig and tests. Handle apierrors.IsNotFound for user-ca-bundle ConfigMap so the base aws.conf is still synced during bootstrap when the CA bundle isn't ready yet. Add error path and edge case tests for missing ConfigMaps and empty CA bundle values. Ticket: CNTRLPLANE-625 Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 113 +++++++++++++++++- .../controllers/resources/resources.go | 15 ++- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 3f6f3b3915a7..933a4c4e80aa 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -9,6 +9,7 @@ import ( hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + "github.com/openshift/hypershift/support/certs" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,7 +34,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { fakeUserCABundle := func() *corev1.ConfigMap { cm := cpomanifests.UserCAConfigMap(hcpNamespace) cm.Data = map[string]string{ - "ca-bundle.crt": "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n", + certs.UserCABundleMapKey: "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n", } return cm } @@ -43,6 +44,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { hcp *hyperv1.HostedControlPlane cpObjects []client.Object guestObjects []client.Object + expectError bool verify func(g Gomega, guestClient client.Client) }{ { @@ -143,6 +145,111 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) }, }, + { + name: "When aws-cloud-config ConfigMap is missing, it should return an error", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + cpObjects: []client.Object{}, + expectError: true, + }, + { + name: "When additionalTrustBundle is set but user-ca-bundle ConfigMap is missing, it should sync base config without CA bundle", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + }, + }, + { + name: "When aws-cloud-config ConfigMap is missing the provider config key, it should return an error", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + cpObjects: []client.Object{ + func() *corev1.ConfigMap { + cm := cpomanifests.AWSProviderConfig(hcpNamespace) + cm.Data = map[string]string{} + return cm + }(), + }, + expectError: true, + }, + { + name: "When additionalTrustBundle has empty ca-bundle.crt value, it should not set CA bundle key", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + func() *corev1.ConfigMap { + cm := cpomanifests.UserCAConfigMap(hcpNamespace) + cm.Data = map[string]string{ + certs.UserCABundleMapKey: "", + } + return cm + }(), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + }, + }, } for _, tc := range testCases { @@ -161,6 +268,10 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { } err := r.reconcileCloudConfig(context.Background(), tc.hcp) + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + return + } g.Expect(err).ToNot(HaveOccurred()) tc.verify(g, guestClient) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 573218e5939d..d64167cede16 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -50,6 +50,7 @@ import ( hyperapi "github.com/openshift/hypershift/support/api" "github.com/openshift/hypershift/support/azureutil" "github.com/openshift/hypershift/support/capabilities" + "github.com/openshift/hypershift/support/certs" "github.com/openshift/hypershift/support/config" "github.com/openshift/hypershift/support/globalconfig" "github.com/openshift/hypershift/support/k8sutil" @@ -2534,14 +2535,22 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(reference), reference); err != nil { return fmt.Errorf("failed to fetch %s/%s configmap from management cluster: %w", reference.Namespace, reference.Name, err) } + providerConfig, ok := reference.Data[aws.ProviderConfigKey] + if !ok || strings.TrimSpace(providerConfig) == "" { + return fmt.Errorf("source configmap %s/%s is missing required key %q", reference.Namespace, reference.Name, aws.ProviderConfigKey) + } var caBundle string if hcp.Spec.AdditionalTrustBundle != nil { cpUserCAConfigMap := cpomanifests.UserCAConfigMap(hcp.Namespace) if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(cpUserCAConfigMap), cpUserCAConfigMap); err != nil { - return fmt.Errorf("failed to fetch user CA bundle from management cluster: %w", err) + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to fetch user CA bundle from management cluster: %w", err) + } + ctrl.LoggerFrom(ctx).Info("user CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(cpUserCAConfigMap)) + } else { + caBundle = cpUserCAConfigMap.Data[certs.UserCABundleMapKey] } - caBundle = cpUserCAConfigMap.Data["ca-bundle.crt"] } cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} @@ -2549,7 +2558,7 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host if cm.Data == nil { cm.Data = map[string]string{} } - cm.Data[aws.ProviderConfigKey] = reference.Data[aws.ProviderConfigKey] + cm.Data[aws.ProviderConfigKey] = providerConfig if caBundle != "" { cm.Data[aws.CABundleKey] = caBundle } else { From fdb74bf924581b8e467d78dbc916ddc7ac2e74e7 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Tue, 21 Apr 2026 08:09:02 +0000 Subject: [PATCH 04/10] fix(operator): use shared constant for cloud-provider-config and aggregate trust bundles Use globalconfig.CloudProviderCMName constant in both infrastructure.go and resources.go instead of duplicated string literals. Also switch from using only spec.AdditionalTrustBundle to the aggregated managed trust bundle (trusted-ca-bundle-managed) that combines both spec.AdditionalTrustBundle and spec.configuration.Proxy.TrustedCA, consistent with the HCP controller's existing aggregation pattern. Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 58 ++++++++++++++----- .../controllers/resources/resources.go | 17 +++--- support/globalconfig/infrastructure.go | 10 +++- support/globalconfig/infrastructure_test.go | 4 +- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 933a4c4e80aa..e3bf5be0090a 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -6,6 +6,7 @@ import ( . "github.com/onsi/gomega" + configv1 "github.com/openshift/api/config/v1" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" @@ -31,10 +32,10 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { return cm } - fakeUserCABundle := func() *corev1.ConfigMap { - cm := cpomanifests.UserCAConfigMap(hcpNamespace) + fakeManagedTrustBundle := func(caData string) *corev1.ConfigMap { + cm := cpomanifests.TrustedCABundleConfigMap(hcpNamespace) cm.Data = map[string]string{ - certs.UserCABundleMapKey: "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n", + certs.UserCABundleMapKey: caData, } return cm } @@ -65,7 +66,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cpObjects: []client.Object{ fakeAWSCloudConfig(), - fakeUserCABundle(), + fakeManagedTrustBundle("-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n"), }, verify: func(g Gomega, guestClient client.Client) { cm := &corev1.ConfigMap{} @@ -79,7 +80,42 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, }, { - name: "When AWS platform without additionalTrustBundle, it should create cloud-provider-config without CA bundle", + name: "When AWS platform with proxy TrustedCA, it should create cloud-provider-config with CA bundle", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + Configuration: &hyperv1.ClusterConfiguration{ + Proxy: &configv1.ProxySpec{ + TrustedCA: configv1.ConfigMapNameReference{ + Name: "proxy-trusted-ca", + }, + }, + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + fakeManagedTrustBundle("-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n"), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) + }, + }, + { + name: "When AWS platform without any trust bundle, it should create cloud-provider-config without CA bundle", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -162,7 +198,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { expectError: true, }, { - name: "When additionalTrustBundle is set but user-ca-bundle ConfigMap is missing, it should sync base config without CA bundle", + name: "When additionalTrustBundle is set but managed trust bundle ConfigMap is missing, it should sync base config without CA bundle", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -214,7 +250,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { expectError: true, }, { - name: "When additionalTrustBundle has empty ca-bundle.crt value, it should not set CA bundle key", + name: "When managed trust bundle has empty ca-bundle.crt value, it should not set CA bundle key", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -231,13 +267,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cpObjects: []client.Object{ fakeAWSCloudConfig(), - func() *corev1.ConfigMap { - cm := cpomanifests.UserCAConfigMap(hcpNamespace) - cm.Data = map[string]string{ - certs.UserCABundleMapKey: "", - } - return cm - }(), + fakeManagedTrustBundle(""), }, verify: func(g Gomega, guestClient client.Client) { cm := &corev1.ConfigMap{} diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index d64167cede16..7bc9b5812cc3 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -106,7 +106,7 @@ const ( ControllerName = "resources" ConfigNamespace = "openshift-config" ConfigManagedNamespace = "openshift-config-managed" - CloudProviderCMName = "cloud-provider-config" + CloudProviderCMName = globalconfig.CloudProviderCMName maxConditionMessageLength = 1024 awsCredentialsTemplate = `[default] role_arn = %s @@ -2540,16 +2540,19 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host return fmt.Errorf("source configmap %s/%s is missing required key %q", reference.Namespace, reference.Name, aws.ProviderConfigKey) } + // Use the aggregated trust bundle that combines spec.AdditionalTrustBundle + // and spec.Configuration.Proxy.TrustedCA, managed by the HCP controller. var caBundle string - if hcp.Spec.AdditionalTrustBundle != nil { - cpUserCAConfigMap := cpomanifests.UserCAConfigMap(hcp.Namespace) - if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(cpUserCAConfigMap), cpUserCAConfigMap); err != nil { + if hcp.Spec.AdditionalTrustBundle != nil || + (hcp.Spec.Configuration != nil && hcp.Spec.Configuration.Proxy != nil && hcp.Spec.Configuration.Proxy.TrustedCA.Name != "") { + managedTrustBundle := cpomanifests.TrustedCABundleConfigMap(hcp.Namespace) + if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(managedTrustBundle), managedTrustBundle); err != nil { if !apierrors.IsNotFound(err) { - return fmt.Errorf("failed to fetch user CA bundle from management cluster: %w", err) + return fmt.Errorf("failed to fetch managed trusted CA bundle from management cluster: %w", err) } - ctrl.LoggerFrom(ctx).Info("user CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(cpUserCAConfigMap)) + ctrl.LoggerFrom(ctx).Info("managed trusted CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(managedTrustBundle)) } else { - caBundle = cpUserCAConfigMap.Data[certs.UserCABundleMapKey] + caBundle = managedTrustBundle.Data[certs.UserCABundleMapKey] } } diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 1c8453d48331..76d2dfb68bd7 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -14,6 +14,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // CloudProviderCMName is the name of the ConfigMap used to store cloud provider configuration + // in the guest cluster's openshift-config namespace. + CloudProviderCMName = "cloud-provider-config" +) + func InfrastructureConfig() *configv1.Infrastructure { infra := &configv1.Infrastructure{ ObjectMeta: metav1.ObjectMeta{ @@ -58,7 +64,7 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted switch platformType { case hyperv1.AWSPlatform: - infra.Spec.CloudConfig.Name = "cloud-provider-config" + infra.Spec.CloudConfig.Name = CloudProviderCMName infra.Spec.CloudConfig.Key = aws.ProviderConfigKey if infra.Spec.PlatformSpec.AWS == nil { infra.Spec.PlatformSpec.AWS = &configv1.AWSPlatformSpec{} @@ -101,7 +107,7 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted case hyperv1.OpenStackPlatform: infra.Spec.PlatformSpec.OpenStack = &configv1.OpenStackPlatformSpec{} // This ConfigMap is populated by the local ignition provider and given to MCO - infra.Spec.CloudConfig.Name = "cloud-provider-config" + infra.Spec.CloudConfig.Name = CloudProviderCMName infra.Spec.CloudConfig.Key = openstack.CloudConfigKey infra.Status.PlatformStatus.OpenStack = &configv1.OpenStackPlatformStatus{ CloudName: "openstack", diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go index 0fac00108f0d..3923fd745ac6 100644 --- a/support/globalconfig/infrastructure_test.go +++ b/support/globalconfig/infrastructure_test.go @@ -205,7 +205,7 @@ func TestReconcileInfrastructure(t *testing.T) { return hcp }(), verify: func(g Gomega, infra *configv1.Infrastructure) { - g.Expect(infra.Spec.CloudConfig.Name).To(Equal("cloud-provider-config")) + g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) g.Expect(infra.Spec.CloudConfig.Key).To(Equal(aws.ProviderConfigKey)) }, }, @@ -315,7 +315,7 @@ func TestReconcileInfrastructure(t *testing.T) { verify: func(g Gomega, infra *configv1.Infrastructure) { g.Expect(infra.Status.Platform).To(Equal(configv1.OpenStackPlatformType)) g.Expect(infra.Spec.PlatformSpec.OpenStack).ToNot(BeNil()) - g.Expect(infra.Spec.CloudConfig.Name).To(Equal("cloud-provider-config")) + g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) g.Expect(infra.Spec.CloudConfig.Key).To(Equal(openstack.CloudConfigKey)) }, }, From 329ded6551d7fd6280cfa1d8b7a6eb62f74bdf6a Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Wed, 22 Apr 2026 14:10:01 +0000 Subject: [PATCH 05/10] fix(lint): reorder imports per gci config in reconcile_cloud_config_test Move configv1 import to the github.com/openshift group, separate from the github.com/openshift/hypershift group, to satisfy the project's gci linter configuration. Co-Authored-By: Claude Opus 4.6 --- .../controllers/resources/reconcile_cloud_config_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index e3bf5be0090a..285dac72aba2 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -6,12 +6,13 @@ import ( . "github.com/onsi/gomega" - configv1 "github.com/openshift/api/config/v1" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" "github.com/openshift/hypershift/support/certs" + configv1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" From d2f4d4b49602062d49257bed0538110946b6c1e8 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Wed, 22 Apr 2026 14:40:56 +0000 Subject: [PATCH 06/10] test: harden error assertions and add proxy TrustedCA parity test Add specific error message assertions (errContains) to error-path test cases to prevent false positives from unintended failures. Add parity test coverage for the Proxy.TrustedCA code path when the managed trust bundle ConfigMap is missing, matching existing AdditionalTrustBundle coverage. Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 285dac72aba2..f3dcd9b2fea6 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -47,6 +47,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { cpObjects []client.Object guestObjects []client.Object expectError bool + errContains string verify func(g Gomega, guestClient client.Client) }{ { @@ -197,6 +198,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cpObjects: []client.Object{}, expectError: true, + errContains: "not found", }, { name: "When additionalTrustBundle is set but managed trust bundle ConfigMap is missing, it should sync base config without CA bundle", @@ -249,6 +251,66 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }(), }, expectError: true, + errContains: aws.ProviderConfigKey, + }, + { + name: "When aws-cloud-config ConfigMap has whitespace-only provider config, it should return an error", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + cpObjects: []client.Object{ + func() *corev1.ConfigMap { + cm := cpomanifests.AWSProviderConfig(hcpNamespace) + cm.Data = map[string]string{ + aws.ProviderConfigKey: " \n\t", + } + return cm + }(), + }, + expectError: true, + errContains: aws.ProviderConfigKey, + }, + { + name: "When proxy TrustedCA is set but managed trust bundle ConfigMap is missing, it should sync base config without CA bundle", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: hcpNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + Configuration: &hyperv1.ClusterConfiguration{ + Proxy: &configv1.ProxySpec{ + TrustedCA: configv1.ConfigMapNameReference{ + Name: "proxy-trusted-ca", + }, + }, + }, + }, + }, + cpObjects: []client.Object{ + fakeAWSCloudConfig(), + }, + verify: func(g Gomega, guestClient client.Client) { + cm := &corev1.ConfigMap{} + err := guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, cm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + }, }, { name: "When managed trust bundle has empty ca-bundle.crt value, it should not set CA bundle key", @@ -301,6 +363,9 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { err := r.reconcileCloudConfig(context.Background(), tc.hcp) if tc.expectError { g.Expect(err).To(HaveOccurred()) + if tc.errContains != "" { + g.Expect(err.Error()).To(ContainSubstring(tc.errContains)) + } return } g.Expect(err).ToNot(HaveOccurred()) From a15d710a5cb6ebc3b774703c7e18fb2976cd2433 Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Wed, 17 Jun 2026 15:21:46 +0000 Subject: [PATCH 07/10] refactor(operator): move shared cloud config constants to support/globalconfig Move CABundleKey and cloud config key constants to support/globalconfig to fix the import direction anti-pattern where support/ imported from control-plane-operator/. This also deduplicates CABundleKey which was identical between the AWS and OpenStack packages. Co-Authored-By: Claude Opus 4.6 --- .../cloud/aws/providerconfig.go | 1 - .../resources/reconcile_cloud_config_test.go | 19 ++++++++++--------- .../controllers/resources/resources.go | 4 ++-- support/globalconfig/infrastructure.go | 18 ++++++++++++++---- support/globalconfig/infrastructure_test.go | 6 ++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go b/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go index 24963b78a57f..5705ef7d4485 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go +++ b/control-plane-operator/controllers/hostedcontrolplane/cloud/aws/providerconfig.go @@ -10,7 +10,6 @@ import ( const ( Provider = podspec.AWSCloudProviderName ProviderConfigKey = "aws.conf" - CABundleKey = "ca-bundle.pem" ) func AWSKMSCredsSecret(controlPlaneNamespace string) *corev1.Secret { diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index f3dcd9b2fea6..13d64c00b39c 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -10,6 +10,7 @@ import ( "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" "github.com/openshift/hypershift/support/certs" + "github.com/openshift/hypershift/support/globalconfig" configv1 "github.com/openshift/api/config/v1" @@ -78,7 +79,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, { @@ -113,7 +114,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, { @@ -140,7 +141,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -167,8 +168,8 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, Data: map[string]string{ - aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", - aws.CABundleKey: "old-ca-bundle", + aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + globalconfig.CABundleKey: "old-ca-bundle", }, }, }, @@ -180,7 +181,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -227,7 +228,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -309,7 +310,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -340,7 +341,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, cm) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(aws.CABundleKey)) + g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, } diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 7bc9b5812cc3..7c2eaa1b436a 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -2563,9 +2563,9 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host } cm.Data[aws.ProviderConfigKey] = providerConfig if caBundle != "" { - cm.Data[aws.CABundleKey] = caBundle + cm.Data[globalconfig.CABundleKey] = caBundle } else { - delete(cm.Data, aws.CABundleKey) + delete(cm.Data, globalconfig.CABundleKey) } return nil }); err != nil { diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 76d2dfb68bd7..5cd3dbcf82a2 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -5,8 +5,6 @@ import ( "strings" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" "github.com/openshift/hypershift/support/netutil" configv1 "github.com/openshift/api/config/v1" @@ -18,6 +16,18 @@ const ( // CloudProviderCMName is the name of the ConfigMap used to store cloud provider configuration // in the guest cluster's openshift-config namespace. CloudProviderCMName = "cloud-provider-config" + + // AWSProviderConfigKey is the key for the AWS cloud provider configuration + // in the cloud-provider-config ConfigMap. + AWSProviderConfigKey = "aws.conf" + + // OpenStackCloudConfigKey is the key for the OpenStack cloud configuration + // in the cloud-provider-config ConfigMap. + OpenStackCloudConfigKey = "cloud.conf" + + // CABundleKey is the key for the CA certificate bundle in cloud-provider-config ConfigMaps. + // Used by both AWS and OpenStack platforms when custom trust bundles are configured. + CABundleKey = "ca-bundle.pem" ) func InfrastructureConfig() *configv1.Infrastructure { @@ -65,7 +75,7 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted switch platformType { case hyperv1.AWSPlatform: infra.Spec.CloudConfig.Name = CloudProviderCMName - infra.Spec.CloudConfig.Key = aws.ProviderConfigKey + infra.Spec.CloudConfig.Key = AWSProviderConfigKey if infra.Spec.PlatformSpec.AWS == nil { infra.Spec.PlatformSpec.AWS = &configv1.AWSPlatformSpec{} } @@ -108,7 +118,7 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted infra.Spec.PlatformSpec.OpenStack = &configv1.OpenStackPlatformSpec{} // This ConfigMap is populated by the local ignition provider and given to MCO infra.Spec.CloudConfig.Name = CloudProviderCMName - infra.Spec.CloudConfig.Key = openstack.CloudConfigKey + infra.Spec.CloudConfig.Key = OpenStackCloudConfigKey infra.Status.PlatformStatus.OpenStack = &configv1.OpenStackPlatformStatus{ CloudName: "openstack", LoadBalancer: &configv1.OpenStackPlatformLoadBalancer{Type: configv1.LoadBalancerTypeUserManaged}, diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go index 3923fd745ac6..05b253b98401 100644 --- a/support/globalconfig/infrastructure_test.go +++ b/support/globalconfig/infrastructure_test.go @@ -7,8 +7,6 @@ import ( . "github.com/onsi/gomega" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" configv1 "github.com/openshift/api/config/v1" @@ -206,7 +204,7 @@ func TestReconcileInfrastructure(t *testing.T) { }(), verify: func(g Gomega, infra *configv1.Infrastructure) { g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) - g.Expect(infra.Spec.CloudConfig.Key).To(Equal(aws.ProviderConfigKey)) + g.Expect(infra.Spec.CloudConfig.Key).To(Equal(AWSProviderConfigKey)) }, }, { @@ -316,7 +314,7 @@ func TestReconcileInfrastructure(t *testing.T) { g.Expect(infra.Status.Platform).To(Equal(configv1.OpenStackPlatformType)) g.Expect(infra.Spec.PlatformSpec.OpenStack).ToNot(BeNil()) g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) - g.Expect(infra.Spec.CloudConfig.Key).To(Equal(openstack.CloudConfigKey)) + g.Expect(infra.Spec.CloudConfig.Key).To(Equal(OpenStackCloudConfigKey)) }, }, { From 7ff17b17c15c3c388fbdf50e593878b3c717019f Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Wed, 17 Jun 2026 15:33:39 +0000 Subject: [PATCH 08/10] fix(fmt): align map literal whitespace in reconcile_cloud_config_test Co-Authored-By: Claude Opus 4.6 --- .../controllers/resources/reconcile_cloud_config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 13d64c00b39c..0b1140a2f971 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -168,7 +168,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, Data: map[string]string{ - aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", globalconfig.CABundleKey: "old-ca-bundle", }, }, From cbc1656ecc5762fabda6660f7ba1584e7187771f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 24 Jun 2026 14:17:01 +0000 Subject: [PATCH 09/10] fix(operator): gate AWS cloud config sync on trust bundle presence Make infra.Spec.CloudConfig and cloud-provider-config ConfigMap creation conditional on having a trust bundle configured (AdditionalTrustBundle or Proxy.TrustedCA). This avoids introducing new CloudConfig and ConfigMap resources into all existing AWS guest clusters, which could change guest operator behavior. When no trust bundle is configured, any previously-synced cloud-provider-config ConfigMap is cleaned up and CloudConfig is cleared. Also replaces aws.ProviderConfigKey usage in resources.go with the shared globalconfig.AWSProviderConfigKey constant, removing the import anti-pattern of support/ depending on control-plane-operator/. Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 54 +++++++++---------- .../controllers/resources/resources.go | 37 +++++++------ support/globalconfig/infrastructure.go | 9 +++- support/globalconfig/infrastructure_test.go | 39 +++++++++++++- 4 files changed, 92 insertions(+), 47 deletions(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 0b1140a2f971..99c9cafd1a91 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -7,7 +7,6 @@ import ( . "github.com/onsi/gomega" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" "github.com/openshift/hypershift/support/certs" "github.com/openshift/hypershift/support/globalconfig" @@ -15,6 +14,7 @@ import ( configv1 "github.com/openshift/api/config/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,7 +29,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { fakeAWSCloudConfig := func() *corev1.ConfigMap { cm := cpomanifests.AWSProviderConfig(hcpNamespace) cm.Data = map[string]string{ - aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + globalconfig.AWSProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", } return cm } @@ -78,7 +78,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, @@ -113,12 +113,12 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, { - name: "When AWS platform without any trust bundle, it should create cloud-provider-config without CA bundle", + name: "When AWS platform without any trust bundle, it should not create cloud-provider-config", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -130,22 +130,17 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, }, }, - cpObjects: []client.Object{ - fakeAWSCloudConfig(), - }, verify: func(g Gomega, guestClient client.Client) { cm := &corev1.ConfigMap{} err := guestClient.Get(context.Background(), client.ObjectKey{ Namespace: ConfigNamespace, Name: CloudProviderCMName, }, cm) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }, }, { - name: "When trust bundle is removed, it should remove CA bundle key from cloud-provider-config", + name: "When trust bundle is removed, it should delete cloud-provider-config", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -157,19 +152,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }, }, }, - cpObjects: []client.Object{ - fakeAWSCloudConfig(), - }, guestObjects: []client.Object{ - // Simulate a previously existing cloud-provider-config with CA bundle &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: ConfigNamespace, Name: CloudProviderCMName, }, Data: map[string]string{ - aws.ProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", - globalconfig.CABundleKey: "old-ca-bundle", + globalconfig.AWSProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + globalconfig.CABundleKey: "old-ca-bundle", }, }, }, @@ -179,13 +170,11 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Namespace: ConfigNamespace, Name: CloudProviderCMName, }, cm) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) - g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }, }, { - name: "When aws-cloud-config ConfigMap is missing, it should return an error", + name: "When aws-cloud-config ConfigMap is missing and trust bundle is set, it should return an error", hcp: &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -195,6 +184,9 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Platform: hyperv1.PlatformSpec{ Type: hyperv1.AWSPlatform, }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, }, }, cpObjects: []client.Object{}, @@ -227,7 +219,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, @@ -242,6 +234,9 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Platform: hyperv1.PlatformSpec{ Type: hyperv1.AWSPlatform, }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, }, }, cpObjects: []client.Object{ @@ -252,7 +247,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { }(), }, expectError: true, - errContains: aws.ProviderConfigKey, + errContains: globalconfig.AWSProviderConfigKey, }, { name: "When aws-cloud-config ConfigMap has whitespace-only provider config, it should return an error", @@ -265,19 +260,22 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Platform: hyperv1.PlatformSpec{ Type: hyperv1.AWSPlatform, }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: "user-ca-bundle", + }, }, }, cpObjects: []client.Object{ func() *corev1.ConfigMap { cm := cpomanifests.AWSProviderConfig(hcpNamespace) cm.Data = map[string]string{ - aws.ProviderConfigKey: " \n\t", + globalconfig.AWSProviderConfigKey: " \n\t", } return cm }(), }, expectError: true, - errContains: aws.ProviderConfigKey, + errContains: globalconfig.AWSProviderConfigKey, }, { name: "When proxy TrustedCA is set but managed trust bundle ConfigMap is missing, it should sync base config without CA bundle", @@ -309,7 +307,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, @@ -340,7 +338,7 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cm.Data).To(HaveKeyWithValue(aws.ProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 7c2eaa1b436a..8bde360dbc24 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -14,7 +14,6 @@ import ( hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane" - "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/aws" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/azure" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/cloud/openstack" kubevirtcsi "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/csi/kubevirt" @@ -2531,29 +2530,35 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host switch hcp.Spec.Platform.Type { case hyperv1.AWSPlatform: + hasTrustBundle := hcp.Spec.AdditionalTrustBundle != nil || + (hcp.Spec.Configuration != nil && hcp.Spec.Configuration.Proxy != nil && hcp.Spec.Configuration.Proxy.TrustedCA.Name != "") + + if !hasTrustBundle { + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} + if err := r.client.Delete(ctx, cm); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to clean up %s/%s configmap: %w", cm.Namespace, cm.Name, err) + } + return nil + } + reference := cpomanifests.AWSProviderConfig(hcp.Namespace) if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(reference), reference); err != nil { return fmt.Errorf("failed to fetch %s/%s configmap from management cluster: %w", reference.Namespace, reference.Name, err) } - providerConfig, ok := reference.Data[aws.ProviderConfigKey] + providerConfig, ok := reference.Data[globalconfig.AWSProviderConfigKey] if !ok || strings.TrimSpace(providerConfig) == "" { - return fmt.Errorf("source configmap %s/%s is missing required key %q", reference.Namespace, reference.Name, aws.ProviderConfigKey) + return fmt.Errorf("source configmap %s/%s is missing required key %q", reference.Namespace, reference.Name, globalconfig.AWSProviderConfigKey) } - // Use the aggregated trust bundle that combines spec.AdditionalTrustBundle - // and spec.Configuration.Proxy.TrustedCA, managed by the HCP controller. var caBundle string - if hcp.Spec.AdditionalTrustBundle != nil || - (hcp.Spec.Configuration != nil && hcp.Spec.Configuration.Proxy != nil && hcp.Spec.Configuration.Proxy.TrustedCA.Name != "") { - managedTrustBundle := cpomanifests.TrustedCABundleConfigMap(hcp.Namespace) - if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(managedTrustBundle), managedTrustBundle); err != nil { - if !apierrors.IsNotFound(err) { - return fmt.Errorf("failed to fetch managed trusted CA bundle from management cluster: %w", err) - } - ctrl.LoggerFrom(ctx).Info("managed trusted CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(managedTrustBundle)) - } else { - caBundle = managedTrustBundle.Data[certs.UserCABundleMapKey] + managedTrustBundle := cpomanifests.TrustedCABundleConfigMap(hcp.Namespace) + if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(managedTrustBundle), managedTrustBundle); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to fetch managed trusted CA bundle from management cluster: %w", err) } + ctrl.LoggerFrom(ctx).Info("managed trusted CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(managedTrustBundle)) + } else { + caBundle = managedTrustBundle.Data[certs.UserCABundleMapKey] } cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} @@ -2561,7 +2566,7 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host if cm.Data == nil { cm.Data = map[string]string{} } - cm.Data[aws.ProviderConfigKey] = providerConfig + cm.Data[globalconfig.AWSProviderConfigKey] = providerConfig if caBundle != "" { cm.Data[globalconfig.CABundleKey] = caBundle } else { diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 5cd3dbcf82a2..4e1085c08691 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -74,8 +74,13 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted switch platformType { case hyperv1.AWSPlatform: - infra.Spec.CloudConfig.Name = CloudProviderCMName - infra.Spec.CloudConfig.Key = AWSProviderConfigKey + if hcp.Spec.AdditionalTrustBundle != nil || + (hcp.Spec.Configuration != nil && hcp.Spec.Configuration.Proxy != nil && hcp.Spec.Configuration.Proxy.TrustedCA.Name != "") { + infra.Spec.CloudConfig.Name = CloudProviderCMName + infra.Spec.CloudConfig.Key = AWSProviderConfigKey + } else { + infra.Spec.CloudConfig = configv1.ConfigMapFileReference{} + } if infra.Spec.PlatformSpec.AWS == nil { infra.Spec.PlatformSpec.AWS = &configv1.AWSPlatformSpec{} } diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go index 05b253b98401..69ec1876999b 100644 --- a/support/globalconfig/infrastructure_test.go +++ b/support/globalconfig/infrastructure_test.go @@ -10,6 +10,7 @@ import ( configv1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) @@ -193,7 +194,7 @@ func TestReconcileInfrastructure(t *testing.T) { }, }, { - name: "When AWS platform is specified, it should set CloudConfig name and key", + name: "When AWS platform without trust bundle, it should not set CloudConfig", inputInfra: InfrastructureConfig(), inputHCP: func() *hyperv1.HostedControlPlane { hcp := baseHCP(hyperv1.AWSPlatform) @@ -202,6 +203,42 @@ func TestReconcileInfrastructure(t *testing.T) { } return hcp }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Spec.CloudConfig.Name).To(BeEmpty()) + g.Expect(infra.Spec.CloudConfig.Key).To(BeEmpty()) + }, + }, + { + name: "When AWS platform with AdditionalTrustBundle, it should set CloudConfig", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.AWSPlatform) + hcp.Spec.Platform.AWS = &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + } + hcp.Spec.AdditionalTrustBundle = &corev1.LocalObjectReference{Name: "user-ca-bundle"} + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) + g.Expect(infra.Spec.CloudConfig.Key).To(Equal(AWSProviderConfigKey)) + }, + }, + { + name: "When AWS platform with proxy TrustedCA, it should set CloudConfig", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.AWSPlatform) + hcp.Spec.Platform.AWS = &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + } + hcp.Spec.Configuration = &hyperv1.ClusterConfiguration{ + Proxy: &configv1.ProxySpec{ + TrustedCA: configv1.ConfigMapNameReference{Name: "proxy-ca"}, + }, + } + return hcp + }(), verify: func(g Gomega, infra *configv1.Infrastructure) { g.Expect(infra.Spec.CloudConfig.Name).To(Equal(CloudProviderCMName)) g.Expect(infra.Spec.CloudConfig.Key).To(Equal(AWSProviderConfigKey)) From 486481875b3b53a3e012be170055b84912f19f6f Mon Sep 17 00:00:00 2001 From: OpenShift CI Bot Date: Thu, 9 Jul 2026 20:02:23 +0000 Subject: [PATCH 10/10] fix(operator): address review feedback on AWS cloud config sync - Use k8sutil.DeleteIfNeeded for consistency with the rest of the file - Reword misleading "skipping CA bundle sync" log message to match actual behavior (execution continues into CreateOrUpdate) - Add kube-cloud-config ConfigMap in openshift-config-managed for AWS, mirroring the OpenStack pattern so CNO can pass the config to cloud-network-config-controller Co-Authored-By: Claude Opus 4.6 --- .../resources/reconcile_cloud_config_test.go | 69 +++++++++++++++++++ .../controllers/resources/resources.go | 39 +++++++---- 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go index 99c9cafd1a91..bb3a2526bb22 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/reconcile_cloud_config_test.go @@ -80,6 +80,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nfake-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, { @@ -115,6 +124,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.CABundleKey, "-----BEGIN CERTIFICATE-----\nproxy-ca-bundle\n-----END CERTIFICATE-----\n")) }, }, { @@ -137,6 +155,13 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }, }, { @@ -163,6 +188,16 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { globalconfig.CABundleKey: "old-ca-bundle", }, }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, + Data: map[string]string{ + globalconfig.AWSProviderConfigKey: "[Global]\nZone = us-east-1a\nVPC = vpc-123\n", + globalconfig.CABundleKey: "old-ca-bundle", + }, + }, }, verify: func(g Gomega, guestClient client.Client) { cm := &corev1.ConfigMap{} @@ -171,6 +206,13 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { Name: CloudProviderCMName, }, cm) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }, }, { @@ -221,6 +263,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(kcc.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -309,6 +360,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(kcc.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, { @@ -340,6 +400,15 @@ func TestReconcileCloudConfig_AWS(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cm.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) g.Expect(cm.Data).ToNot(HaveKey(globalconfig.CABundleKey)) + + kcc := &corev1.ConfigMap{} + err = guestClient.Get(context.Background(), client.ObjectKey{ + Namespace: ConfigManagedNamespace, + Name: "kube-cloud-config", + }, kcc) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcc.Data).To(HaveKeyWithValue(globalconfig.AWSProviderConfigKey, "[Global]\nZone = us-east-1a\nVPC = vpc-123\n")) + g.Expect(kcc.Data).ToNot(HaveKey(globalconfig.CABundleKey)) }, }, } diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 8bde360dbc24..72dd1d6b19d1 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -2535,9 +2535,13 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host if !hasTrustBundle { cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} - if err := r.client.Delete(ctx, cm); err != nil && !apierrors.IsNotFound(err) { + if _, err := k8sutil.DeleteIfNeeded(ctx, r.client, cm); err != nil { return fmt.Errorf("failed to clean up %s/%s configmap: %w", cm.Namespace, cm.Name, err) } + cmKCC := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigManagedNamespace, Name: "kube-cloud-config"}} + if _, err := k8sutil.DeleteIfNeeded(ctx, r.client, cmKCC); err != nil { + return fmt.Errorf("failed to clean up %s/%s configmap: %w", cmKCC.Namespace, cmKCC.Name, err) + } return nil } @@ -2556,25 +2560,30 @@ func (r *reconciler) reconcileCloudConfig(ctx context.Context, hcp *hyperv1.Host if !apierrors.IsNotFound(err) { return fmt.Errorf("failed to fetch managed trusted CA bundle from management cluster: %w", err) } - ctrl.LoggerFrom(ctx).Info("managed trusted CA bundle ConfigMap not found, skipping CA bundle sync", "configmap", client.ObjectKeyFromObject(managedTrustBundle)) + ctrl.LoggerFrom(ctx).Info("managed trusted CA bundle ConfigMap not found, proceeding without CA bundle", "configmap", client.ObjectKeyFromObject(managedTrustBundle)) } else { caBundle = managedTrustBundle.Data[certs.UserCABundleMapKey] } - cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: ConfigNamespace, Name: CloudProviderCMName}} - if _, err := r.CreateOrUpdate(ctx, r.client, cm, func() error { - if cm.Data == nil { - cm.Data = map[string]string{} - } - cm.Data[globalconfig.AWSProviderConfigKey] = providerConfig - if caBundle != "" { - cm.Data[globalconfig.CABundleKey] = caBundle - } else { - delete(cm.Data, globalconfig.CABundleKey) + for _, target := range []struct{ namespace, name string }{ + {ConfigNamespace, CloudProviderCMName}, + {ConfigManagedNamespace, "kube-cloud-config"}, + } { + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: target.namespace, Name: target.name}} + if _, err := r.CreateOrUpdate(ctx, r.client, cm, func() error { + if cm.Data == nil { + cm.Data = map[string]string{} + } + cm.Data[globalconfig.AWSProviderConfigKey] = providerConfig + if caBundle != "" { + cm.Data[globalconfig.CABundleKey] = caBundle + } else { + delete(cm.Data, globalconfig.CABundleKey) + } + return nil + }); err != nil { + return fmt.Errorf("failed to reconcile the %s/%s configmap: %w", cm.Namespace, cm.Name, err) } - return nil - }); err != nil { - return fmt.Errorf("failed to reconcile the %s/%s configmap: %w", cm.Namespace, cm.Name, err) } case hyperv1.AzurePlatform: // This is needed for the e2e tests and only for Azure: https://github.com/openshift/origin/blob/625733dd1ce7ebf40c3dd0abd693f7bb54f2d580/test/extended/util/cluster/cluster.go#L186