@@ -25,7 +25,9 @@ import (
2525 certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
2626 cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
2727 operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/api/v1alpha1"
28+ corev1 "k8s.io/api/core/v1"
2829 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+ "k8s.io/apimachinery/pkg/types"
2931 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3032 logf "sigs.k8s.io/controller-runtime/pkg/log"
3133)
@@ -42,6 +44,9 @@ const (
4244 caCertificateSuffix = "-ca"
4345 controllerCertSuffix = "-controller-tls"
4446 routerCertSuffix = "-router-%d-tls"
47+
48+ // CA ConfigMap naming
49+ caConfigMapSuffix = "-service-ca-cert"
4550)
4651
4752// getServerCertDurationSettings
@@ -73,6 +78,12 @@ func getServerCertDurationSettings(js *operatorv1alpha1.Jumpstarter) (time.Durat
7378func (r * JumpstarterReconciler ) reconcileCertificates (ctx context.Context , js * operatorv1alpha1.Jumpstarter ) error {
7479 log := logf .FromContext (ctx )
7580
81+ // Always reconcile the CA ConfigMap first - this ensures cleanup during config transitions
82+ // and provides the CA bundle for clients regardless of certificate reconciliation state
83+ if err := r .reconcileCAConfigMap (ctx , js ); err != nil {
84+ return fmt .Errorf ("failed to reconcile CA ConfigMap: %w" , err )
85+ }
86+
7687 if ! js .Spec .CertManager .Enabled {
7788 // If cert-manager integration is disabled, skip certificate reconciliation
7889 // we do not remove existing certificates or issuers here,
@@ -462,6 +473,103 @@ func (r *JumpstarterReconciler) reconcileCertificateResource(ctx context.Context
462473 return nil
463474}
464475
476+ // reconcileCAConfigMap creates or updates the CA certificate ConfigMap.
477+ // This ConfigMap contains the CA bundle that clients can use to verify TLS connections.
478+ // The ConfigMap is ALWAYS created to ensure proper cleanup during configuration transitions.
479+ // This configmap is used by jmp admin cli when creating exporters or clients.
480+ func (r * JumpstarterReconciler ) reconcileCAConfigMap (ctx context.Context , js * operatorv1alpha1.Jumpstarter ) error {
481+ log := logf .FromContext (ctx )
482+
483+ // fixed name because we only support one "jumpstater" per namespace, and
484+ // we want to ensure that the CA configmap is findable by jmp admin cli
485+ // when creating exporters or clients
486+ configMapName := "jumpstarter" + caConfigMapSuffix
487+ caCert := ""
488+
489+ // If cert-manager is disabled, create empty ConfigMap
490+ if ! js .Spec .CertManager .Enabled {
491+ log .V (1 ).Info ("cert-manager disabled, creating empty CA ConfigMap" )
492+ } else if js .Spec .CertManager .Server != nil && js .Spec .CertManager .Server .IssuerRef != nil {
493+ // External issuer mode
494+ if len (js .Spec .CertManager .Server .IssuerRef .CABundle ) > 0 {
495+ // Use provided CA bundle
496+ caCert = string (js .Spec .CertManager .Server .IssuerRef .CABundle )
497+ log .V (1 ).Info ("Using CA bundle from external issuer configuration" )
498+ } else {
499+ // External issuer without CA bundle - leave empty (publicly trusted CA)
500+ log .V (1 ).Info ("External issuer without CA bundle, creating empty CA ConfigMap" )
501+ }
502+ } else {
503+ // Self-signed CA mode - read from CA secret
504+ selfSignedEnabled := true
505+ if js .Spec .CertManager .Server != nil && js .Spec .CertManager .Server .SelfSigned != nil {
506+ selfSignedEnabled = js .Spec .CertManager .Server .SelfSigned .Enabled
507+ }
508+
509+ if selfSignedEnabled {
510+ caSecretName := js .Name + caCertificateSuffix
511+ caSecret := & corev1.Secret {}
512+ err := r .Client .Get (ctx , types.NamespacedName {
513+ Name : caSecretName ,
514+ Namespace : js .Namespace ,
515+ }, caSecret )
516+ if err != nil {
517+ // CA secret doesn't exist yet - this is expected during initial setup
518+ // The ConfigMap will be updated once the CA certificate is ready
519+ log .V (1 ).Info ("CA secret not found, creating empty CA ConfigMap" , "secret" , caSecretName )
520+ } else if cert , ok := caSecret .Data ["tls.crt" ]; ok {
521+ caCert = string (cert )
522+ log .V (1 ).Info ("Using CA certificate from self-signed CA secret" , "secret" , caSecretName )
523+ } else {
524+ log .V (1 ).Info ("CA secret missing tls.crt key, creating empty CA ConfigMap" , "secret" , caSecretName )
525+ }
526+ } else {
527+ // Self-signed disabled and no external issuer - leave empty
528+ log .V (1 ).Info ("Self-signed CA disabled, creating empty CA ConfigMap" )
529+ }
530+ }
531+
532+ // Create the ConfigMap
533+ labels := map [string ]string {
534+ "app" : js .Name ,
535+ "app.kubernetes.io/managed-by" : "jumpstarter-operator" ,
536+ }
537+
538+ desiredConfigMap := & corev1.ConfigMap {
539+ ObjectMeta : metav1.ObjectMeta {
540+ Name : configMapName ,
541+ Namespace : js .Namespace ,
542+ Labels : labels ,
543+ },
544+ Data : map [string ]string {
545+ "ca.crt" : caCert ,
546+ },
547+ }
548+
549+ existingConfigMap := & corev1.ConfigMap {}
550+ existingConfigMap .Name = desiredConfigMap .Name
551+ existingConfigMap .Namespace = desiredConfigMap .Namespace
552+
553+ op , err := controllerutil .CreateOrUpdate (ctx , r .Client , existingConfigMap , func () error {
554+ existingConfigMap .Labels = desiredConfigMap .Labels
555+ existingConfigMap .Data = desiredConfigMap .Data
556+ return controllerutil .SetControllerReference (js , existingConfigMap , r .Scheme )
557+ })
558+
559+ if err != nil {
560+ return fmt .Errorf ("failed to reconcile CA ConfigMap %s: %w" , configMapName , err )
561+ }
562+
563+ log .Info ("CA ConfigMap reconciled" , "name" , configMapName , "operation" , op , "hasCA" , caCert != "" )
564+ return nil
565+ }
566+
567+ // GetCAConfigMapName returns the name of the CA certificate ConfigMap.
568+ // The name is fixed to "jumpstarter-service-ca-cert" for discoverability by jmp admin cli.
569+ func GetCAConfigMapName (js * operatorv1alpha1.Jumpstarter ) string {
570+ return "jumpstarter" + caConfigMapSuffix
571+ }
572+
465573// GetControllerCertSecretName returns the name of the controller TLS secret.
466574func GetControllerCertSecretName (js * operatorv1alpha1.Jumpstarter ) string {
467575 return js .Name + controllerCertSuffix
0 commit comments