pkg/trait/jvm.go (255 lines of code) (raw):
/*
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 (
"fmt"
"net/url"
"path/filepath"
"sort"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
"github.com/apache/camel-k/v2/pkg/builder"
"github.com/apache/camel-k/v2/pkg/util"
"github.com/apache/camel-k/v2/pkg/util/camel"
"github.com/apache/camel-k/v2/pkg/util/envvar"
"github.com/apache/camel-k/v2/pkg/util/sets"
)
const (
jvmTraitID = "jvm"
jvmTraitOrder = 2000
defaultMaxMemoryScale = 6
defaultMaxMemoryPercentage = int64(50)
lowMemoryThreshold = 300
lowMemoryMAxMemoryDefaultPercentage = int64(25)
)
type jvmTrait struct {
BaseTrait
traitv1.JVMTrait `property:",squash"`
}
func newJvmTrait() Trait {
return &jvmTrait{
BaseTrait: NewBaseTrait(jvmTraitID, jvmTraitOrder),
}
}
func (t *jvmTrait) Configure(e *Environment) (bool, *TraitCondition, error) {
// Deprecated: the JVM has to be a platform trait and the user should not be able to disable it
if !ptr.Deref(t.Enabled, true) {
notice := userDisabledMessage + "; this configuration is deprecated and may be removed within next releases"
return false, NewIntegrationCondition("JVM", v1.IntegrationConditionTraitInfo, corev1.ConditionTrue, TraitConfigurationReason, notice), nil
}
if (e.IntegrationKit != nil && !e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady)) || !e.IntegrationInRunningPhases() {
return false, nil, nil
}
//nolint: staticcheck
if ((e.Integration != nil && !e.Integration.IsManagedBuild()) || (e.IntegrationKit != nil && e.IntegrationKit.IsSynthetic())) &&
t.Jar == "" {
// We skip this trait since we cannot make any assumption on the container Java tooling running
// for the synthetic IntegrationKit
return false, NewIntegrationConditionPlatformDisabledWithMessage(
"JVM",
"integration kit was not created via Camel K operator and the user did not provide the jar to execute",
), nil
}
// The JVM trait must be disabled in case the current IntegrationKit corresponds to a native build
if qt := e.Catalog.GetTrait(quarkusTraitID); qt != nil {
if quarkus, ok := qt.(*quarkusTrait); ok && quarkus.isNativeIntegration(e) {
return false, NewIntegrationConditionPlatformDisabledWithMessage("JVM", "quarkus native build"), nil
}
}
if t.Jar == "" && e.Integration.Status.Jar != "" {
t.Jar = e.Integration.Status.Jar
}
return true, nil, nil
}
func (t *jvmTrait) Apply(e *Environment) error {
container := e.GetIntegrationContainer()
if container == nil {
return fmt.Errorf("unable to find a container for %s Integration", e.Integration.Name)
}
// Build the container command
// Other traits may have already contributed some arguments
args := container.Args
if ptr.Deref(t.Debug, false) {
debugArgs := t.enableDebug(e)
args = append(args, debugArgs)
}
hasHeapSizeOption := false
// Add JVM options
if len(t.Options) > 0 {
hasHeapSizeOption = util.StringSliceContainsAnyOf(t.Options, "-Xmx", "-XX:MaxHeapSize", "-XX:MinRAMPercentage", "-XX:MaxRAMPercentage")
args = append(args, t.Options...)
}
// Tune JVM maximum heap size based on the container memory limit, if any.
// This is configured off-container, thus is limited to explicit user configuration.
// We may want to inject a wrapper script into the container image, so that it can
// be performed in-container, based on CGroups memory resource control files.
if memory, hasLimit := container.Resources.Limits[corev1.ResourceMemory]; !hasHeapSizeOption && hasLimit {
// Simple heuristic that caps the maximum heap size to 50% of the memory limit
percentage := defaultMaxMemoryPercentage
// Unless the memory limit is lower than 300M, in which case we leave more room for the non-heap memory
if resource.NewScaledQuantity(lowMemoryThreshold, defaultMaxMemoryScale).Cmp(memory) > 0 {
percentage = lowMemoryMAxMemoryDefaultPercentage
}
//nolint:mnd
memScaled := memory.ScaledValue(resource.Mega) * percentage / 100
args = append(args, fmt.Sprintf("-Xmx%dM", memScaled))
}
httpProxyArgs, err := t.prepareHTTPProxy(container)
if err != nil {
return err
}
if httpProxyArgs != nil {
args = append(args, httpProxyArgs...)
}
return t.feedContainer(container, args, e)
}
//nolint:nestif
func (t *jvmTrait) feedContainer(container *corev1.Container, args []string, e *Environment) error {
// If user provided the jar, we will execute on the container something like
// java -Dxyx ... -cp ... -jar my-app.jar
// For this reason it's important that the container is a java based container able to run a Camel (hence Java) application
container.WorkingDir = builder.DeploymentDir
container.Command = []string{"java"}
classpathItems := t.prepareClasspathItems(container)
if t.Jar != "" {
// User is providing the Jar to execute explicitly
args = append(args, "-cp", classpathItems)
args = append(args, "-jar", t.Jar)
} else {
kit, err := t.getIntegrationKit(e)
if err != nil {
return err
}
if kit != nil {
// managed Integrations
kitDepsDirs := kit.Status.GetDependenciesPaths()
if kitDepsDirs.IsEmpty() {
// Use legacy Camel Quarkus expected structure
kitDepsDirs = getLegacyCamelQuarkusDependenciesPaths()
}
classpathItems = getClasspath(kitDepsDirs, classpathItems)
}
args = append(args, "-cp", classpathItems)
args = append(args, e.CamelCatalog.Runtime.ApplicationClass)
}
container.Args = args
return nil
}
// getClasspath merges the classpath required by the kit with any value provided in the trait.
func getClasspath(depsDirs *sets.Set, jvmTraitClasspath string) string {
if !depsDirs.IsEmpty() {
if jvmTraitClasspath != "" {
jvmTraitClasspathSet := getClasspathSet(jvmTraitClasspath)
depsDirs = sets.Union(depsDirs, jvmTraitClasspathSet)
}
classPaths := depsDirs.List()
sort.Strings(classPaths)
return strings.Join(classPaths, ":")
}
return jvmTraitClasspath
}
func getClasspathSet(cps string) *sets.Set {
s := sets.NewSet()
for _, cp := range strings.Split(cps, ":") {
s.Add(cp)
}
return s
}
func (t *jvmTrait) getIntegrationKit(e *Environment) (*v1.IntegrationKit, error) {
kit := e.IntegrationKit
if kit == nil && e.Integration.Status.IntegrationKit != nil {
name := e.Integration.Status.IntegrationKit.Name
ns := e.Integration.GetIntegrationKitNamespace(e.Platform)
kit = v1.NewIntegrationKit(ns, name)
if err := t.Client.Get(e.Ctx, ctrl.ObjectKeyFromObject(kit), kit); err != nil {
return nil, fmt.Errorf("unable to find integration kit %s/%s: %w", ns, name, err)
}
}
return kit, nil
}
func (t *jvmTrait) enableDebug(e *Environment) string {
suspend := "n"
if ptr.Deref(t.DebugSuspend, false) {
suspend = "y"
}
// Add label to mark the pods with debug enabled
e.Resources.VisitPodTemplateMeta(func(meta *metav1.ObjectMeta) {
if meta.Labels == nil {
meta.Labels = make(map[string]string)
}
meta.Labels["camel.apache.org/debug"] = trueString
})
t.DebugAddress = "*:5005"
return fmt.Sprintf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=%s,address=%s",
suspend, t.DebugAddress)
}
func (t *jvmTrait) prepareClasspathItems(container *corev1.Container) string {
existingClasspaths := extractExistingClasspathItems(container)
classpath := sets.NewSet()
// Deprecated: replaced by /etc/camel/resources.d/[_configmaps/_secrets] (camel.ResourcesConfigmapsMountPath/camel.ResourcesSecretsMountPath).
classpath.Add("./resources")
classpath.Add(filepath.ToSlash(camel.ResourcesConfigmapsMountPath))
classpath.Add(filepath.ToSlash(camel.ResourcesSecretsMountPath))
// Deprecated: replaced by /etc/camel/resources.d/[_configmaps/_secrets] (camel.ResourcesConfigmapsMountPath/camel.ResourcesSecretsMountPath).
//nolint: staticcheck
classpath.Add(filepath.ToSlash(camel.ResourcesDefaultMountPath))
if t.Classpath != "" {
classpath.Add(strings.Split(t.Classpath, ":")...)
}
// Add mounted resources to the class path
for _, m := range container.VolumeMounts {
classpath.Add(m.MountPath)
}
items := classpath.List()
// Keep class path sorted so that it's consistent over reconciliation cycles
sort.Strings(items)
if existingClasspaths != nil {
existingClasspaths = append(existingClasspaths, items...)
return strings.Join(existingClasspaths, ":")
}
return strings.Join(items, ":")
}
// extractExistingClasspathItems returns any container classpath option (if exists).
func extractExistingClasspathItems(container *corev1.Container) []string {
for i, arg := range container.Args {
if arg == "-cp" || arg == "-classpath" {
if i < len(container.Args) {
// return the next argument
return strings.Split(container.Args[i+1], ":")
}
}
}
return nil
}
// Translate HTTP proxy environment variables, that are set by the environment trait,
// into corresponding JVM system properties.
func (t *jvmTrait) prepareHTTPProxy(container *corev1.Container) ([]string, error) {
var args []string
//nolint:dupl,nestif
if HTTPProxy := envvar.Get(container.Env, "HTTP_PROXY"); HTTPProxy != nil {
u, err := url.Parse(HTTPProxy.Value)
if err != nil {
return args, err
}
if !util.StringSliceContainsAnyOf(t.Options, "http.proxyHost") {
args = append(args, fmt.Sprintf("-Dhttp.proxyHost=%q", u.Hostname()))
}
if port := u.Port(); !util.StringSliceContainsAnyOf(t.Options, "http.proxyPort") && port != "" {
args = append(args, fmt.Sprintf("-Dhttp.proxyPort=%q", u.Port()))
}
if user := u.User; !util.StringSliceContainsAnyOf(t.Options, "http.proxyUser") && user != nil {
args = append(args, fmt.Sprintf("-Dhttp.proxyUser=%q", user.Username()))
if password, ok := user.Password(); !util.StringSliceContainsAnyOf(t.Options, "http.proxyUser") && ok {
args = append(args, fmt.Sprintf("-Dhttp.proxyPassword=%q", password))
}
}
}
//nolint:dupl,nestif
if HTTPSProxy := envvar.Get(container.Env, "HTTPS_PROXY"); HTTPSProxy != nil {
u, err := url.Parse(HTTPSProxy.Value)
if err != nil {
return args, err
}
if !util.StringSliceContainsAnyOf(t.Options, "https.proxyHost") {
args = append(args, fmt.Sprintf("-Dhttps.proxyHost=%q", u.Hostname()))
}
if port := u.Port(); !util.StringSliceContainsAnyOf(t.Options, "https.proxyPort") && port != "" {
args = append(args, fmt.Sprintf("-Dhttps.proxyPort=%q", u.Port()))
}
if user := u.User; !util.StringSliceContainsAnyOf(t.Options, "https.proxyUser") && user != nil {
args = append(args, fmt.Sprintf("-Dhttps.proxyUser=%q", user.Username()))
if password, ok := user.Password(); !util.StringSliceContainsAnyOf(t.Options, "https.proxyUser") && ok {
args = append(args, fmt.Sprintf("-Dhttps.proxyPassword=%q", password))
}
}
}
if noProxy := envvar.Get(container.Env, "NO_PROXY"); noProxy != nil {
if !util.StringSliceContainsAnyOf(t.Options, "http.nonProxyHosts") {
// Convert to the format expected by the JVM http.nonProxyHosts system property
hosts := strings.Split(strings.ReplaceAll(noProxy.Value, " ", ""), ",")
for i, host := range hosts {
if strings.HasPrefix(host, ".") {
hosts[i] = strings.Replace(host, ".", "*.", 1)
}
}
args = append(args, fmt.Sprintf("-Dhttp.nonProxyHosts=%q", strings.Join(hosts, "|")))
}
}
return args, nil
}
// Deprecated: to be removed as soon as version 2.3.x is no longer supported.
func getLegacyCamelQuarkusDependenciesPaths() *sets.Set {
s := sets.NewSet()
s.Add("dependencies/*")
s.Add("dependencies/lib/boot/*")
s.Add("dependencies/lib/main/*")
s.Add("dependencies/quarkus/*")
return s
}