Repository: apache/camel-k
Pull request #1226 (2020-01-23 16:26:18)

JVM trait
This PR introduces the JVM trait, a merger of the Classpath and Debug traits, that is meant to control JVM related configuration.

**Release Note**
```release-note
feat(trait): Unified JVM trait
```



Patch:
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index fb4ee9d93..318f21aac 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -19,10 +19,8 @@
 ** xref:traits/affinity.adoc[Affinity]
 ** xref:traits/builder.adoc[Builder]
 ** xref:traits/camel.adoc[Camel]
-** xref:traits/classpath.adoc[Classpath]
 ** xref:traits/container.adoc[Container]
 ** xref:traits/cron.adoc[Cron]
-** xref:traits/debug.adoc[Debug]
 ** xref:traits/dependencies.adoc[Dependencies]
 ** xref:traits/deployer.adoc[Deployer]
 ** xref:traits/deployment.adoc[Deployment]
@@ -31,6 +29,7 @@
 ** xref:traits/ingress.adoc[Ingress]
 ** xref:traits/istio.adoc[Istio]
 ** xref:traits/jolokia.adoc[Jolokia]
+** xref:traits/jvm.adoc[Jvm]
 ** xref:traits/knative-service.adoc[Knative Service]
 ** xref:traits/knative.adoc[Knative]
 ** xref:traits/owner.adoc[Owner]
diff --git a/docs/modules/ROOT/pages/traits/classpath.adoc b/docs/modules/ROOT/pages/traits/classpath.adoc
deleted file mode 100755
index 9b5f516fc..000000000
--- a/docs/modules/ROOT/pages/traits/classpath.adoc
+++ /dev/null
@@ -1,31 +0,0 @@
-= Classpath Trait
-
-// Start of autogenerated code - DO NOT EDIT! (description)
-The Classpath trait is used internally to configure the classpath of the final integration.
-
-
-This trait is available in the following profiles: **Kubernetes, Knative, OpenShift**.
-
-WARNING: The classpath trait is a *platform trait*: disabling it may compromise the platform functionality.
-
-// End of autogenerated code - DO NOT EDIT! (description)
-// Start of autogenerated code - DO NOT EDIT! (configuration)
-== Configuration
-
-Trait properties can be specified when running any integration with the CLI:
-```
-kamel run --trait classpath.[key]=[value] integration.groovy
-```
-The following configuration options are available:
-
-[cols="2,1,5a"]
-|===
-|Property | Type | Description
-
-| classpath.enabled
-| bool
-| Can be used to enable or disable a trait. All traits share this common property.
-
-|===
-
-// End of autogenerated code - DO NOT EDIT! (configuration)
diff --git a/docs/modules/ROOT/pages/traits/jvm.adoc b/docs/modules/ROOT/pages/traits/jvm.adoc
new file mode 100755
index 000000000..8dbfad488
--- /dev/null
+++ b/docs/modules/ROOT/pages/traits/jvm.adoc
@@ -0,0 +1,47 @@
+= Jvm Trait
+
+// Start of autogenerated code - DO NOT EDIT! (description)
+The JVM trait is used to configure the JVM that runs the integration.
+
+
+This trait is available in the following profiles: **Kubernetes, Knative, OpenShift**.
+
+WARNING: The jvm trait is a *platform trait*: disabling it may compromise the platform functionality.
+
+// End of autogenerated code - DO NOT EDIT! (description)
+// Start of autogenerated code - DO NOT EDIT! (configuration)
+== Configuration
+
+Trait properties can be specified when running any integration with the CLI:
+```
+kamel run --trait jvm.[key]=[value] --trait jvm.[key2]=[value2] integration.groovy
+```
+The following configuration options are available:
+
+[cols="2,1,5a"]
+|===
+|Property | Type | Description
+
+| jvm.enabled
+| bool
+| Can be used to enable or disable a trait. All traits share this common property.
+
+| jvm.debug
+| bool
+| Activates remote debugging, so that a debugger can be attached to the JVM, e.g., using port-forwarding
+
+| jvm.debug-suspend
+| bool
+| Suspends the target JVM immediately before the main class is loaded
+
+| jvm.debug-address
+| string
+| Transport address at which to listen for the newly launched JVM
+
+| jvm.options
+| string
+| A comma-separated list of JVM options
+
+|===
+
+// End of autogenerated code - DO NOT EDIT! (configuration)
diff --git a/docs/modules/ROOT/pages/traits/traits.adoc b/docs/modules/ROOT/pages/traits/traits.adoc
index 44bc59e30..f0c4b2438 100644
--- a/docs/modules/ROOT/pages/traits/traits.adoc
+++ b/docs/modules/ROOT/pages/traits/traits.adoc
@@ -37,10 +37,8 @@ See the trait description pages for more information on a specific trait:
 * xref:traits/affinity.adoc[Affinity Trait]
 * xref:traits/builder.adoc[Builder Trait]
 * xref:traits/camel.adoc[Camel Trait]
-* xref:traits/classpath.adoc[Classpath Trait]
 * xref:traits/container.adoc[Container Trait]
 * xref:traits/cron.adoc[Cron Trait]
-* xref:traits/debug.adoc[Debug Trait]
 * xref:traits/dependencies.adoc[Dependencies Trait]
 * xref:traits/deployer.adoc[Deployer Trait]
 * xref:traits/deployment.adoc[Deployment Trait]
@@ -49,6 +47,7 @@ See the trait description pages for more information on a specific trait:
 * xref:traits/ingress.adoc[Ingress Trait]
 * xref:traits/istio.adoc[Istio Trait]
 * xref:traits/jolokia.adoc[Jolokia Trait]
+* xref:traits/jvm.adoc[Jvm Trait]
 * xref:traits/knative-service.adoc[Knative Service Trait]
 * xref:traits/knative.adoc[Knative Trait]
 * xref:traits/owner.adoc[Owner Trait]
diff --git a/go.mod b/go.mod
index ac7aa1e65..07f35d9db 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/stoewer/go-strcase v1.0.2
 	github.com/stretchr/testify v1.4.0
 	go.uber.org/multierr v1.1.0
+	gopkg.in/inf.v0 v0.9.1
 	gopkg.in/yaml.v2 v2.2.4
 	k8s.io/api v0.0.0
 	k8s.io/apimachinery v0.0.0
diff --git a/pkg/apis/camel/v1/integrationkit_types.go b/pkg/apis/camel/v1/integrationkit_types.go
index 5f3c046f4..e80cc89f1 100644
--- a/pkg/apis/camel/v1/integrationkit_types.go
+++ b/pkg/apis/camel/v1/integrationkit_types.go
@@ -79,8 +79,8 @@ type IntegrationKitPhase string
 type IntegrationKitConditionType string
 
 const (
-	// IntegrationKindKind --
-	IntegrationKindKind string = "IntegrationKit"
+	// IntegrationKitKind --
+	IntegrationKitKind string = "IntegrationKit"
 
 	// IntegrationKitTypePlatform --
 	IntegrationKitTypePlatform = "platform"
diff --git a/pkg/apis/camel/v1/integrationkit_types_support.go b/pkg/apis/camel/v1/integrationkit_types_support.go
index 9c7864996..df3a95df4 100644
--- a/pkg/apis/camel/v1/integrationkit_types_support.go
+++ b/pkg/apis/camel/v1/integrationkit_types_support.go
@@ -27,7 +27,7 @@ func NewIntegrationKit(namespace string, name string) IntegrationKit {
 	return IntegrationKit{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: SchemeGroupVersion.String(),
-			Kind:       IntegrationKindKind,
+			Kind:       IntegrationKitKind,
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
@@ -41,7 +41,7 @@ func NewIntegrationKitList() IntegrationKitList {
 	return IntegrationKitList{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: SchemeGroupVersion.String(),
-			Kind:       IntegrationKindKind,
+			Kind:       IntegrationKitKind,
 		},
 	}
 }
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index a232039e5..6413cd4a3 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -50,6 +50,11 @@ func NewKamelCommand(ctx context.Context) (*cobra.Command, error) {
 	var err error
 	cmd := kamelPreAddCommandInit(&options)
 	addKamelSubcommands(cmd, &options)
+
+	if err := addHelpSubCommands(cmd, &options); err != nil {
+		return cmd, err
+	}
+
 	err = kamelPostAddCommandInit(cmd)
 
 	return cmd, err
@@ -117,6 +122,26 @@ func addKamelSubcommands(cmd *cobra.Command, options *RootCmdOptions) {
 	cmd.AddCommand(cmdOnly(newCmdBuilder(options)))
 }
 
+func addHelpSubCommands(cmd *cobra.Command, options *RootCmdOptions) error {
+	cmd.InitDefaultHelpCmd()
+
+	var helpCmd *cobra.Command
+	for _, c := range cmd.Commands() {
+		if c.Name() == "help" {
+			helpCmd = c
+			break
+		}
+	}
+
+	if helpCmd == nil {
+		return errors.New("could not find any configured help command")
+	}
+
+	helpCmd.AddCommand(cmdOnly(newTraitHelpCmd(options)))
+
+	return nil
+}
+
 func (command *RootCmdOptions) preRun(cmd *cobra.Command, _ []string) error {
 	if command.Namespace == "" {
 		var current string
diff --git a/pkg/cmd/trait_help.go b/pkg/cmd/trait_help.go
new file mode 100644
index 000000000..170abef55
--- /dev/null
+++ b/pkg/cmd/trait_help.go
@@ -0,0 +1,219 @@
+/*
+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 cmd
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+
+	"github.com/apache/camel-k/pkg/util/indentedwriter"
+
+	"github.com/fatih/structs"
+	"gopkg.in/yaml.v2"
+
+	"github.com/apache/camel-k/pkg/trait"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/spf13/cobra"
+)
+
+func newTraitHelpCmd(rootCmdOptions *RootCmdOptions) (*cobra.Command, *traitHelpCommandOptions) {
+	options := traitHelpCommandOptions{
+		RootCmdOptions: rootCmdOptions,
+	}
+
+	cmd := cobra.Command{
+		Use:     "trait",
+		Short:   "Trait help information",
+		Long:    `Displays help information for traits in a specified output format.`,
+		PreRunE: decode(&options),
+		RunE: func(_ *cobra.Command, args []string) error {
+			if err := options.validate(args); err != nil {
+				return err
+			}
+			return options.run(args)
+		},
+	}
+
+	cmd.Flags().Bool("all", false, "Include all traits")
+	cmd.Flags().StringP("output", "o", "", "Output format. One of json, yaml")
+
+	return &cmd, &options
+}
+
+type traitHelpCommandOptions struct {
+	*RootCmdOptions
+	IncludeAll   bool   `mapstructure:"all"`
+	OutputFormat string `mapstructure:"output"`
+}
+
+type traitDescription struct {
+	Name       trait.ID                   `json:"name" yaml:"name"`
+	Platform   bool                       `json:"platform" yaml:"platform"`
+	Profiles   []string                   `json:"profiles" yaml:"profiles"`
+	Properties []traitPropertyDescription `json:"properties" yaml:"properties"`
+}
+
+type traitPropertyDescription struct {
+	Name         string      `json:"name" yaml:"name"`
+	TypeName     string      `json:"type" yaml:"type"`
+	DefaultValue interface{} `json:"defaultValue,omitempty" yaml:"defaultValue,omitempty"`
+}
+
+func (command *traitHelpCommandOptions) validate(args []string) error {
+	if command.IncludeAll && len(args) > 0 {
+		return errors.New("invalid combination: both all flag and a named trait is set")
+	}
+	if !command.IncludeAll && len(args) == 0 {
+		return errors.New("invalid combination: neither all flag nor a named trait is set")
+	}
+	return nil
+}
+
+func (command *traitHelpCommandOptions) run(args []string) error {
+	var traitDescriptions []*traitDescription
+	var catalog = trait.NewCatalog(command.Context, nil)
+
+	for _, tp := range v1.AllTraitProfiles {
+		traits := catalog.TraitsForProfile(tp)
+		for _, t := range traits {
+			if len(args) == 1 && trait.ID(args[0]) != t.ID() {
+				continue
+			}
+
+			td := findTraitDescription(t.ID(), traitDescriptions)
+			if td == nil {
+				td = &traitDescription{
+					Name:     t.ID(),
+					Platform: t.IsPlatformTrait(),
+					Profiles: make([]string, 0),
+				}
+				computeTraitProperties(structs.Fields(t), &td.Properties)
+				traitDescriptions = append(traitDescriptions, td)
+			}
+			td.addProfile(string(tp))
+		}
+	}
+
+	if len(args) == 1 && len(traitDescriptions) == 0 {
+		return fmt.Errorf("no trait named '%s' exists", args[0])
+	}
+
+	switch strings.ToUpper(command.OutputFormat) {
+	case "JSON":
+		res, err := json.Marshal(traitDescriptions)
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(res))
+	case "YAML":
+		res, err := yaml.Marshal(traitDescriptions)
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(res))
+	default:
+		fmt.Println(outputTraits(traitDescriptions))
+	}
+
+	return nil
+}
+
+func (td *traitDescription) addProfile(tp string) {
+	for _, p := range td.Profiles {
+		if p == tp {
+			return
+		}
+	}
+	td.Profiles = append(td.Profiles, tp)
+}
+
+func findTraitDescription(id trait.ID, traitDescriptions []*traitDescription) *traitDescription {
+	for _, td := range traitDescriptions {
+		if td.Name == id {
+			return td
+		}
+	}
+	return nil
+}
+
+func computeTraitProperties(fields []*structs.Field, properties *[]traitPropertyDescription) {
+	for _, f := range fields {
+		if f.IsEmbedded() && f.IsExported() && f.Kind() == reflect.Struct {
+			computeTraitProperties(f.Fields(), properties)
+		}
+
+		if !f.IsExported() || f.IsEmbedded() {
+			continue
+		}
+
+		property := f.Tag("property")
+		if property == "" {
+			continue
+		}
+
+		tp := traitPropertyDescription{}
+		tp.Name = property
+
+		switch f.Kind() {
+		case reflect.Ptr:
+			tp.TypeName = reflect.TypeOf(f.Value()).Elem().String()
+		case reflect.Slice:
+			tp.TypeName = fmt.Sprintf("slice:%s", reflect.TypeOf(f.Value()).Elem().String())
+		default:
+			tp.TypeName = f.Kind().String()
+		}
+
+		if f.IsZero() {
+			if tp.TypeName == "bool" {
+				tp.DefaultValue = false
+			} else {
+				tp.DefaultValue = nil
+			}
+		} else {
+			tp.DefaultValue = f.Value()
+		}
+
+		*properties = append(*properties, tp)
+	}
+}
+
+func outputTraits(descriptions []*traitDescription) string {
+	return indentedwriter.IndentedString(func(out io.Writer) {
+		w := indentedwriter.NewWriter(out)
+
+		for _, td := range descriptions {
+			w.Write(0, "Name:\t%s\n", td.Name)
+			w.Write(0, "Profiles:\t%s\n", strings.Join(td.Profiles, ","))
+			w.Write(0, "Platform:\t%t\n", td.Platform)
+			w.Write(0, "Properties:\n")
+			for _, p := range td.Properties {
+				w.Write(1, "%s:\n", p.Name)
+				w.Write(2, "Type:\t%s\n", p.TypeName)
+				if p.DefaultValue != nil {
+					w.Write(2, "Default Value:\t%v\n", p.DefaultValue)
+				}
+			}
+			w.Writeln(0, "")
+		}
+	})
+}
diff --git a/pkg/install/common.go b/pkg/install/common.go
index 315c224ac..22fd6e743 100644
--- a/pkg/install/common.go
+++ b/pkg/install/common.go
@@ -95,7 +95,7 @@ func RuntimeObjectOrCollect(ctx context.Context, c client.Client, namespace stri
 			return nil
 		}
 		// Don't recreate integration kits, platforms, etc
-		if obj.GetObjectKind().GroupVersionKind().Kind == v1.IntegrationKindKind {
+		if obj.GetObjectKind().GroupVersionKind().Kind == v1.IntegrationKitKind {
 			return nil
 		}
 		if obj.GetObjectKind().GroupVersionKind().Kind == v1.IntegrationPlatformKind {
diff --git a/pkg/trait/container.go b/pkg/trait/container.go
index 02dd117d1..fb5d4c669 100644
--- a/pkg/trait/container.go
+++ b/pkg/trait/container.go
@@ -101,10 +101,9 @@ func (t *containerTrait) Configure(e *Environment) (bool, error) {
 
 func (t *containerTrait) Apply(e *Environment) error {
 	container := corev1.Container{
-		Name:    t.Name,
-		Image:   e.Integration.Status.Image,
-		Env:     make([]corev1.EnvVar, 0),
-		Command: []string{"java"},
+		Name:  t.Name,
+		Image: e.Integration.Status.Image,
+		Env:   make([]corev1.EnvVar, 0),
 	}
 
 	// combine Environment of integration with platform, kit, integration
diff --git a/pkg/trait/debug.go b/pkg/trait/debug.go
deleted file mode 100644
index 38d3c5fa4..000000000
--- a/pkg/trait/debug.go
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-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"
-
-	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
-)
-
-// The Debug trait can be used to enable debugging on the integration container,
-// so that a remote debugger can be attached.
-//
-// +camel-k:trait=debug
-type debugTrait struct {
-	BaseTrait `property:",squash"`
-}
-
-func newDebugTrait() *debugTrait {
-	return &debugTrait{
-		BaseTrait: newBaseTrait("debug"),
-	}
-}
-
-func (t *debugTrait) Configure(e *Environment) (bool, error) {
-	if t.Enabled != nil && *t.Enabled {
-		return e.IntegrationInPhase(v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning), nil
-	}
-
-	return false, nil
-}
-
-func (t *debugTrait) Apply(e *Environment) error {
-	container := e.getIntegrationContainer()
-	if container == nil {
-		return fmt.Errorf("unable to find integration container")
-	}
-
-	// TODO: Add options to configure debugging agent
-	container.Args = append(container.Args, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")
-
-	return nil
-}
diff --git a/pkg/trait/classpath.go b/pkg/trait/jvm.go
similarity index 55%
rename from pkg/trait/classpath.go
rename to pkg/trait/jvm.go
index 71b7cc460..89f3710d4 100644
--- a/pkg/trait/classpath.go
+++ b/pkg/trait/jvm.go
@@ -22,9 +22,14 @@ import (
 	"sort"
 	"strings"
 
+	"gopkg.in/inf.v0"
+
 	"github.com/pkg/errors"
 	"github.com/scylladb/go-set/strset"
 
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
@@ -35,20 +40,30 @@ const (
 	defaultMainClass = "org.apache.camel.k.main.Application"
 )
 
-// The Classpath trait is used internally to configure the classpath of the final integration.
+// The JVM trait is used to configure the JVM that runs the integration.
 //
-// +camel-k:trait=classpath
-type classpathTrait struct {
+// +camel-k:trait=jvm
+type jvmTrait struct {
 	BaseTrait `property:",squash"`
+	// Activates remote debugging, so that a debugger can be attached to the JVM, e.g., using port-forwarding
+	Debug bool `property:"debug"`
+	// Suspends the target JVM immediately before the main class is loaded
+	DebugSuspend bool `property:"debug-suspend"`
+	// Transport address at which to listen for the newly launched JVM
+	DebugAddress string `property:"debug-address"`
+	// A comma-separated list of JVM options
+	Options *string `property:"options"`
 }
 
-func newClasspathTrait() *classpathTrait {
-	return &classpathTrait{
-		BaseTrait: newBaseTrait("classpath"),
+func newJvmTrait() *jvmTrait {
+	return &jvmTrait{
+		BaseTrait: newBaseTrait("jvm"),
+		// To be defaulted to "*:5005" when upgrading the default base image to JDK9+
+		DebugAddress: "5005",
 	}
 }
 
-func (t *classpathTrait) Configure(e *Environment) (bool, error) {
+func (t *jvmTrait) Configure(e *Environment) (bool, error) {
 	if t.Enabled != nil && !*t.Enabled {
 		return false, nil
 	}
@@ -57,7 +72,7 @@ func (t *classpathTrait) Configure(e *Environment) (bool, error) {
 		e.InPhase(v1.IntegrationKitPhaseReady, v1.IntegrationPhaseRunning), nil
 }
 
-func (t *classpathTrait) Apply(e *Environment) error {
+func (t *jvmTrait) Apply(e *Environment) error {
 	kit := e.IntegrationKit
 
 	if kit == nil && e.Integration.Status.Kit != "" {
@@ -100,6 +115,47 @@ func (t *classpathTrait) Apply(e *Environment) error {
 
 	container := e.getIntegrationContainer()
 	if container != nil {
+		// Set the container command and working directory
+		container.Command = []string{"java"}
+		container.WorkingDir = "/deployments"
+
+		// Remote debugging
+		if t.Debug {
+			suspend := "n"
+			if t.DebugSuspend {
+				suspend = "y"
+			}
+			container.Args = append(container.Args,
+				fmt.Sprintf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=%s,address=%s",
+					suspend, t.DebugAddress))
+		}
+
+		hasHeapSizeOption := false
+		// Add JVM options
+		if t.Options != nil {
+			hasHeapSizeOption = strings.Contains(*t.Options, "-Xmx") ||
+				strings.Contains(*t.Options, "-XX:MaxHeapSize") ||
+				strings.Contains(*t.Options, "-XX:MinRAMPercentage") ||
+				strings.Contains(*t.Options, "-XX:MaxRAMPercentage")
+
+			container.Args = append(container.Args, strings.Split(*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 := int64(50)
+			// Unless the memory limit is lower than 300M, in which case we leave more room for the non-heap memory
+			if resource.NewScaledQuantity(300, 6).Cmp(memory) > 0 {
+				percentage = 25
+			}
+			memory.AsDec().Mul(memory.AsDec(), inf.NewDec(percentage, 2))
+			container.Args = append(container.Args, fmt.Sprintf("-Xmx%dM", memory.ScaledValue(resource.Mega)))
+		}
+
 		// Add mounted resources to the class path
 		for _, m := range container.VolumeMounts {
 			classpath.Add(m.MountPath)
@@ -117,15 +173,12 @@ func (t *classpathTrait) Apply(e *Environment) error {
 		} else {
 			container.Args = append(container.Args, defaultMainClass)
 		}
-
-		// Set the container working directory
-		container.WorkingDir = "/deployments"
 	}
 
 	return nil
 }
 
 // IsPlatformTrait overrides base class method
-func (t *classpathTrait) IsPlatformTrait() bool {
+func (t *jvmTrait) IsPlatformTrait() bool {
 	return true
 }
diff --git a/pkg/trait/trait_catalog.go b/pkg/trait/trait_catalog.go
index a045f3085..41d0fea32 100644
--- a/pkg/trait/trait_catalog.go
+++ b/pkg/trait/trait_catalog.go
@@ -35,7 +35,6 @@ type Catalog struct {
 	tPlatform         Trait
 	tAffinity         Trait
 	tCamel            Trait
-	tDebug            Trait
 	tDependencies     Trait
 	tDeployer         Trait
 	tCron             Trait
@@ -52,7 +51,7 @@ type Catalog struct {
 	tBuilder          Trait
 	tIstio            Trait
 	tEnvironment      Trait
-	tClasspath        Trait
+	tJvm              Trait
 	tRestDsl          Trait
 	tProbes           Trait
 	tQuarkus          Trait
@@ -67,7 +66,6 @@ func NewCatalog(ctx context.Context, c client.Client) *Catalog {
 		tPlatform:         newPlatformTrait(),
 		tAffinity:         newAffinityTrait(),
 		tCamel:            newCamelTrait(),
-		tDebug:            newDebugTrait(),
 		tRestDsl:          newRestDslTrait(),
 		tKnative:          newKnativeTrait(),
 		tDependencies:     newDependenciesTrait(),
@@ -85,7 +83,7 @@ func NewCatalog(ctx context.Context, c client.Client) *Catalog {
 		tBuilder:          newBuilderTrait(),
 		tIstio:            newIstioTrait(),
 		tEnvironment:      newEnvironmentTrait(),
-		tClasspath:        newClasspathTrait(),
+		tJvm:              newJvmTrait(),
 		tProbes:           newProbesTrait(),
 		tQuarkus:          newQuarkusTrait(),
 		tContainer:        newContainerTrait(),
@@ -108,7 +106,6 @@ func (c *Catalog) allTraits() []Trait {
 		c.tPlatform,
 		c.tAffinity,
 		c.tCamel,
-		c.tDebug,
 		c.tRestDsl,
 		c.tKnative,
 		c.tDependencies,
@@ -126,7 +123,7 @@ func (c *Catalog) allTraits() []Trait {
 		c.tBuilder,
 		c.tIstio,
 		c.tEnvironment,
-		c.tClasspath,
+		c.tJvm,
 		c.tProbes,
 		c.tQuarkus,
 		c.tContainer,
@@ -166,8 +163,7 @@ func (c *Catalog) TraitsForProfile(profile v1.TraitProfile) []Trait {
 			c.tPullSecret,
 			c.tJolokia,
 			c.tPrometheus,
-			c.tDebug,
-			c.tClasspath,
+			c.tJvm,
 			c.tProbes,
 			c.tRoute,
 			c.tIstio,
@@ -192,8 +188,7 @@ func (c *Catalog) TraitsForProfile(profile v1.TraitProfile) []Trait {
 			c.tPullSecret,
 			c.tJolokia,
 			c.tPrometheus,
-			c.tDebug,
-			c.tClasspath,
+			c.tJvm,
 			c.tProbes,
 			c.tIngress,
 			c.tIstio,
@@ -219,8 +214,7 @@ func (c *Catalog) TraitsForProfile(profile v1.TraitProfile) []Trait {
 			c.tPullSecret,
 			c.tJolokia,
 			c.tPrometheus,
-			c.tDebug,
-			c.tClasspath,
+			c.tJvm,
 			c.tProbes,
 			c.tIstio,
 			c.tOwner,


Test patch:
diff --git a/pkg/builder/builder_steps_test.go b/pkg/builder/builder_steps_test.go
index 39c7ef2c2..a60744d49 100644
--- a/pkg/builder/builder_steps_test.go
+++ b/pkg/builder/builder_steps_test.go
@@ -154,7 +154,7 @@ func TestListPublishedImages(t *testing.T) {
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -173,7 +173,7 @@ func TestListPublishedImages(t *testing.T) {
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
diff --git a/pkg/cmd/trait_help_test.go b/pkg/cmd/trait_help_test.go
new file mode 100644
index 000000000..e438ce088
--- /dev/null
+++ b/pkg/cmd/trait_help_test.go
@@ -0,0 +1,37 @@
+/*
+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 cmd
+
+import (
+	"testing"
+
+	"github.com/apache/camel-k/pkg/util/test"
+)
+
+func TestHelpForNonExistentTrait(t *testing.T) {
+	options, rootCommand := kamelTestPreAddCommandInit()
+	traitHelpCommand, _ := newTraitHelpCmd(options)
+	rootCommand.AddCommand(traitHelpCommand)
+
+	kamelTestPostAddCommandInit(t, rootCommand)
+
+	_, err := test.ExecuteCommand(rootCommand, "trait", "foobar")
+	if err == nil {
+		t.Fatalf("Expected error result for invalid trait 'foobar'")
+	}
+}
diff --git a/pkg/controller/integration/util_test.go b/pkg/controller/integration/util_test.go
index d7efedcbf..3f32dc6bc 100644
--- a/pkg/controller/integration/util_test.go
+++ b/pkg/controller/integration/util_test.go
@@ -34,7 +34,7 @@ func TestLookupKitForIntegration_DiscardKitsInError(t *testing.T) {
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -56,7 +56,7 @@ func TestLookupKitForIntegration_DiscardKitsInError(t *testing.T) {
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -110,7 +110,7 @@ func TestLookupKitForIntegration_DiscardKitsWithIncompatibleTraits(t *testing.T)
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -148,7 +148,7 @@ func TestLookupKitForIntegration_DiscardKitsWithIncompatibleTraits(t *testing.T)
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -181,7 +181,7 @@ func TestLookupKitForIntegration_DiscardKitsWithIncompatibleTraits(t *testing.T)
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
@@ -224,7 +224,7 @@ func TestLookupKitForIntegration_DiscardKitsWithIncompatibleTraits(t *testing.T)
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "ns",
diff --git a/pkg/trait/debug_test.go b/pkg/trait/debug_test.go
deleted file mode 100644
index 32b3e1dea..000000000
--- a/pkg/trait/debug_test.go
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-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 (
-	"context"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	appsv1 "k8s.io/api/apps/v1"
-	corev1 "k8s.io/api/core/v1"
-
-	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
-	"github.com/apache/camel-k/pkg/util/camel"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
-)
-
-func TestDebugTraitApplicability(t *testing.T) {
-	catalog, err := camel.DefaultCatalog()
-	assert.Nil(t, err)
-
-	env := Environment{
-		CamelCatalog: catalog,
-		Integration: &v1.Integration{
-			Status: v1.IntegrationStatus{
-				Phase: v1.IntegrationPhaseDeploying,
-			},
-			Spec: v1.IntegrationSpec{
-				Traits: map[string]v1.TraitSpec{
-					"debug": {
-						Configuration: map[string]string{
-							"enabled": "true",
-						},
-					},
-				},
-			},
-		},
-	}
-
-	trait := newDebugTrait()
-
-	enabled, err := trait.Configure(&env)
-	assert.Nil(t, err)
-	assert.False(t, enabled)
-
-	env.Integration.Status.Phase = v1.IntegrationPhaseRunning
-
-	enabled, err = trait.Configure(&env)
-	assert.Nil(t, err)
-	assert.False(t, enabled)
-}
-
-func TestApplyDebugTrait(t *testing.T) {
-	environment := Environment{
-		Catalog: NewCatalog(context.TODO(), nil),
-		Integration: &v1.Integration{
-			Status: v1.IntegrationStatus{
-				Phase: v1.IntegrationPhaseDeploying,
-			},
-			Spec: v1.IntegrationSpec{
-				Traits: map[string]v1.TraitSpec{
-					"debug": {
-						Configuration: map[string]string{
-							"enabled": "true",
-						},
-					},
-				},
-			},
-		},
-		Resources: kubernetes.NewCollection(),
-	}
-
-	d := appsv1.Deployment{
-		Spec: appsv1.DeploymentSpec{
-			Template: corev1.PodTemplateSpec{
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{
-						{
-							Name: defaultContainerName,
-						},
-					},
-				},
-			},
-		},
-	}
-
-	environment.Resources.Add(&d)
-
-	trait := newDebugTrait()
-
-	assert.Nil(t, trait.Apply(&environment))
-	assert.Equal(t, d.Spec.Template.Spec.Containers[0].Args, []string{
-		"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005",
-	})
-}
diff --git a/pkg/trait/classpath_test.go b/pkg/trait/jvm_test.go
similarity index 70%
rename from pkg/trait/classpath_test.go
rename to pkg/trait/jvm_test.go
index 5b12675f0..e180202b7 100644
--- a/pkg/trait/classpath_test.go
+++ b/pkg/trait/jvm_test.go
@@ -22,29 +22,30 @@ import (
 	"sort"
 	"testing"
 
+	"github.com/scylladb/go-set/strset"
+	"github.com/stretchr/testify/assert"
+
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	serving "knative.dev/serving/pkg/apis/serving/v1"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/test"
-
-	"github.com/scylladb/go-set/strset"
-	"github.com/stretchr/testify/assert"
 )
 
-func TestConfigureClasspathTraitInRightPhasesDoesSucceed(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestConfigureJvmTraitInRightPhasesDoesSucceed(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 
 	configured, err := trait.Configure(environment)
 	assert.Nil(t, err)
 	assert.True(t, configured)
 }
 
-func TestConfigureClasspathTraitInWrongIntegrationPhaseDoesNotSucceed(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestConfigureJvmTraitInWrongIntegrationPhaseDoesNotSucceed(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 	environment.Integration.Status.Phase = v1.IntegrationPhaseError
 
 	configured, err := trait.Configure(environment)
@@ -52,8 +53,8 @@ func TestConfigureClasspathTraitInWrongIntegrationPhaseDoesNotSucceed(t *testing
 	assert.False(t, configured)
 }
 
-func TestConfigureClasspathTraitInWrongIntegrationKitPhaseDoesNotSucceed(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestConfigureJvmTraitInWrongIntegrationKitPhaseDoesNotSucceed(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 	environment.IntegrationKit.Status.Phase = v1.IntegrationKitPhaseWaitingForPlatform
 
 	configured, err := trait.Configure(environment)
@@ -61,8 +62,8 @@ func TestConfigureClasspathTraitInWrongIntegrationKitPhaseDoesNotSucceed(t *test
 	assert.False(t, configured)
 }
 
-func TestConfigureClasspathDisabledTraitDoesNotSucceed(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestConfigureJvmDisabledTraitDoesNotSucceed(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 	trait.Enabled = new(bool)
 
 	configured, err := trait.Configure(environment)
@@ -70,8 +71,8 @@ func TestConfigureClasspathDisabledTraitDoesNotSucceed(t *testing.T) {
 	assert.False(t, configured)
 }
 
-func TestApplyClasspathTraitWithDeploymentResource(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestApplyJvmTraitWithDeploymentResource(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 
 	d := appsv1.Deployment{
 		Spec: appsv1.DeploymentSpec{
@@ -108,8 +109,8 @@ func TestApplyClasspathTraitWithDeploymentResource(t *testing.T) {
 	})
 }
 
-func TestApplyClasspathTraitWithKNativeResource(t *testing.T) {
-	trait, environment := createNominalClasspathTest()
+func TestApplyJvmTraitWithKNativeResource(t *testing.T) {
+	trait, environment := createNominalJvmTest()
 
 	s := serving.Service{}
 	s.Spec.ConfigurationSpec.Template = serving.RevisionTemplateSpec{}
@@ -140,16 +141,51 @@ func TestApplyClasspathTraitWithKNativeResource(t *testing.T) {
 	})
 }
 
-func createNominalClasspathTest() (*classpathTrait, *Environment) {
-	return createClasspathTestWithKitType(v1.IntegrationKitTypePlatform)
+func TestApplyJvmTraitWithDebugEnabled(t *testing.T) {
+	trait, environment := createNominalJvmTest()
+	trait.Debug = true
+	trait.DebugSuspend = true
+
+	d := appsv1.Deployment{
+		Spec: appsv1.DeploymentSpec{
+			Template: corev1.PodTemplateSpec{
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name: defaultContainerName,
+							VolumeMounts: []corev1.VolumeMount{
+								{
+									MountPath: "/mount/path",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	environment.Resources.Add(&d)
+
+	err := trait.Apply(environment)
+
+	assert.Nil(t, err)
+
+	assert.Contains(t, d.Spec.Template.Spec.Containers[0].Args,
+		"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005",
+	)
+}
+
+func createNominalJvmTest() (*jvmTrait, *Environment) {
+	return createJvmTestWithKitType(v1.IntegrationKitTypePlatform)
 }
 
-func createClasspathTestWithKitType(kitType string) (*classpathTrait, *Environment) {
+func createJvmTestWithKitType(kitType string) (*jvmTrait, *Environment) {
 	client, _ := test.NewFakeClient(
 		&v1.IntegrationKit{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: v1.SchemeGroupVersion.String(),
-				Kind:       v1.IntegrationKindKind,
+				Kind:       v1.IntegrationKitKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: "kit-namespace",
@@ -161,7 +197,7 @@ func createClasspathTestWithKitType(kitType string) (*classpathTrait, *Environme
 		},
 	)
 
-	trait := newClasspathTrait()
+	trait := newJvmTrait()
 	enabled := true
 	trait.Enabled = &enabled
 	trait.ctx = context.TODO()
diff --git a/pkg/trait/trait_test.go b/pkg/trait/trait_test.go
index bab4131e7..72a8da062 100644
--- a/pkg/trait/trait_test.go
+++ b/pkg/trait/trait_test.go
@@ -376,7 +376,7 @@ func TestOnlySomeTraitsInfluenceBuild(t *testing.T) {
 
 func TestOnlySomeTraitsArePlatform(t *testing.T) {
 	c := NewTraitTestCatalog()
-	platformTraits := []string{"builder", "camel", "classpath", "container", "dependencies", "deployer", "deployment", "environment", "rest-dsl", "owner", "platform"}
+	platformTraits := []string{"builder", "camel", "jvm", "container", "dependencies", "deployer", "deployment", "environment", "rest-dsl", "owner", "platform"}
 
 	for _, trait := range c.allTraits() {
 		if trait.IsPlatformTrait() {