Skip to content

Commit 59bbc49

Browse files
committed
cert-manager: verify non-deletion of certs/issuers when disabled
1 parent d37dd5a commit 59bbc49

8 files changed

Lines changed: 183 additions & 47 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ type ExporterOptions struct {
269269
type GRPCConfig struct {
270270
// TLS configuration for secure gRPC communication.
271271
// Requires a Kubernetes secret containing the TLS certificate and private key.
272-
// If useCertManager is enabled, this secret will be automatically created.
273-
// See also: spec.useCertManager for automatic certificate management.
272+
// If spec.certManager.enabled is true, this secret will be automatically managed and
273+
// configured by cert-manager.
274274
TLS TLSConfig `json:"tls,omitempty"`
275275

276276
// List of gRPC endpoints to expose.

controller/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ metadata:
1818
}
1919
]
2020
capabilities: Basic Install
21-
createdAt: "2026-01-28T10:52:26Z"
21+
createdAt: "2026-01-28T15:18:09Z"
2222
operators.operatorframework.io/builder: operator-sdk-v1.41.1
2323
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
2424
name: jumpstarter-operator.v0.8.0

controller/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,8 @@ spec:
776776
description: |-
777777
TLS configuration for secure gRPC communication.
778778
Requires a Kubernetes secret containing the TLS certificate and private key.
779-
If useCertManager is enabled, this secret will be automatically created.
780-
See also: spec.useCertManager for automatic certificate management.
779+
If spec.certManager.enabled is true, this secret will be automatically managed and
780+
configured by cert-manager.
781781
properties:
782782
certSecret:
783783
description: |-
@@ -1323,8 +1323,8 @@ spec:
13231323
description: |-
13241324
TLS configuration for secure gRPC communication.
13251325
Requires a Kubernetes secret containing the TLS certificate and private key.
1326-
If useCertManager is enabled, this secret will be automatically created.
1327-
See also: spec.useCertManager for automatic certificate management.
1326+
If spec.certManager.enabled is true, this secret will be automatically managed and
1327+
configured by cert-manager.
13281328
properties:
13291329
certSecret:
13301330
description: |-

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,8 @@ spec:
776776
description: |-
777777
TLS configuration for secure gRPC communication.
778778
Requires a Kubernetes secret containing the TLS certificate and private key.
779-
If useCertManager is enabled, this secret will be automatically created.
780-
See also: spec.useCertManager for automatic certificate management.
779+
If spec.certManager.enabled is true, this secret will be automatically managed and
780+
configured by cert-manager.
781781
properties:
782782
certSecret:
783783
description: |-
@@ -1323,8 +1323,8 @@ spec:
13231323
description: |-
13241324
TLS configuration for secure gRPC communication.
13251325
Requires a Kubernetes secret containing the TLS certificate and private key.
1326-
If useCertManager is enabled, this secret will be automatically created.
1327-
See also: spec.useCertManager for automatic certificate management.
1326+
If spec.certManager.enabled is true, this secret will be automatically managed and
1327+
configured by cert-manager.
13281328
properties:
13291329
certSecret:
13301330
description: |-

controller/deploy/operator/dist/install.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,8 +1179,8 @@ spec:
11791179
description: |-
11801180
TLS configuration for secure gRPC communication.
11811181
Requires a Kubernetes secret containing the TLS certificate and private key.
1182-
If useCertManager is enabled, this secret will be automatically created.
1183-
See also: spec.useCertManager for automatic certificate management.
1182+
If spec.certManager.enabled is true, this secret will be automatically managed and
1183+
configured by cert-manager.
11841184
properties:
11851185
certSecret:
11861186
description: |-
@@ -1726,8 +1726,8 @@ spec:
17261726
description: |-
17271727
TLS configuration for secure gRPC communication.
17281728
Requires a Kubernetes secret containing the TLS certificate and private key.
1729-
If useCertManager is enabled, this secret will be automatically created.
1730-
See also: spec.useCertManager for automatic certificate management.
1729+
If spec.certManager.enabled is true, this secret will be automatically managed and
1730+
configured by cert-manager.
17311731
properties:
17321732
certSecret:
17331733
description: |-

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ func (r *JumpstarterReconciler) reconcileCertificates(ctx context.Context, js *o
7474
log := logf.FromContext(ctx)
7575

7676
if !js.Spec.CertManager.Enabled {
77+
// If cert-manager integration is disabled, skip certificate reconciliation
78+
// we do not remove existing certificates or issuers here,
79+
// that must be handled by the administrator, to avoid issues if the certificates
80+
// are disabled by error, enabled again, at least the certificates will remain.
7781
log.V(1).Info("cert-manager integration disabled, skipping certificate reconciliation")
7882
return nil
7983
}
@@ -86,6 +90,12 @@ func (r *JumpstarterReconciler) reconcileCertificates(ctx context.Context, js *o
8690
return fmt.Errorf("failed to reconcile issuer: %w", err)
8791
}
8892

93+
// Skip certificate creation if no issuer is configured
94+
if issuerRef.Name == "" {
95+
log.Info("No issuer configured, skipping certificate creation")
96+
return nil
97+
}
98+
8999
// Create controller certificate
90100
if err := r.reconcileControllerCertificate(ctx, js, issuerRef); err != nil {
91101
return fmt.Errorf("failed to reconcile controller certificate: %w", err)
@@ -117,9 +127,22 @@ func (r *JumpstarterReconciler) reconcileIssuer(ctx context.Context, js *operato
117127
}, nil
118128
}
119129

120-
// Default to self-signed CA mode
121-
log.Info("Using self-signed CA mode")
122-
return r.reconcileSelfSignedIssuer(ctx, js)
130+
// Check if self-signed mode is enabled
131+
// Default to true if SelfSigned config is nil or Enabled is not explicitly set
132+
selfSignedEnabled := true
133+
if js.Spec.CertManager.Server != nil && js.Spec.CertManager.Server.SelfSigned != nil {
134+
selfSignedEnabled = js.Spec.CertManager.Server.SelfSigned.Enabled
135+
}
136+
137+
if selfSignedEnabled {
138+
log.Info("Using self-signed CA mode")
139+
return r.reconcileSelfSignedIssuer(ctx, js)
140+
}
141+
142+
// Self-signed is disabled and no external issuer is configured
143+
// Return empty issuer reference - status update will handle the error condition
144+
log.Info("Self-signed CA is disabled and no external issuer configured, skipping issuer creation")
145+
return cmmeta.ObjectReference{}, nil
123146
}
124147

125148
// reconcileSelfSignedIssuer creates the self-signed CA infrastructure:

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ func (r *JumpstarterReconciler) checkIssuerReady(ctx context.Context, js *operat
158158
issuerName = js.Spec.CertManager.Server.IssuerRef.Name
159159
issuerKind = js.Spec.CertManager.Server.IssuerRef.Kind
160160
} else {
161+
// Check if self-signed is enabled (default to true if not specified)
162+
selfSignedEnabled := true
163+
if js.Spec.CertManager.Server != nil && js.Spec.CertManager.Server.SelfSigned != nil {
164+
selfSignedEnabled = js.Spec.CertManager.Server.SelfSigned.Enabled
165+
}
166+
167+
if !selfSignedEnabled {
168+
// Self-signed is disabled and no external issuer is configured
169+
return false, "no issuer configured: selfSigned.enabled is false and no external issuerRef provided"
170+
}
171+
161172
// Self-signed CA issuer
162173
issuerName = js.Name + caIssuerSuffix
163174
issuerKind = "Issuer"

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

Lines changed: 131 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -855,33 +855,19 @@ spec:
855855
})
856856

857857
It("should mount TLS certificates in controller deployment", func() {
858-
By("waiting for controller deployment to be available")
858+
By("waiting for controller deployment to be available with TLS mount")
859859
controllerDeploymentName := jumpstarterName + "-controller"
860860
Eventually(func(g Gomega) {
861-
deployment := &appsv1.Deployment{}
862-
err := k8sClient.Get(ctx, types.NamespacedName{
863-
Name: controllerDeploymentName,
864-
Namespace: certManagerTestNamespace,
865-
}, deployment)
866-
g.Expect(err).NotTo(HaveOccurred())
867-
}, 2*time.Minute).Should(Succeed())
868-
869-
verifyDeploymentHasTLSMount(certManagerTestNamespace, controllerDeploymentName)
861+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, controllerDeploymentName)
862+
}, 2*time.Minute, 2*time.Second).Should(Succeed())
870863
})
871864

872865
It("should mount TLS certificates in router deployment", func() {
873-
By("waiting for router deployment to be available")
866+
By("waiting for router deployment to be available with TLS mount")
874867
routerDeploymentName := jumpstarterName + "-router-0"
875868
Eventually(func(g Gomega) {
876-
deployment := &appsv1.Deployment{}
877-
err := k8sClient.Get(ctx, types.NamespacedName{
878-
Name: routerDeploymentName,
879-
Namespace: certManagerTestNamespace,
880-
}, deployment)
881-
g.Expect(err).NotTo(HaveOccurred())
882-
}, 2*time.Minute).Should(Succeed())
883-
884-
verifyDeploymentHasTLSMount(certManagerTestNamespace, routerDeploymentName)
869+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, routerDeploymentName)
870+
}, 2*time.Minute, 2*time.Second).Should(Succeed())
885871
})
886872

887873
It("should report CertManagerAvailable condition as True", func() {
@@ -940,6 +926,86 @@ spec:
940926
}
941927
})
942928

929+
It("should preserve certificates when cert-manager is disabled and re-enabled", func() {
930+
By("starting with cert-manager enabled and verifying TLS is configured")
931+
controllerDeploymentName := jumpstarterName + "-controller"
932+
routerDeploymentName := jumpstarterName + "-router-0"
933+
controllerCertName := jumpstarterName + "-controller-tls"
934+
routerCertName := fmt.Sprintf("%s-router-0-tls", jumpstarterName)
935+
issuerNames := []string{
936+
jumpstarterName + "-selfsigned-issuer",
937+
jumpstarterName + "-ca-issuer",
938+
}
939+
940+
// Verify initial state has TLS configured
941+
Eventually(func(g Gomega) {
942+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, controllerDeploymentName)
943+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, routerDeploymentName)
944+
}, 1*time.Minute, 2*time.Second).Should(Succeed())
945+
946+
By("disabling cert-manager")
947+
jumpstarter := &operatorv1alpha1.Jumpstarter{}
948+
err := k8sClient.Get(ctx, types.NamespacedName{
949+
Name: jumpstarterName,
950+
Namespace: certManagerTestNamespace,
951+
}, jumpstarter)
952+
Expect(err).NotTo(HaveOccurred())
953+
954+
jumpstarter.Spec.CertManager.Enabled = false
955+
err = k8sClient.Update(ctx, jumpstarter)
956+
Expect(err).NotTo(HaveOccurred())
957+
958+
By("waiting for and verifying deployments are reconciled WITHOUT TLS configuration")
959+
Eventually(func(g Gomega) {
960+
verifyDeploymentHasNoTLSMount(g, certManagerTestNamespace, controllerDeploymentName)
961+
verifyDeploymentHasNoTLSMount(g, certManagerTestNamespace, routerDeploymentName)
962+
}, 2*time.Minute, 2*time.Second).Should(Succeed())
963+
964+
By("verifying Certificate and Issuer resources still exist (not deleted)")
965+
cert := &certmanagerv1.Certificate{}
966+
err = k8sClient.Get(ctx, types.NamespacedName{
967+
Name: controllerCertName,
968+
Namespace: certManagerTestNamespace,
969+
}, cert)
970+
Expect(err).NotTo(HaveOccurred(), "Controller certificate should still exist")
971+
972+
err = k8sClient.Get(ctx, types.NamespacedName{
973+
Name: routerCertName,
974+
Namespace: certManagerTestNamespace,
975+
}, cert)
976+
Expect(err).NotTo(HaveOccurred(), "Router certificate should still exist")
977+
978+
for _, issuerName := range issuerNames {
979+
issuer := &certmanagerv1.Issuer{}
980+
err = k8sClient.Get(ctx, types.NamespacedName{
981+
Name: issuerName,
982+
Namespace: certManagerTestNamespace,
983+
}, issuer)
984+
Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Issuer %s should still exist", issuerName))
985+
}
986+
987+
By("re-enabling cert-manager")
988+
err = k8sClient.Get(ctx, types.NamespacedName{
989+
Name: jumpstarterName,
990+
Namespace: certManagerTestNamespace,
991+
}, jumpstarter)
992+
Expect(err).NotTo(HaveOccurred())
993+
994+
jumpstarter.Spec.CertManager.Enabled = true
995+
err = k8sClient.Update(ctx, jumpstarter)
996+
Expect(err).NotTo(HaveOccurred())
997+
998+
By("verifying deployments are reconciled WITH TLS configuration again")
999+
Eventually(func(g Gomega) {
1000+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, controllerDeploymentName)
1001+
verifyDeploymentHasTLSMount(g, certManagerTestNamespace, routerDeploymentName)
1002+
}, 2*time.Minute, 2*time.Second).Should(Succeed())
1003+
1004+
By("verifying system is ready with certificates")
1005+
waitForCondition(certManagerTestNamespace, jumpstarterName,
1006+
operatorv1alpha1.ConditionTypeReady, metav1.ConditionTrue, 3*time.Minute)
1007+
})
1008+
9431009
AfterAll(func() {
9441010
DeleteTestNamespace(certManagerTestNamespace)
9451011
})
@@ -1368,15 +1434,14 @@ func verifyTLSSecret(namespace, name string) {
13681434
}
13691435

13701436
// verifyDeploymentHasTLSMount checks that a deployment has the TLS volume mount and env vars.
1371-
func verifyDeploymentHasTLSMount(namespace, name string) {
1372-
By(fmt.Sprintf("verifying deployment %s has TLS mount", name))
1373-
1437+
// This is used with Gomega assertions to verify the deployment has been reconciled with TLS.
1438+
func verifyDeploymentHasTLSMount(g Gomega, namespace, name string) {
13741439
deployment := &appsv1.Deployment{}
13751440
err := k8sClient.Get(ctx, types.NamespacedName{
13761441
Name: name,
13771442
Namespace: namespace,
13781443
}, deployment)
1379-
Expect(err).NotTo(HaveOccurred())
1444+
g.Expect(err).NotTo(HaveOccurred())
13801445

13811446
// Check for tls-certs volume
13821447
hasVolume := false
@@ -1386,10 +1451,10 @@ func verifyDeploymentHasTLSMount(namespace, name string) {
13861451
break
13871452
}
13881453
}
1389-
Expect(hasVolume).To(BeTrue(), fmt.Sprintf("deployment %s missing tls-certs volume", name))
1454+
g.Expect(hasVolume).To(BeTrue(), fmt.Sprintf("deployment %s missing tls-certs volume", name))
13901455

13911456
// Check for volume mount in the first container
1392-
Expect(deployment.Spec.Template.Spec.Containers).NotTo(BeEmpty())
1457+
g.Expect(deployment.Spec.Template.Spec.Containers).NotTo(BeEmpty())
13931458
container := deployment.Spec.Template.Spec.Containers[0]
13941459

13951460
hasMount := false
@@ -1399,7 +1464,7 @@ func verifyDeploymentHasTLSMount(namespace, name string) {
13991464
break
14001465
}
14011466
}
1402-
Expect(hasMount).To(BeTrue(), fmt.Sprintf("deployment %s missing /tls volume mount", name))
1467+
g.Expect(hasMount).To(BeTrue(), fmt.Sprintf("deployment %s missing /tls volume mount", name))
14031468

14041469
// Check for EXTERNAL_CERT_PEM and EXTERNAL_KEY_PEM env vars
14051470
hasCertEnv := false
@@ -1412,8 +1477,45 @@ func verifyDeploymentHasTLSMount(namespace, name string) {
14121477
hasKeyEnv = true
14131478
}
14141479
}
1415-
Expect(hasCertEnv).To(BeTrue(), fmt.Sprintf("deployment %s missing EXTERNAL_CERT_PEM env var", name))
1416-
Expect(hasKeyEnv).To(BeTrue(), fmt.Sprintf("deployment %s missing EXTERNAL_KEY_PEM env var", name))
1480+
g.Expect(hasCertEnv).To(BeTrue(), fmt.Sprintf("deployment %s missing EXTERNAL_CERT_PEM env var", name))
1481+
g.Expect(hasKeyEnv).To(BeTrue(), fmt.Sprintf("deployment %s missing EXTERNAL_KEY_PEM env var", name))
1482+
}
1483+
1484+
// verifyDeploymentHasNoTLSMount checks that a deployment does NOT have TLS configuration.
1485+
// This is used with Gomega assertions to verify the deployment has been reconciled without TLS.
1486+
func verifyDeploymentHasNoTLSMount(g Gomega, namespace, name string) {
1487+
deployment := &appsv1.Deployment{}
1488+
err := k8sClient.Get(ctx, types.NamespacedName{
1489+
Name: name,
1490+
Namespace: namespace,
1491+
}, deployment)
1492+
g.Expect(err).NotTo(HaveOccurred())
1493+
1494+
// Check that tls-certs volume is NOT present
1495+
for _, vol := range deployment.Spec.Template.Spec.Volumes {
1496+
g.Expect(vol.Name).NotTo(Equal("tls-certs"),
1497+
fmt.Sprintf("deployment %s should not have tls-certs volume", name))
1498+
}
1499+
1500+
// Check for volume mount in the first container
1501+
g.Expect(deployment.Spec.Template.Spec.Containers).NotTo(BeEmpty())
1502+
container := deployment.Spec.Template.Spec.Containers[0]
1503+
1504+
// Check that /tls volume mount is NOT present
1505+
for _, mount := range container.VolumeMounts {
1506+
if mount.Name == "tls-certs" {
1507+
g.Expect(mount.MountPath).NotTo(Equal("/tls"),
1508+
fmt.Sprintf("deployment %s should not have /tls volume mount", name))
1509+
}
1510+
}
1511+
1512+
// Check that EXTERNAL_CERT_PEM and EXTERNAL_KEY_PEM env vars are NOT present
1513+
for _, env := range container.Env {
1514+
g.Expect(env.Name).NotTo(Equal("EXTERNAL_CERT_PEM"),
1515+
fmt.Sprintf("deployment %s should not have EXTERNAL_CERT_PEM env var", name))
1516+
g.Expect(env.Name).NotTo(Equal("EXTERNAL_KEY_PEM"),
1517+
fmt.Sprintf("deployment %s should not have EXTERNAL_KEY_PEM env var", name))
1518+
}
14171519
}
14181520

14191521
// dumpCertManagerResourcesOnFailure dumps cert-manager and Jumpstarter resources for debugging test failures.

0 commit comments

Comments
 (0)