This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 4e4527a699e8f95de176eade5d18a3f2a781819f Author: Pranjul Kalsi <[email protected]> AuthorDate: Sun Dec 14 20:04:01 2025 +0530 refactor(jvm): move CA cert volumes and init container to mount/init-containers traits --- docs/modules/ROOT/partials/apis/camel-k-crds.adoc | 16 +++ docs/modules/traits/pages/jvm.adoc | 6 +- pkg/apis/camel/v1/trait/zz_generated.deepcopy.go | 2 +- pkg/apis/camel/v1/zz_generated.deepcopy.go | 1 + .../duck/keda/v1alpha1/zz_generated.deepcopy.go | 2 +- pkg/trait/init_containers.go | 21 ++++ pkg/trait/init_containers_test.go | 68 ++++++++++++ pkg/trait/jvm.go | 121 +-------------------- pkg/trait/jvm_cacert.go | 70 ++++++++++++ pkg/trait/jvm_test.go | 7 +- pkg/trait/mount.go | 51 +++++++++ pkg/trait/mount_test.go | 64 +++++++++++ 12 files changed, 303 insertions(+), 126 deletions(-) diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc index da56e3365..ef2d79834 100644 --- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc +++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc @@ -7796,6 +7796,22 @@ The Jar dependency which will run the application. Leave it empty for managed In A list of JVM agents to download and execute with format `<agent-name>;<agent-url>[;<jvm-agent-options>]`. +|`caCert` + +string +| + + +The secret should contain PEM-encoded certificates. +Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + +|`caCertMountPath` + +string +| + + +The path where the generated truststore will be mounted +Default: "/etc/camel/conf.d/_truststore" + |=== diff --git a/docs/modules/traits/pages/jvm.adoc b/docs/modules/traits/pages/jvm.adoc index fd82ed728..f894ee970 100755 --- a/docs/modules/traits/pages/jvm.adoc +++ b/docs/modules/traits/pages/jvm.adoc @@ -64,11 +64,13 @@ Deprecated: no longer in use. | jvm.ca-cert | string -| 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` +| The secret should contain PEM-encoded certificates. +Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" | jvm.ca-cert-mount-path | string -| The path where the generated truststore will be mounted. Default: `/etc/camel/conf.d/_truststore` +| The path where the generated truststore will be mounted +Default: "/etc/camel/conf.d/_truststore" |=== diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go index 9fe9ae247..10bb5627b 100644 --- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package trait import ( - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/util/intstr" ) diff --git a/pkg/apis/camel/v1/zz_generated.deepcopy.go b/pkg/apis/camel/v1/zz_generated.deepcopy.go index 9b1a09711..caf14921f 100644 --- a/pkg/apis/camel/v1/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/zz_generated.deepcopy.go @@ -6,6 +6,7 @@ package v1 import ( "encoding/json" + "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/apis/duck/keda/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/duck/keda/v1alpha1/zz_generated.deepcopy.go index 27d57d72a..450aba305 100644 --- a/pkg/apis/duck/keda/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/duck/keda/v1alpha1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha1 import ( - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/trait/init_containers.go b/pkg/trait/init_containers.go index 87a49b488..4ae0ad9ba 100644 --- a/pkg/trait/init_containers.go +++ b/pkg/trait/init_containers.go @@ -90,6 +90,27 @@ func (t *initContainersTrait) Configure(e *Environment) (bool, *TraitCondition, } t.tasks = append(t.tasks, agentDownloadTask) } + // Set the CA cert truststore init container if configured + if ok && jvm.hasCACert() { + _, secretKey, err := parseSecretRef(jvm.CACert) + if err != nil { + return false, nil, err + } + if secretKey == "" { + secretKey = "ca.crt" + } + + keytoolCmd := fmt.Sprintf( + "keytool -importcert -noprompt -alias custom-ca -storepass %s -keystore %s -file /etc/secrets/cacert/%s", + getTrustStorePassword(e.Integration.Name), jvm.getTrustStorePath(), secretKey, + ) + caCertTask := containerTask{ + name: "generate-truststore", + image: defaults.BaseImage(), + command: keytoolCmd, + } + t.tasks = append(t.tasks, caCertTask) + } } return len(t.tasks) > 0, nil, nil diff --git a/pkg/trait/init_containers_test.go b/pkg/trait/init_containers_test.go index 3b7212b71..695430027 100644 --- a/pkg/trait/init_containers_test.go +++ b/pkg/trait/init_containers_test.go @@ -363,3 +363,71 @@ func TestApplyInitContainerWithAgents(t *testing.T) { deploy.Spec.Template.Spec.InitContainers[0].Command) assert.NotEqual(t, ptr.To(corev1.ContainerRestartPolicyAlways), deploy.Spec.Template.Spec.InitContainers[0].RestartPolicy) } + +func TestApplyInitContainerWithCACert(t *testing.T) { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-it", + Labels: map[string]string{ + v1.IntegrationLabel: "my-it", + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{}, + }, + }, + } + catalog, _ := camel.DefaultCatalog() + traitCatalog := NewCatalog(nil) + fakeClient, _ := internal.NewFakeClient(deployment) + environment := Environment{ + Client: fakeClient, + CamelCatalog: catalog, + Catalog: traitCatalog, + Resources: kubernetes.NewCollection(), + Integration: &v1.Integration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-it", + }, + Spec: v1.IntegrationSpec{ + Traits: v1.Traits{ + JVM: &trait.JVMTrait{ + CACert: "secret:my-ca-secret", + }, + }, + }, + Status: v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseRunning, + }, + }, + Platform: &v1.IntegrationPlatform{ + Spec: v1.IntegrationPlatformSpec{ + Cluster: v1.IntegrationPlatformClusterOpenShift, + Build: v1.IntegrationPlatformBuildSpec{ + PublishStrategy: v1.IntegrationPlatformBuildPublishStrategyJib, + Registry: v1.RegistrySpec{Address: "registry"}, + RuntimeVersion: catalog.Runtime.Version, + }, + }, + Status: v1.IntegrationPlatformStatus{ + Phase: v1.IntegrationPlatformPhaseReady, + }, + }, + } + environment.Resources.Add(deployment) + environment.Platform.ResyncStatusFullConfig() + _, _, err := traitCatalog.apply(&environment) + + require.NoError(t, err) + + deploy := environment.Resources.GetDeploymentForIntegration(environment.Integration) + require.NotNil(t, deploy) + + require.Len(t, deploy.Spec.Template.Spec.InitContainers, 1) + initContainer := deploy.Spec.Template.Spec.InitContainers[0] + assert.Equal(t, "generate-truststore", initContainer.Name) + assert.Equal(t, defaults.BaseImage(), initContainer.Image) + + assert.Contains(t, initContainer.Command[0], "keytool") +} diff --git a/pkg/trait/jvm.go b/pkg/trait/jvm.go index 9923b379b..efe76877e 100644 --- a/pkg/trait/jvm.go +++ b/pkg/trait/jvm.go @@ -18,14 +18,12 @@ limitations under the License. package trait import ( - "errors" "fmt" "net/url" "path/filepath" "sort" "strings" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,10 +47,6 @@ const ( defaultMaxMemoryPercentage = int64(50) lowMemoryThreshold = 300 lowMemoryMAxMemoryDefaultPercentage = int64(25) - defaultCACertMountPath = "/etc/camel/conf.d/_truststore" - caCertVolumeName = "jvm-truststore" - caCertSecretVolumeName = "ca-cert-secret" //nolint:gosec // G101: not a credential, just a volume name - trustStoreName = "truststore.jks" ) type jvmTrait struct { @@ -384,126 +378,19 @@ func getLegacyCamelQuarkusDependenciesPaths() *sets.Set { return s } -// parseSecretRef parses a secret reference in the format "secret:name" or "secret:name/key". -func parseSecretRef(ref string) (string, string, error) { - if !strings.HasPrefix(ref, "secret:") { - return "", "", fmt.Errorf("invalid CA cert reference %q: must start with 'secret:'", ref) - } - - ref = strings.TrimPrefix(ref, "secret:") - parts := strings.SplitN(ref, "/", 2) - secretName, secretKey := parts[0], "" - - if len(parts) > 1 { - secretKey = parts[1] - } - if secretName == "" { - return "", "", errors.New("invalid CA cert reference: secret name is empty") - } - - return secretName, secretKey, nil -} - -// configureCACert sets up the truststore for CA certificates. +// configureCACert returns the JVM arguments for truststore configuration. func (t *jvmTrait) configureCaCert(e *Environment) ([]string, error) { if t.CACert == "" { return nil, nil } - secretName, secretKey, err := parseSecretRef(t.CACert) + _, _, err := parseSecretRef(t.CACert) if err != nil { return nil, err } - if secretKey == "" { - secretKey = "ca.crt" - } - - mountPath := defaultCACertMountPath - if t.CACertMountPath != "" { - mountPath = t.CACertMountPath - } - - // Use a deterministic password based on integration name to avoid - // changing the deployment spec on every reconciliation cycle. - // For a truststore i.e public CA certs only, security of this password is not critical. - trustStorePass := "camelk-" + e.Integration.Name - trustStorePath := filepath.Join(mountPath, trustStoreName) - - // add secret volume. - secretVolume := corev1.Volume{ - Name: caCertSecretVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - }, - }, - } - - // add an emptyDir volume. - trustStoreVolume := corev1.Volume{ - Name: caCertVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - } - - // add volumes to deployment. - e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) { - deployment.Spec.Template.Spec.Volumes = append( - deployment.Spec.Template.Spec.Volumes, - secretVolume, trustStoreVolume, - ) - }) - - // add mount to integration container - container := e.GetIntegrationContainer() - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: caCertVolumeName, - MountPath: mountPath, - ReadOnly: true, - }) - - initContainer := corev1.Container{ - Name: "generate-truststore", - Image: container.Image, - ImagePullPolicy: container.ImagePullPolicy, - Command: []string{ - "keytool", - "-importcert", - "-noprompt", - "-alias", - "custom-ca", - "-storepass", - trustStorePass, - "-keystore", - trustStorePath, - "-file", - filepath.Join("/etc/secrets/cacert", secretKey), - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: caCertSecretVolumeName, - MountPath: "/etc/secrets/cacert", - ReadOnly: true, - }, - { - Name: caCertVolumeName, - MountPath: mountPath, - }, - }, - } - - // add to deployment container - e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) { - deployment.Spec.Template.Spec.InitContainers = append( - deployment.Spec.Template.Spec.InitContainers, - initContainer, - ) - }) - return []string{ - "-Djavax.net.ssl.trustStore=" + trustStorePath, - "-Djavax.net.ssl.trustStorePassword=" + trustStorePass, + "-Djavax.net.ssl.trustStore=" + t.getTrustStorePath(), + "-Djavax.net.ssl.trustStorePassword=" + getTrustStorePassword(e.Integration.Name), }, nil } diff --git a/pkg/trait/jvm_cacert.go b/pkg/trait/jvm_cacert.go new file mode 100644 index 000000000..60b8ff273 --- /dev/null +++ b/pkg/trait/jvm_cacert.go @@ -0,0 +1,70 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package trait + +import ( + "errors" + "fmt" + "strings" +) + +const ( + defaultCACertMountPath = "/etc/camel/conf.d/_truststore" + caCertVolumeName = "jvm-truststore" + caCertSecretVolumeName = "ca-cert-secret" //nolint:gosec // G101: not a credential, just a volume name + trustStoreName = "truststore.jks" +) + +func (t *jvmTrait) hasCACert() bool { + return t.CACert != "" +} + +func (t *jvmTrait) getCACertMountPath() string { + if t.CACertMountPath != "" { + return t.CACertMountPath + } + + return defaultCACertMountPath +} + +// parseSecretRef parses a secret reference in the format "secret:name" or "secret:name/key". +func parseSecretRef(ref string) (string, string, error) { + if !strings.HasPrefix(ref, "secret:") { + return "", "", fmt.Errorf("invalid CA cert reference %q: must start with 'secret:'", ref) + } + + ref = strings.TrimPrefix(ref, "secret:") + parts := strings.SplitN(ref, "/", 2) + secretName, secretKey := parts[0], "" + + if len(parts) > 1 { + secretKey = parts[1] + } + if secretName == "" { + return "", "", errors.New("invalid CA cert reference: secret name is empty") + } + + return secretName, secretKey, nil +} + +func (t *jvmTrait) getTrustStorePath() string { + return t.getCACertMountPath() + "/" + trustStoreName +} + +func getTrustStorePassword(integrationName string) string { + return "camelk-" + integrationName +} diff --git a/pkg/trait/jvm_test.go b/pkg/trait/jvm_test.go index cb9c99730..76a750a80 100644 --- a/pkg/trait/jvm_test.go +++ b/pkg/trait/jvm_test.go @@ -747,12 +747,9 @@ func TestApplyJvmTraitWithCACert(t *testing.T) { err = trait.Apply(environment) require.NoError(t, err) + // JVM trait now only adds JVM args for truststore assert.Contains(t, d.Spec.Template.Spec.Containers[0].Args, "-Djavax.net.ssl.trustStore=/etc/camel/conf.d/_truststore/truststore.jks") - assert.Len(t, d.Spec.Template.Spec.Volumes, 2) - assert.Equal(t, "ca-cert-secret", d.Spec.Template.Spec.Volumes[0].Name) - assert.Equal(t, "jvm-truststore", d.Spec.Template.Spec.Volumes[1].Name) - assert.Len(t, d.Spec.Template.Spec.InitContainers, 1) - assert.Equal(t, "generate-truststore", d.Spec.Template.Spec.InitContainers[0].Name) + assert.Contains(t, d.Spec.Template.Spec.Containers[0].Args, "-Djavax.net.ssl.trustStorePassword=camelk-my-it") } func TestParseSecretRef(t *testing.T) { diff --git a/pkg/trait/mount.go b/pkg/trait/mount.go index a23825ff4..74a650f79 100644 --- a/pkg/trait/mount.go +++ b/pkg/trait/mount.go @@ -190,6 +190,7 @@ func (t *mountTrait) configureVolumesAndMounts( } // Mount the agent volume if any agent exists trait := e.Catalog.GetTrait(jvmTraitID) + //nolint:nestif if trait != nil { jvm, ok := trait.(*jvmTrait) if ok && jvm.hasJavaAgents() { @@ -203,6 +204,56 @@ func (t *mountTrait) configureVolumesAndMounts( (*icnts)[i].VolumeMounts = append((*icnts)[i].VolumeMounts, *volumeMount) } } + // Mount CA cert volumes if configured + if ok && jvm.hasCACert() { + secretName, _, err := parseSecretRef(jvm.CACert) + if err != nil { + return err + } + mountPath := jvm.getCACertMountPath() + + // Secret volume for CA cert + secretVolume := corev1.Volume{ + Name: caCertSecretVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + } + *vols = append(*vols, secretVolume) + + // EmptyDir volume for truststore output + trustStoreVolume := corev1.Volume{ + Name: caCertVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + *vols = append(*vols, trustStoreVolume) + + // Mount truststore to main container + *mnts = append(*mnts, corev1.VolumeMount{ + Name: caCertVolumeName, + MountPath: mountPath, + ReadOnly: true, + }) + + // Mount volumes to init containers for truststore generation + for i := range *icnts { + (*icnts)[i].VolumeMounts = append((*icnts)[i].VolumeMounts, + corev1.VolumeMount{ + Name: caCertSecretVolumeName, + MountPath: "/etc/secrets/cacert", + ReadOnly: true, + }, + corev1.VolumeMount{ + Name: caCertVolumeName, + MountPath: mountPath, + }, + ) + } + } } return nil diff --git a/pkg/trait/mount_test.go b/pkg/trait/mount_test.go index 5d05e4f0e..3698dbbb5 100644 --- a/pkg/trait/mount_test.go +++ b/pkg/trait/mount_test.go @@ -703,3 +703,67 @@ func TestMountMultipleKeysSameSecret(t *testing.T) { } assert.Equal(t, 1, found, "Expected exactly 1 volume mounted at my-secret-1") } + +func TestCACertVolume(t *testing.T) { + traitCatalog := NewCatalog(nil) + + environment := getNominalEnv(t, traitCatalog) + + environment.Integration.Spec.Traits.Mount = &traitv1.MountTrait{} + environment.Integration.Spec.Traits.JVM = &traitv1.JVMTrait{ + CACert: "secret:my-ca-secret", + } + environment.Platform.ResyncStatusFullConfig() + conditions, traits, err := traitCatalog.apply(environment) + + require.NoError(t, err) + assert.NotEmpty(t, traits) + assert.NotEmpty(t, conditions) + assert.NotEmpty(t, environment.ExecutedTraits) + assert.NotNil(t, environment.GetTrait("mount")) + + deployment := environment.Resources.GetDeployment(func(service *appsv1.Deployment) bool { + return service.Name == "hello" + }) + assert.NotNil(t, deployment) + spec := deployment.Spec.Template.Spec + + // Should have: 2 base volumes + secret volume + emptyDir volume = 4 + assert.Len(t, spec.Volumes, 4) + + // Check secret volume exists + var secretVolume *corev1.Volume + for _, v := range spec.Volumes { + if v.Name == caCertSecretVolumeName { + secretVolume = &v + break + } + } + assert.NotNil(t, secretVolume, "Expected secret volume for CA cert") + assert.NotNil(t, secretVolume.Secret) + assert.Equal(t, "my-ca-secret", secretVolume.Secret.SecretName) + + // Check emptyDir volume exists + var emptyDirVolume *corev1.Volume + for _, v := range spec.Volumes { + if v.Name == caCertVolumeName { + emptyDirVolume = &v + break + } + } + assert.NotNil(t, emptyDirVolume, "Expected emptyDir volume for truststore") + assert.NotNil(t, emptyDirVolume.EmptyDir) + + assert.Condition(t, func() bool { + for _, container := range spec.Containers { + if container.Name == "integration" { + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name == caCertVolumeName { + return volumeMount.MountPath == defaultCACertMountPath + } + } + } + } + return false + }) +}
