Skip to content

Commit 77c7f09

Browse files
committed
cert-manager: expose ca-bundle as configmap for jmp
1 parent 4cbecf2 commit 77c7f09

6 files changed

Lines changed: 187 additions & 1 deletion

File tree

controller/deploy/operator/api/v1alpha1/jumpstarter_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,13 @@ type IssuerReference struct {
622622
// Only change this if using a custom issuer from a different API group.
623623
// +kubebuilder:default="cert-manager.io"
624624
Group string `json:"group,omitempty"`
625+
626+
// CABundle is an optional base64-encoded PEM CA certificate bundle for this issuer.
627+
// Required when using external issuers with non-publicly-trusted CAs.
628+
// This will be published to the {name}-service-ca-cert ConfigMap for clients to use.
629+
// For self-signed CA mode, this is automatically populated from the CA secret.
630+
// +optional
631+
CABundle []byte `json:"caBundle,omitempty"`
625632
}
626633

627634
// JumpstarterStatus defines the observed state of Jumpstarter.

controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ spec:
476476
Use this to integrate with existing PKI infrastructure (ACME, Vault, etc.).
477477
This overrides SelfSigned.Enabled = true which is the default setting
478478
properties:
479+
caBundle:
480+
description: |-
481+
CABundle is an optional base64-encoded PEM CA certificate bundle for this issuer.
482+
Required when using external issuers with non-publicly-trusted CAs.
483+
This will be published to the {name}-service-ca-cert ConfigMap for clients to use.
484+
For self-signed CA mode, this is automatically populated from the CA secret.
485+
format: byte
486+
type: string
479487
group:
480488
default: cert-manager.io
481489
description: |-

controller/deploy/operator/dist/install.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,14 @@ spec:
879879
Use this to integrate with existing PKI infrastructure (ACME, Vault, etc.).
880880
This overrides SelfSigned.Enabled = true which is the default setting
881881
properties:
882+
caBundle:
883+
description: |-
884+
CABundle is an optional base64-encoded PEM CA certificate bundle for this issuer.
885+
Required when using external issuers with non-publicly-trusted CAs.
886+
This will be published to the {name}-service-ca-cert ConfigMap for clients to use.
887+
For self-signed CA mode, this is automatically populated from the CA secret.
888+
format: byte
889+
type: string
882890
group:
883891
default: cert-manager.io
884892
description: |-

controller/deploy/operator/internal/controller/jumpstarter/certificates.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
7378
func (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.
466574
func GetControllerCertSecretName(js *operatorv1alpha1.Jumpstarter) string {
467575
return js.Name + controllerCertSuffix

controller/deploy/operator/test/e2e/e2e_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,36 @@ spec:
886886
verifyTLSSecret(certManagerTestNamespace, routerCertName)
887887
})
888888

889+
It("should create the CA ConfigMap with the CA certificate", func() {
890+
By("verifying the CA ConfigMap was created with the correct CA certificate")
891+
caConfigMapName := "jumpstarter-service-ca-cert"
892+
caSecretName := jumpstarterName + "-ca"
893+
894+
Eventually(func(g Gomega) {
895+
// Get the CA ConfigMap
896+
cm := &corev1.ConfigMap{}
897+
err := k8sClient.Get(ctx, types.NamespacedName{
898+
Name: caConfigMapName,
899+
Namespace: certManagerTestNamespace,
900+
}, cm)
901+
g.Expect(err).NotTo(HaveOccurred())
902+
903+
// Get the CA secret to compare
904+
caSecret := &corev1.Secret{}
905+
err = k8sClient.Get(ctx, types.NamespacedName{
906+
Name: caSecretName,
907+
Namespace: certManagerTestNamespace,
908+
}, caSecret)
909+
g.Expect(err).NotTo(HaveOccurred())
910+
911+
// Verify the CA ConfigMap contains the CA certificate from the secret
912+
g.Expect(cm.Data).To(HaveKey("ca.crt"))
913+
g.Expect(cm.Data["ca.crt"]).NotTo(BeEmpty(), "CA ConfigMap should contain the CA certificate")
914+
g.Expect(cm.Data["ca.crt"]).To(Equal(string(caSecret.Data["tls.crt"])),
915+
"CA ConfigMap should contain the same certificate as the CA secret")
916+
}, 2*time.Minute).Should(Succeed())
917+
})
918+
889919
It("should mount TLS certificates in controller deployment", func() {
890920
By("waiting for controller deployment to be available with TLS mount")
891921
controllerDeploymentName := jumpstarterName + "-controller"
@@ -1180,6 +1210,26 @@ spec:
11801210
verifyTLSSecret(externalIssuerTestNamespace, routerCertName)
11811211
})
11821212

1213+
It("should create an empty CA ConfigMap for external issuer without CABundle", func() {
1214+
By("verifying the CA ConfigMap was created but is empty (external issuer without CABundle)")
1215+
// Fixed name for discoverability by jmp admin cli
1216+
caConfigMapName := "jumpstarter-service-ca-cert"
1217+
1218+
Eventually(func(g Gomega) {
1219+
cm := &corev1.ConfigMap{}
1220+
err := k8sClient.Get(ctx, types.NamespacedName{
1221+
Name: caConfigMapName,
1222+
Namespace: externalIssuerTestNamespace,
1223+
}, cm)
1224+
g.Expect(err).NotTo(HaveOccurred())
1225+
1226+
// Verify the CA ConfigMap exists but ca.crt is empty (publicly trusted CA)
1227+
g.Expect(cm.Data).To(HaveKey("ca.crt"))
1228+
g.Expect(cm.Data["ca.crt"]).To(BeEmpty(),
1229+
"CA ConfigMap should be empty for external issuer without CABundle")
1230+
}, 1*time.Minute).Should(Succeed())
1231+
})
1232+
11831233
AfterAll(func() {
11841234
DeleteTestNamespace(externalIssuerTestNamespace)
11851235
_ = deleteSelfSignedClusterIssuer(clusterIssuerName)

0 commit comments

Comments
 (0)