diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go index 56c48bc14283..db77f19adf77 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go @@ -2080,6 +2080,38 @@ 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) + } + + 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["config"] = reference.Data["aws.conf"] + + // Sync CA bundle from additionalTrustBundle if configured. + // The CCCMO's trusted_ca_bundle_controller looks for the "ca-bundle.pem" key + // in the cloud-provider-config ConfigMap to include in the merged trust bundle + // for CCM pods. This ensures that custom CAs for AWS API endpoints are trusted. + 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 get AdditionalTrustBundle ConfigMap: %w", err) + } + if caBundle, ok := cpUserCAConfigMap.Data["ca-bundle.crt"]; ok { + cm.Data["ca-bundle.pem"] = caBundle + } + } else { + delete(cm.Data, "ca-bundle.pem") + } + 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/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go index 69ac404af2e5..1404ffbdcd42 100644 --- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go @@ -600,6 +600,144 @@ func TestReconcileUserCertCABundle(t *testing.T) { } } +func TestReconcileCloudConfigAWS(t *testing.T) { + testNamespace := "master-cluster1" + testHCPName := "cluster1" + tests := map[string]struct { + inputHCP *hyperv1.HostedControlPlane + inputObjects []client.Object + expectConfig bool + expectCABundle bool + }{ + "When AWS platform has no additionalTrustBundle it should create cloud-provider-config without ca-bundle.pem": { + inputHCP: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: testHCPName, + Namespace: testNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + inputObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: cpomanifests.AWSProviderConfig(testNamespace).ObjectMeta, + Data: map[string]string{ + "aws.conf": "[Global]\nZone = us-east-1a\n", + }, + }, + }, + expectConfig: true, + expectCABundle: false, + }, + "When AWS platform has additionalTrustBundle it should include ca-bundle.pem in cloud-provider-config": { + inputHCP: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: testHCPName, + Namespace: testNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + AdditionalTrustBundle: &corev1.LocalObjectReference{ + Name: cpomanifests.UserCAConfigMap(testNamespace).Name, + }, + }, + }, + inputObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: cpomanifests.AWSProviderConfig(testNamespace).ObjectMeta, + Data: map[string]string{ + "aws.conf": "[Global]\nZone = us-east-1a\n", + }, + }, + &corev1.ConfigMap{ + ObjectMeta: cpomanifests.UserCAConfigMap(testNamespace).ObjectMeta, + Data: map[string]string{ + "ca-bundle.crt": "-----BEGIN CERTIFICATE-----\ntest-ca-cert\n-----END CERTIFICATE-----\n", + }, + }, + }, + expectConfig: true, + expectCABundle: true, + }, + "When AWS platform removes additionalTrustBundle it should remove ca-bundle.pem from cloud-provider-config": { + inputHCP: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: testHCPName, + Namespace: testNamespace, + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + }, + }, + }, + inputObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: cpomanifests.AWSProviderConfig(testNamespace).ObjectMeta, + Data: map[string]string{ + "aws.conf": "[Global]\nZone = us-east-1a\n", + }, + }, + }, + expectConfig: true, + expectCABundle: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + g := NewGomegaWithT(t) + + // For the "remove" test case, pre-populate the guest cluster with a ConfigMap that has ca-bundle.pem + guestObjects := []client.Object{} + if strings.Contains(name, "removes") { + guestObjects = append(guestObjects, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, + Data: map[string]string{ + "config": "[Global]\nZone = us-east-1a\n", + "ca-bundle.pem": "old-ca-bundle", + }, + }) + } + + r := &reconciler{ + client: fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(guestObjects...).Build(), + CreateOrUpdateProvider: &simpleCreateOrUpdater{}, + cpClient: fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(append(test.inputObjects, test.inputHCP)...).Build(), + hcpName: testHCPName, + hcpNamespace: testNamespace, + } + err := r.reconcileCloudConfig(t.Context(), test.inputHCP) + g.Expect(err).To(BeNil()) + + guestCloudConfig := &corev1.ConfigMap{} + err = r.client.Get(t.Context(), types.NamespacedName{ + Namespace: ConfigNamespace, + Name: CloudProviderCMName, + }, guestCloudConfig) + + if test.expectConfig { + g.Expect(err).To(BeNil()) + g.Expect(guestCloudConfig.Data["config"]).To(Equal("[Global]\nZone = us-east-1a\n")) + } + + if test.expectCABundle { + g.Expect(guestCloudConfig.Data["ca-bundle.pem"]).To(Equal("-----BEGIN CERTIFICATE-----\ntest-ca-cert\n-----END CERTIFICATE-----\n")) + } else if test.expectConfig { + _, hasCABundle := guestCloudConfig.Data["ca-bundle.pem"] + g.Expect(hasCABundle).To(BeFalse()) + } + }) + } +} + var _ manifestReconciler = manifestAndReconcile[*rbacv1.ClusterRole]{} func TestDestroyCloudResources(t *testing.T) { diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 525f41ced2b9..ce4bc7e52548 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -77,6 +77,11 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted }) } infra.Status.PlatformStatus.AWS.ResourceTags = tags + // Set CloudConfig to reference the cloud-provider-config ConfigMap in the guest cluster. + // This allows the kube-cloud-config controller and CCCMO to locate the cloud provider + // configuration, including any additional trust bundles for custom AWS API endpoints. + infra.Spec.CloudConfig.Name = "cloud-provider-config" + infra.Spec.CloudConfig.Key = "config" case hyperv1.AzurePlatform: infra.Spec.CloudConfig.Name = "cloud.conf" if infra.Status.PlatformStatus.Azure == nil { diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go new file mode 100644 index 000000000000..c6178a8b00da --- /dev/null +++ b/support/globalconfig/infrastructure_test.go @@ -0,0 +1,69 @@ +package globalconfig + +import ( + "testing" + + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + + configv1 "github.com/openshift/api/config/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestReconcileInfrastructure_AWSCloudConfig(t *testing.T) { + tests := map[string]struct { + hcp *hyperv1.HostedControlPlane + expectedCloudConfigName string + expectedCloudConfigKey string + }{ + "When reconciling AWS infrastructure it should set CloudConfig name and key": { + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: "master-cluster1", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + InfraID: "test-infra-id", + }, + Status: hyperv1.HostedControlPlaneStatus{ + ControlPlaneEndpoint: hyperv1.APIEndpoint{ + Host: "api.example.com", + Port: 6443, + }, + }, + }, + expectedCloudConfigName: "cloud-provider-config", + expectedCloudConfigKey: "config", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + infra := InfrastructureConfig() + ReconcileInfrastructure(infra, test.hcp) + + if infra.Spec.CloudConfig.Name != test.expectedCloudConfigName { + t.Errorf("expected CloudConfig.Name = %q, got %q", test.expectedCloudConfigName, infra.Spec.CloudConfig.Name) + } + if infra.Spec.CloudConfig.Key != test.expectedCloudConfigKey { + t.Errorf("expected CloudConfig.Key = %q, got %q", test.expectedCloudConfigKey, infra.Spec.CloudConfig.Key) + } + + if infra.Status.PlatformStatus == nil || infra.Status.PlatformStatus.AWS == nil { + t.Fatal("expected AWS PlatformStatus to be set") + } + if infra.Status.PlatformStatus.AWS.Region != "us-east-1" { + t.Errorf("expected AWS region = %q, got %q", "us-east-1", infra.Status.PlatformStatus.AWS.Region) + } + if infra.Spec.PlatformSpec.Type != configv1.AWSPlatformType { + t.Errorf("expected PlatformSpec.Type = %q, got %q", configv1.AWSPlatformType, infra.Spec.PlatformSpec.Type) + } + }) + } +}