diff --git a/internal/controller/pattern_controller.go b/internal/controller/pattern_controller.go index 9ff9bf766..ed8ad91f2 100644 --- a/internal/controller/pattern_controller.go +++ b/internal/controller/pattern_controller.go @@ -254,6 +254,18 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return r.actionPerformed(qualifiedInstance, "error while creating trustedbundle cm", errCABundle) } + // Wait for the trusted-ca-bundle configmap to be populated by the cluster network operator + // before creating the ArgoCD CR. This prevents a race where the repo-server init container + // runs before the CA bundle is injected, leaving ArgoCD unable to verify public TLS certs. + populated, errPopulated := isTrustedBundleCMPopulated(r.fullClient, getClusterWideArgoNamespace()) + if errPopulated != nil { + return r.actionPerformed(qualifiedInstance, "error checking trusted-ca-bundle population", errPopulated) + } + if !populated { + return r.actionPerformed(qualifiedInstance, "waiting for trusted-ca-bundle to be populated", + fmt.Errorf("trusted-ca-bundle configmap in %s not yet populated by cluster network operator", getClusterWideArgoNamespace())) + } + // We only update the clusterwide argo instance so we can define our own 'initcontainers' section err = createOrUpdateArgoCD(r.dynamicClient, r.fullClient, ClusterWideArgoName, clusterWideNS) if err != nil { diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 212a4d3d6..6275ceae9 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -50,6 +50,8 @@ var ( logKeys = map[string]bool{} ) +const trustedBundleCM = "trusted-ca-bundle" + func logOnce(message string) { if _, ok := logKeys[message]; ok { return @@ -236,17 +238,16 @@ func createNamespace(kubeClient kubernetes.Interface, namespace string) error { } func createTrustedBundleCM(fullClient kubernetes.Interface, namespace string) error { - name := "trusted-ca-bundle" cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: name, + Name: trustedBundleCM, Namespace: namespace, Labels: map[string]string{ "config.openshift.io/inject-trusted-cabundle": "true", }, }, } - _, err := fullClient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + _, err := fullClient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), trustedBundleCM, metav1.GetOptions{}) if err != nil { if kerrors.IsNotFound(err) { _, err = fullClient.CoreV1().ConfigMaps(namespace).Create(context.TODO(), cm, metav1.CreateOptions{}) @@ -257,6 +258,22 @@ func createTrustedBundleCM(fullClient kubernetes.Interface, namespace string) er return nil } +// isTrustedBundleCMPopulated checks if the trusted-ca-bundle configmap has been +// populated with CA certificates by the cluster network operator. This prevents +// a race condition where the ArgoCD repo-server starts before the CA bundle is +// injected, causing TLS verification failures for public repositories. +func isTrustedBundleCMPopulated(fullClient kubernetes.Interface, namespace string) (bool, error) { + cm, err := fullClient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), trustedBundleCM, metav1.GetOptions{}) + if err != nil { + return false, err + } + bundle, ok := cm.Data["ca-bundle.crt"] + if !ok || bundle == "" { + return false, nil + } + return true, nil +} + func getClusterWideArgoNamespace() string { // Once we add support for running the cluster-wide argo instance // we will need to amend the logic here diff --git a/internal/controller/utils_test.go b/internal/controller/utils_test.go index 6d2def7f1..5e1800fd9 100644 --- a/internal/controller/utils_test.go +++ b/internal/controller/utils_test.go @@ -2510,3 +2510,59 @@ var _ = Describe("createTrustedBundleCM", func() { }) }) }) + +var _ = Describe("isTrustedBundleCMPopulated", func() { + var kubeClient kubernetes.Interface + + BeforeEach(func() { + kubeClient = fake.NewSimpleClientset() + }) + + Context("when the configmap does not exist", func() { + It("should return an error", func() { + _, err := isTrustedBundleCMPopulated(kubeClient, "test-namespace") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when the configmap exists but has no data", func() { + BeforeEach(func() { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "trusted-ca-bundle", + Namespace: "test-namespace", + }, + } + _, err := kubeClient.CoreV1().ConfigMaps("test-namespace").Create(context.Background(), cm, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return false", func() { + populated, err := isTrustedBundleCMPopulated(kubeClient, "test-namespace") + Expect(err).ToNot(HaveOccurred()) + Expect(populated).To(BeFalse()) + }) + }) + + Context("when the configmap exists and has CA data", func() { + BeforeEach(func() { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "trusted-ca-bundle", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "ca-bundle.crt": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----\n", + }, + } + _, err := kubeClient.CoreV1().ConfigMaps("test-namespace").Create(context.Background(), cm, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return true", func() { + populated, err := isTrustedBundleCMPopulated(kubeClient, "test-namespace") + Expect(err).ToNot(HaveOccurred()) + Expect(populated).To(BeTrue()) + }) + }) +})