Skip to content

Commit 37b0145

Browse files
committed
refactor(jvm): move CA cert volumes and init container to mount/init-containers traits
1 parent c9e074f commit 37b0145

File tree

12 files changed

+303
-126
lines changed

12 files changed

+303
-126
lines changed

docs/modules/ROOT/partials/apis/camel-k-crds.adoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7678,6 +7678,22 @@ The Jar dependency which will run the application. Leave it empty for managed In
76787678
76797679
A list of JVM agents to download and execute with format `<agent-name>;<agent-url>[;<jvm-agent-options>]`.
76807680
7681+
|`caCert` +
7682+
string
7683+
|
7684+
7685+
7686+
The secret should contain PEM-encoded certificates.
7687+
Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt"
7688+
7689+
|`caCertMountPath` +
7690+
string
7691+
|
7692+
7693+
7694+
The path where the generated truststore will be mounted
7695+
Default: "/etc/camel/conf.d/_truststore"
7696+
76817697
76827698
|===
76837699

docs/modules/traits/pages/jvm.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ Deprecated: no longer in use.
6464

6565
| jvm.ca-cert
6666
| string
67-
| A reference to a Secret containing CA certificate(s) to be trusted by the JVM. The secret should contain PEM-encoded certificates. Example: `secret:my-ca-certs` or `secret:my-ca-certs/custom-ca.crt`
67+
| The secret should contain PEM-encoded certificates.
68+
Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt"
6869

6970
| jvm.ca-cert-mount-path
7071
| string
71-
| The path where the generated truststore will be mounted. Default: `/etc/camel/conf.d/_truststore`
72+
| The path where the generated truststore will be mounted
73+
Default: "/etc/camel/conf.d/_truststore"
7274

7375
|===
7476

pkg/apis/camel/v1/trait/zz_generated.deepcopy.go

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

pkg/apis/camel/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/duck/keda/v1alpha1/zz_generated.deepcopy.go

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

pkg/trait/init_containers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ func (t *initContainersTrait) Configure(e *Environment) (bool, *TraitCondition,
9090
}
9191
t.tasks = append(t.tasks, agentDownloadTask)
9292
}
93+
// Set the CA cert truststore init container if configured
94+
if ok && jvm.hasCACert() {
95+
_, secretKey, err := parseSecretRef(jvm.CACert)
96+
if err != nil {
97+
return false, nil, err
98+
}
99+
if secretKey == "" {
100+
secretKey = "ca.crt"
101+
}
102+
103+
keytoolCmd := fmt.Sprintf(
104+
"keytool -importcert -noprompt -alias custom-ca -storepass %s -keystore %s -file /etc/secrets/cacert/%s",
105+
getTrustStorePassword(e.Integration.Name), jvm.getTrustStorePath(), secretKey,
106+
)
107+
caCertTask := containerTask{
108+
name: "generate-truststore",
109+
image: defaults.BaseImage(),
110+
command: keytoolCmd,
111+
}
112+
t.tasks = append(t.tasks, caCertTask)
113+
}
93114
}
94115

95116
return len(t.tasks) > 0, nil, nil

pkg/trait/init_containers_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,71 @@ func TestApplyInitContainerWithAgents(t *testing.T) {
363363
deploy.Spec.Template.Spec.InitContainers[0].Command)
364364
assert.NotEqual(t, ptr.To(corev1.ContainerRestartPolicyAlways), deploy.Spec.Template.Spec.InitContainers[0].RestartPolicy)
365365
}
366+
367+
func TestApplyInitContainerWithCACert(t *testing.T) {
368+
deployment := &appsv1.Deployment{
369+
ObjectMeta: metav1.ObjectMeta{
370+
Name: "my-it",
371+
Labels: map[string]string{
372+
v1.IntegrationLabel: "my-it",
373+
},
374+
},
375+
Spec: appsv1.DeploymentSpec{
376+
Template: corev1.PodTemplateSpec{
377+
Spec: corev1.PodSpec{},
378+
},
379+
},
380+
}
381+
catalog, _ := camel.DefaultCatalog()
382+
traitCatalog := NewCatalog(nil)
383+
fakeClient, _ := internal.NewFakeClient(deployment)
384+
environment := Environment{
385+
Client: fakeClient,
386+
CamelCatalog: catalog,
387+
Catalog: traitCatalog,
388+
Resources: kubernetes.NewCollection(),
389+
Integration: &v1.Integration{
390+
ObjectMeta: metav1.ObjectMeta{
391+
Name: "my-it",
392+
},
393+
Spec: v1.IntegrationSpec{
394+
Traits: v1.Traits{
395+
JVM: &trait.JVMTrait{
396+
CACert: "secret:my-ca-secret",
397+
},
398+
},
399+
},
400+
Status: v1.IntegrationStatus{
401+
Phase: v1.IntegrationPhaseRunning,
402+
},
403+
},
404+
Platform: &v1.IntegrationPlatform{
405+
Spec: v1.IntegrationPlatformSpec{
406+
Cluster: v1.IntegrationPlatformClusterOpenShift,
407+
Build: v1.IntegrationPlatformBuildSpec{
408+
PublishStrategy: v1.IntegrationPlatformBuildPublishStrategyJib,
409+
Registry: v1.RegistrySpec{Address: "registry"},
410+
RuntimeVersion: catalog.Runtime.Version,
411+
},
412+
},
413+
Status: v1.IntegrationPlatformStatus{
414+
Phase: v1.IntegrationPlatformPhaseReady,
415+
},
416+
},
417+
}
418+
environment.Resources.Add(deployment)
419+
environment.Platform.ResyncStatusFullConfig()
420+
_, _, err := traitCatalog.apply(&environment)
421+
422+
require.NoError(t, err)
423+
424+
deploy := environment.Resources.GetDeploymentForIntegration(environment.Integration)
425+
require.NotNil(t, deploy)
426+
427+
require.Len(t, deploy.Spec.Template.Spec.InitContainers, 1)
428+
initContainer := deploy.Spec.Template.Spec.InitContainers[0]
429+
assert.Equal(t, "generate-truststore", initContainer.Name)
430+
assert.Equal(t, defaults.BaseImage(), initContainer.Image)
431+
432+
assert.Contains(t, initContainer.Command[0], "keytool")
433+
}

pkg/trait/jvm.go

Lines changed: 4 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ limitations under the License.
1818
package trait
1919

2020
import (
21-
"errors"
2221
"fmt"
2322
"net/url"
2423
"path/filepath"
2524
"sort"
2625
"strings"
2726

28-
appsv1 "k8s.io/api/apps/v1"
2927
corev1 "k8s.io/api/core/v1"
3028
"k8s.io/apimachinery/pkg/api/resource"
3129
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -49,10 +47,6 @@ const (
4947
defaultMaxMemoryPercentage = int64(50)
5048
lowMemoryThreshold = 300
5149
lowMemoryMAxMemoryDefaultPercentage = int64(25)
52-
defaultCACertMountPath = "/etc/camel/conf.d/_truststore"
53-
caCertVolumeName = "jvm-truststore"
54-
caCertSecretVolumeName = "ca-cert-secret" //nolint:gosec // G101: not a credential, just a volume name
55-
trustStoreName = "truststore.jks"
5650
)
5751

5852
type jvmTrait struct {
@@ -383,126 +377,19 @@ func getLegacyCamelQuarkusDependenciesPaths() *sets.Set {
383377
return s
384378
}
385379

386-
// parseSecretRef parses a secret reference in the format "secret:name" or "secret:name/key".
387-
func parseSecretRef(ref string) (string, string, error) {
388-
if !strings.HasPrefix(ref, "secret:") {
389-
return "", "", fmt.Errorf("invalid CA cert reference %q: must start with 'secret:'", ref)
390-
}
391-
392-
ref = strings.TrimPrefix(ref, "secret:")
393-
parts := strings.SplitN(ref, "/", 2)
394-
secretName, secretKey := parts[0], ""
395-
396-
if len(parts) > 1 {
397-
secretKey = parts[1]
398-
}
399-
if secretName == "" {
400-
return "", "", errors.New("invalid CA cert reference: secret name is empty")
401-
}
402-
403-
return secretName, secretKey, nil
404-
}
405-
406-
// configureCACert sets up the truststore for CA certificates.
380+
// configureCACert returns the JVM arguments for truststore configuration.
407381
func (t *jvmTrait) configureCaCert(e *Environment) ([]string, error) {
408382
if t.CACert == "" {
409383
return nil, nil
410384
}
411385

412-
secretName, secretKey, err := parseSecretRef(t.CACert)
386+
_, _, err := parseSecretRef(t.CACert)
413387
if err != nil {
414388
return nil, err
415389
}
416390

417-
if secretKey == "" {
418-
secretKey = "ca.crt"
419-
}
420-
421-
mountPath := defaultCACertMountPath
422-
if t.CACertMountPath != "" {
423-
mountPath = t.CACertMountPath
424-
}
425-
426-
// Use a deterministic password based on integration name to avoid
427-
// changing the deployment spec on every reconciliation cycle.
428-
// For a truststore i.e public CA certs only, security of this password is not critical.
429-
trustStorePass := "camelk-" + e.Integration.Name
430-
trustStorePath := filepath.Join(mountPath, trustStoreName)
431-
432-
// add secret volume.
433-
secretVolume := corev1.Volume{
434-
Name: caCertSecretVolumeName,
435-
VolumeSource: corev1.VolumeSource{
436-
Secret: &corev1.SecretVolumeSource{
437-
SecretName: secretName,
438-
},
439-
},
440-
}
441-
442-
// add an emptyDir volume.
443-
trustStoreVolume := corev1.Volume{
444-
Name: caCertVolumeName,
445-
VolumeSource: corev1.VolumeSource{
446-
EmptyDir: &corev1.EmptyDirVolumeSource{},
447-
},
448-
}
449-
450-
// add volumes to deployment.
451-
e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) {
452-
deployment.Spec.Template.Spec.Volumes = append(
453-
deployment.Spec.Template.Spec.Volumes,
454-
secretVolume, trustStoreVolume,
455-
)
456-
})
457-
458-
// add mount to integration container
459-
container := e.GetIntegrationContainer()
460-
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
461-
Name: caCertVolumeName,
462-
MountPath: mountPath,
463-
ReadOnly: true,
464-
})
465-
466-
initContainer := corev1.Container{
467-
Name: "generate-truststore",
468-
Image: container.Image,
469-
ImagePullPolicy: container.ImagePullPolicy,
470-
Command: []string{
471-
"keytool",
472-
"-importcert",
473-
"-noprompt",
474-
"-alias",
475-
"custom-ca",
476-
"-storepass",
477-
trustStorePass,
478-
"-keystore",
479-
trustStorePath,
480-
"-file",
481-
filepath.Join("/etc/secrets/cacert", secretKey),
482-
},
483-
VolumeMounts: []corev1.VolumeMount{
484-
{
485-
Name: caCertSecretVolumeName,
486-
MountPath: "/etc/secrets/cacert",
487-
ReadOnly: true,
488-
},
489-
{
490-
Name: caCertVolumeName,
491-
MountPath: mountPath,
492-
},
493-
},
494-
}
495-
496-
// add to deployment container
497-
e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) {
498-
deployment.Spec.Template.Spec.InitContainers = append(
499-
deployment.Spec.Template.Spec.InitContainers,
500-
initContainer,
501-
)
502-
})
503-
504391
return []string{
505-
"-Djavax.net.ssl.trustStore=" + trustStorePath,
506-
"-Djavax.net.ssl.trustStorePassword=" + trustStorePass,
392+
"-Djavax.net.ssl.trustStore=" + t.getTrustStorePath(),
393+
"-Djavax.net.ssl.trustStorePassword=" + getTrustStorePassword(e.Integration.Name),
507394
}, nil
508395
}

pkg/trait/jvm_cacert.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
package trait
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"strings"
23+
)
24+
25+
const (
26+
defaultCACertMountPath = "/etc/camel/conf.d/_truststore"
27+
caCertVolumeName = "jvm-truststore"
28+
caCertSecretVolumeName = "ca-cert-secret" //nolint:gosec // G101: not a credential, just a volume name
29+
trustStoreName = "truststore.jks"
30+
)
31+
32+
func (t *jvmTrait) hasCACert() bool {
33+
return t.CACert != ""
34+
}
35+
36+
func (t *jvmTrait) getCACertMountPath() string {
37+
if t.CACertMountPath != "" {
38+
return t.CACertMountPath
39+
}
40+
41+
return defaultCACertMountPath
42+
}
43+
44+
// parseSecretRef parses a secret reference in the format "secret:name" or "secret:name/key".
45+
func parseSecretRef(ref string) (string, string, error) {
46+
if !strings.HasPrefix(ref, "secret:") {
47+
return "", "", fmt.Errorf("invalid CA cert reference %q: must start with 'secret:'", ref)
48+
}
49+
50+
ref = strings.TrimPrefix(ref, "secret:")
51+
parts := strings.SplitN(ref, "/", 2)
52+
secretName, secretKey := parts[0], ""
53+
54+
if len(parts) > 1 {
55+
secretKey = parts[1]
56+
}
57+
if secretName == "" {
58+
return "", "", errors.New("invalid CA cert reference: secret name is empty")
59+
}
60+
61+
return secretName, secretKey, nil
62+
}
63+
64+
func (t *jvmTrait) getTrustStorePath() string {
65+
return t.getCACertMountPath() + "/" + trustStoreName
66+
}
67+
68+
func getTrustStorePassword(integrationName string) string {
69+
return "camelk-" + integrationName
70+
}

0 commit comments

Comments
 (0)