You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2022/01/12 08:15:18 UTC

[camel-k] 18/22: Fix #1107: fix findings

This is an automated email from the ASF dual-hosted git repository.

nferraro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 9d89922951da6dcfc258d7fad097e7e47b78a4dc
Author: nicolaferraro <ni...@gmail.com>
AuthorDate: Wed Dec 22 00:54:16 2021 +0100

    Fix #1107: fix findings
---
 addons/keda/keda.go                    |  43 ++++++-------
 addons/keda/keda_test.go               |  61 ++++++++++++++++---
 docs/modules/traits/pages/keda.adoc    |   4 --
 e2e/common/scale_binding_test.go       |  11 +---
 e2e/common/scale_integration_test.go   |  11 +---
 pkg/client/{serverside.go => apply.go} |   0
 pkg/client/client.go                   |   2 +
 pkg/client/scale.go                    |  35 +++++++++++
 pkg/cmd/run.go                         |   2 +-
 pkg/resources/resources.go             |   4 +-
 pkg/trait/deployer.go                  | 108 +--------------------------------
 pkg/util/test/client.go                |  45 +++++++++++++-
 resources/traits.yaml                  |   4 --
 13 files changed, 157 insertions(+), 173 deletions(-)

diff --git a/addons/keda/keda.go b/addons/keda/keda.go
index ad9f71d..e6e1d5e 100644
--- a/addons/keda/keda.go
+++ b/addons/keda/keda.go
@@ -38,7 +38,7 @@ import (
 	"github.com/apache/camel-k/pkg/util/source"
 	"github.com/apache/camel-k/pkg/util/uri"
 	"github.com/pkg/errors"
-	scase "github.com/stoewer/go-strcase"
+	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	ctrl "sigs.k8s.io/controller-runtime/pkg/client"
@@ -76,8 +76,6 @@ type kedaTrait struct {
 	trait.BaseTrait `property:",squash"`
 	// Enables automatic configuration of the trait. Allows the trait to infer KEDA triggers from the Kamelets.
 	Auto *bool `property:"auto" json:"auto,omitempty"`
-	// Convert metadata properties to camelCase (needed because Camel K trait properties use kebab-case from command line). Disabled by default.
-	CamelCaseConversion *bool `property:"camel-case-conversion" json:"camelCaseConversion,omitempty"`
 	// Set the spec->replicas field on the top level controller to an explicit value if missing, to allow KEDA to recognize it as a scalable resource.
 	HackControllerReplicas *bool `property:"hack-controller-replicas" json:"hackControllerReplicas,omitempty"`
 	// Interval (seconds) to check each trigger on.
@@ -170,11 +168,7 @@ func (t *kedaTrait) addScalingResources(e *trait.Environment) error {
 	for idx, trigger := range t.Triggers {
 		meta := make(map[string]string)
 		for k, v := range trigger.Metadata {
-			kk := k
-			if t.CamelCaseConversion != nil && *t.CamelCaseConversion {
-				kk = scase.LowerCamelCase(k)
-			}
-			meta[kk] = v
+			meta[k] = v
 		}
 		var authenticationRef *kedav1alpha1.ScaledObjectAuthRef
 		if len(trigger.authentication) > 0 && trigger.AuthenticationSecret != "" {
@@ -269,28 +263,25 @@ func (t *kedaTrait) addScalingResources(e *trait.Environment) error {
 
 func (t *kedaTrait) hackControllerReplicas(e *trait.Environment) error {
 	ctrlRef := t.getTopControllerReference(e)
+	scale := autoscalingv1.Scale{
+		Spec: autoscalingv1.ScaleSpec{
+			Replicas: int32(1),
+		},
+	}
+	scalesClient, err := e.Client.ScalesClient()
+	if err != nil {
+		return err
+	}
 	if ctrlRef.Kind == camelv1alpha1.KameletBindingKind {
-		// Update the KameletBinding directly (do not add it to env resources, it's the integration parent)
-		key := ctrl.ObjectKey{
-			Namespace: e.Integration.Namespace,
-			Name:      ctrlRef.Name,
-		}
-		klb := camelv1alpha1.KameletBinding{}
-		if err := e.Client.Get(e.Ctx, key, &klb); err != nil {
+		scale.ObjectMeta.Name = ctrlRef.Name
+		_, err = scalesClient.Scales(e.Integration.Namespace).Update(e.Ctx, camelv1alpha1.SchemeGroupVersion.WithResource("kameletbindings").GroupResource(), &scale, metav1.UpdateOptions{})
+		if err != nil {
 			return err
 		}
-		if klb.Spec.Replicas == nil {
-			one := int32(1)
-			klb.Spec.Replicas = &one
-			if err := e.Client.Update(e.Ctx, &klb); err != nil {
-				return err
-			}
-		}
 	} else if e.Integration.Spec.Replicas == nil {
-		one := int32(1)
-		e.Integration.Spec.Replicas = &one
-		// Update the Integration directly as the spec section is not merged by default
-		if err := e.Client.Update(e.Ctx, e.Integration); err != nil {
+		scale.ObjectMeta.Name = e.Integration.Name
+		_, err = scalesClient.Scales(e.Integration.Namespace).Update(e.Ctx, camelv1.SchemeGroupVersion.WithResource("integrations").GroupResource(), &scale, metav1.UpdateOptions{})
+		if err != nil {
 			return err
 		}
 	}
diff --git a/addons/keda/keda_test.go b/addons/keda/keda_test.go
index 08a627e..4783653 100644
--- a/addons/keda/keda_test.go
+++ b/addons/keda/keda_test.go
@@ -35,7 +35,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 var (
@@ -348,14 +347,58 @@ func TestHackReplicas(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, res)
 	assert.NoError(t, keda.Apply(env))
-	it := camelv1.Integration{}
-	key := client.ObjectKey{
-		Namespace: "test",
-		Name:      "my-it",
-	}
-	assert.NoError(t, env.Client.Get(env.Ctx, key, &it))
-	assert.NotNil(t, it.Spec.Replicas)
-	assert.Equal(t, int32(1), *it.Spec.Replicas)
+	scalesClient, err := env.Client.ScalesClient()
+	assert.NoError(t, err)
+	sc, err := scalesClient.Scales("test").Get(env.Ctx, camelv1.SchemeGroupVersion.WithResource("integrations").GroupResource(), "my-it", metav1.GetOptions{})
+	assert.NoError(t, err)
+	assert.Equal(t, int32(1), sc.Spec.Replicas)
+}
+
+func TestHackKLBReplicas(t *testing.T) {
+	keda, _ := NewKedaTrait().(*kedaTrait)
+	keda.Enabled = &testingTrue
+	keda.Auto = &testingFalse
+	keda.Triggers = append(keda.Triggers, kedaTrigger{
+		Type: "custom",
+		Metadata: map[string]string{
+			"a": "b",
+		},
+	})
+	keda.HackControllerReplicas = &testingTrue
+	env := createBasicTestEnvironment(
+		&camelv1alpha1.KameletBinding{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test",
+				Name:      "my-klb",
+			},
+		},
+		&camelv1.Integration{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test",
+				Name:      "my-it",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: camelv1alpha1.SchemeGroupVersion.String(),
+						Kind:       "KameletBinding",
+						Name:       "my-klb",
+					},
+				},
+			},
+			Status: camelv1.IntegrationStatus{
+				Phase: camelv1.IntegrationPhaseInitialization,
+			},
+		},
+	)
+
+	res, err := keda.Configure(env)
+	assert.NoError(t, err)
+	assert.True(t, res)
+	assert.NoError(t, keda.Apply(env))
+	scalesClient, err := env.Client.ScalesClient()
+	assert.NoError(t, err)
+	sc, err := scalesClient.Scales("test").Get(env.Ctx, camelv1alpha1.SchemeGroupVersion.WithResource("kameletbindings").GroupResource(), "my-klb", metav1.GetOptions{})
+	assert.NoError(t, err)
+	assert.Equal(t, int32(1), sc.Spec.Replicas)
 }
 
 func getScaledObject(e *trait.Environment) *v1alpha1.ScaledObject {
diff --git a/docs/modules/traits/pages/keda.adoc b/docs/modules/traits/pages/keda.adoc
index df6c8d9..b1a5827 100644
--- a/docs/modules/traits/pages/keda.adoc
+++ b/docs/modules/traits/pages/keda.adoc
@@ -38,10 +38,6 @@ The following configuration options are available:
 | bool
 | Enables automatic configuration of the trait. Allows the trait to infer KEDA triggers from the Kamelets.
 
-| keda.camel-case-conversion
-| bool
-| Convert metadata properties to camelCase (needed because Camel K trait properties use kebab-case from command line). Disabled by default.
-
 | keda.hack-controller-replicas
 | bool
 | Set the spec->replicas field on the top level controller to an explicit value if missing, to allow KEDA to recognize it as a scalable resource.
diff --git a/e2e/common/scale_binding_test.go b/e2e/common/scale_binding_test.go
index 49d9668..3bbc2ee 100644
--- a/e2e/common/scale_binding_test.go
+++ b/e2e/common/scale_binding_test.go
@@ -33,10 +33,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/restmapper"
-	"k8s.io/client-go/scale"
-
 	. "github.com/apache/camel-k/e2e/support"
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
@@ -80,12 +76,7 @@ func TestKameletBindingScale(t *testing.T) {
 
 		t.Run("Scale kamelet binding with polymorphic client", func(t *testing.T) {
 			RegisterTestingT(t)
-			// Polymorphic scale client
-			groupResources, err := restmapper.GetAPIGroupResources(TestClient().Discovery())
-			Expect(err).To(BeNil())
-			mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
-			resolver := scale.NewDiscoveryScaleKindResolver(TestClient().Discovery())
-			scaleClient, err := scale.NewForConfig(TestClient().GetConfig(), mapper, dynamic.LegacyAPIPathResolverFunc, resolver)
+			scaleClient, err := TestClient().ScalesClient()
 			Expect(err).To(BeNil())
 
 			// Patch the integration scale subresource
diff --git a/e2e/common/scale_integration_test.go b/e2e/common/scale_integration_test.go
index 5c70901..1c01d1c 100644
--- a/e2e/common/scale_integration_test.go
+++ b/e2e/common/scale_integration_test.go
@@ -33,10 +33,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/restmapper"
-	"k8s.io/client-go/scale"
-
 	. "github.com/apache/camel-k/e2e/support"
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/pkg/client/camel/clientset/versioned"
@@ -67,12 +63,7 @@ func TestIntegrationScale(t *testing.T) {
 
 		t.Run("Scale integration with polymorphic client", func(t *testing.T) {
 			RegisterTestingT(t)
-			// Polymorphic scale client
-			groupResources, err := restmapper.GetAPIGroupResources(TestClient().Discovery())
-			Expect(err).To(BeNil())
-			mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
-			resolver := scale.NewDiscoveryScaleKindResolver(TestClient().Discovery())
-			scaleClient, err := scale.NewForConfig(TestClient().GetConfig(), mapper, dynamic.LegacyAPIPathResolverFunc, resolver)
+			scaleClient, err := TestClient().ScalesClient()
 			Expect(err).To(BeNil())
 
 			// Patch the integration scale subresource
diff --git a/pkg/client/serverside.go b/pkg/client/apply.go
similarity index 100%
rename from pkg/client/serverside.go
rename to pkg/client/apply.go
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 2cf73c2..967a5fc 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -26,6 +26,7 @@ import (
 	user "github.com/mitchellh/go-homedir"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	"k8s.io/client-go/scale"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -64,6 +65,7 @@ type Client interface {
 	GetConfig() *rest.Config
 	GetCurrentNamespace(kubeConfig string) (string, error)
 	ServerOrClientSideApplier() ServerOrClientSideApplier
+	ScalesClient() (scale.ScalesGetter, error)
 }
 
 // Injectable identifies objects that can receive a Client.
diff --git a/pkg/client/scale.go b/pkg/client/scale.go
new file mode 100644
index 0000000..7bcf7d7
--- /dev/null
+++ b/pkg/client/scale.go
@@ -0,0 +1,35 @@
+/*
+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 client
+
+import (
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/scale"
+)
+
+func (c *defaultClient) ScalesClient() (scale.ScalesGetter, error) {
+	// Polymorphic scale client
+	groupResources, err := restmapper.GetAPIGroupResources(c.Discovery())
+	if err != nil {
+		return nil, err
+	}
+	mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
+	resolver := scale.NewDiscoveryScaleKindResolver(c.Discovery())
+	return scale.NewForConfig(c.GetConfig(), mapper, dynamic.LegacyAPIPathResolverFunc, resolver)
+}
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index d2d260f..4721fef 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -54,7 +54,7 @@ import (
 	"github.com/apache/camel-k/pkg/util/watch"
 )
 
-var traitConfigRegexp = regexp.MustCompile(`^([a-z0-9-]+)((?:\[[0-9]+\]|\.[a-z0-9-]+)+)=(.*)$`)
+var traitConfigRegexp = regexp.MustCompile(`^([a-z0-9-]+)((?:\.[a-z0-9-]+)(?:\[[0-9]+\]|\.[A-Za-z0-9-_]+)*)=(.*)$`)
 
 func newCmdRun(rootCmdOptions *RootCmdOptions) (*cobra.Command, *runCmdOptions) {
 	options := runCmdOptions{
diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go
index 80a8c6d..5d8301d 100644
--- a/pkg/resources/resources.go
+++ b/pkg/resources/resources.go
@@ -555,9 +555,9 @@ var assets = func() http.FileSystem {
 		"/traits.yaml": &vfsgen۰CompressedFileInfo{
 			name:             "traits.yaml",
 			modTime:          time.Time{},
-			uncompressedSize: 49570,
+			uncompressedSize: 49341,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x7d\xfd\x73\x5b\xb9\x91\xe0\xef\xf3\x57\xa0\xb4\x57\x65\x49\x45\x52\x9e\xc9\x26\x3b\xa7\xbb\xd9\x94\xc6\x76\x12\xcd\xf8\x43\x67\x3b\xb3\x97\x9a\x9b\x0a\xc1\xf7\x9a\x24\xcc\x47\xe0\x05\xc0\x93\xcc\xdc\xde\xff\x7e\x85\xee\xc6\xc7\x7b\x24\x25\xca\xb6\x66\xa3\xad\xdd\x54\xed\x58\xd2\x03\xd0\x68\x34\xfa\xbb\x1b\xde\x4a\xe5\xdd\xf9\x57\x63\xa1\xe5\x1a\xce\x85\x9c\xcf\x95\x56\x7e\xf3\x95\x10\x6d\x23\xfd\xdc\xd8\xf5\xb9\x [...]
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x7d\xfd\x73\x5b\xb9\x91\xe0\xef\xf3\x57\xa0\xb4\x57\x65\x49\x45\x52\x9e\xc9\x26\x3b\xa7\xbb\xd9\x94\xc6\x76\x12\xcd\xf8\x43\x67\x3b\xb3\x97\x9a\x9b\x0a\xc1\xf7\x9a\x24\xcc\x47\xe0\x05\xc0\x93\xcc\xdc\xde\xff\x7e\x85\xee\xc6\xc7\x7b\x24\x25\xca\xb6\x66\xa3\xad\xdd\x54\xed\x58\xd2\x03\xd0\x68\x34\xfa\xbb\x1b\xde\x4a\xe5\xdd\xf9\x57\x63\xa1\xe5\x1a\xce\x85\x9c\xcf\x95\x56\x7e\xf3\x95\x10\x6d\x23\xfd\xdc\xd8\xf5\xb9\x [...]
 		},
 	}
 	fs["/"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
diff --git a/pkg/trait/deployer.go b/pkg/trait/deployer.go
index 67cdb79..7735a37 100644
--- a/pkg/trait/deployer.go
+++ b/pkg/trait/deployer.go
@@ -17,22 +17,6 @@ limitations under the License.
 
 package trait
 
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-	"strings"
-
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/types"
-
-	ctrl "sigs.k8s.io/controller-runtime/pkg/client"
-
-	"github.com/apache/camel-k/pkg/util/patch"
-)
-
 // The deployer trait is responsible for deploying the resources owned by the integration, and can be used
 // to explicitly select the underlying controller that will manage the integration pods.
 //
@@ -45,8 +29,6 @@ type deployerTrait struct {
 
 var _ ControllerStrategySelector = &deployerTrait{}
 
-var hasServerSideApply = true
-
 func newDeployerTrait() Trait {
 	return &deployerTrait{
 		BaseTrait: NewBaseTrait("deployer", 900),
@@ -60,28 +42,9 @@ func (t *deployerTrait) Configure(e *Environment) (bool, error) {
 func (t *deployerTrait) Apply(e *Environment) error {
 	// Register a post action that patches the resources generated by the traits
 	e.PostActions = append(e.PostActions, func(env *Environment) error {
+		applier := e.Client.ServerOrClientSideApplier()
 		for _, resource := range env.Resources.Items() {
-			// We assume that server-side apply is enabled by default.
-			// It is currently convoluted to check pro-actively whether server-side apply
-			// is enabled. This is possible to fetch the OpenAPI endpoint, which returns
-			// the entire server API document, then lookup the resource PATCH endpoint, and
-			// check its list of accepted MIME types.
-			// As a simpler solution, we fall back to client-side apply at the first
-			// 415 error, and assume server-side apply is not available globally.
-			if hasServerSideApply {
-				err := t.serverSideApply(env, resource)
-				switch {
-				case err == nil:
-					continue
-				case isIncompatibleServerError(err):
-					t.L.Info("Fallback to client-side apply to patch resources")
-					hasServerSideApply = false
-				default:
-					// Keep server-side apply unless server is incompatible with it
-					return err
-				}
-			}
-			if err := t.clientSideApply(env, resource); err != nil {
+			if err := applier.Apply(e.Ctx, resource); err != nil {
 				return err
 			}
 		}
@@ -91,73 +54,6 @@ func (t *deployerTrait) Apply(e *Environment) error {
 	return nil
 }
 
-func (t *deployerTrait) serverSideApply(env *Environment, resource ctrl.Object) error {
-	target, err := patch.PositiveApplyPatch(resource)
-	if err != nil {
-		return err
-	}
-	err = env.Client.Patch(env.Ctx, target, ctrl.Apply, ctrl.ForceOwnership, ctrl.FieldOwner("camel-k-operator"))
-	if err != nil {
-		return fmt.Errorf("error during apply resource: %s/%s: %w", resource.GetNamespace(), resource.GetName(), err)
-	}
-	// Update the resource with the response returned from the API server
-	return t.unstructuredToRuntimeObject(target, resource)
-}
-
-func (t *deployerTrait) clientSideApply(env *Environment, resource ctrl.Object) error {
-	err := env.Client.Create(env.Ctx, resource)
-	if err == nil {
-		return nil
-	} else if !k8serrors.IsAlreadyExists(err) {
-		return fmt.Errorf("error during create resource: %s/%s: %w", resource.GetNamespace(), resource.GetName(), err)
-	}
-	object := &unstructured.Unstructured{}
-	object.SetNamespace(resource.GetNamespace())
-	object.SetName(resource.GetName())
-	object.SetGroupVersionKind(resource.GetObjectKind().GroupVersionKind())
-	err = env.Client.Get(env.Ctx, ctrl.ObjectKeyFromObject(object), object)
-	if err != nil {
-		return err
-	}
-	p, err := patch.PositiveMergePatch(object, resource)
-	if err != nil {
-		return err
-	} else if len(p) == 0 {
-		// Update the resource with the object returned from the API server
-		return t.unstructuredToRuntimeObject(object, resource)
-	}
-	err = env.Client.Patch(env.Ctx, resource, ctrl.RawPatch(types.MergePatchType, p))
-	if err != nil {
-		return fmt.Errorf("error during patch %s/%s: %w", resource.GetNamespace(), resource.GetName(), err)
-	}
-	return nil
-}
-
-func (t *deployerTrait) unstructuredToRuntimeObject(u *unstructured.Unstructured, obj ctrl.Object) error {
-	data, err := json.Marshal(u)
-	if err != nil {
-		return err
-	}
-	return json.Unmarshal(data, obj)
-}
-
-func isIncompatibleServerError(err error) bool {
-	// First simpler check for older servers (i.e. OpenShift 3.11)
-	if strings.Contains(err.Error(), "415: Unsupported Media Type") {
-		return true
-	}
-
-	// 415: Unsupported media type means we're talking to a server which doesn't
-	// support server-side apply.
-	var serr *k8serrors.StatusError
-	if errors.As(err, &serr) {
-		return serr.Status().Code == http.StatusUnsupportedMediaType
-	}
-
-	// Non-StatusError means the error isn't because the server is incompatible.
-	return false
-}
-
 func (t *deployerTrait) SelectControllerStrategy(e *Environment) (*ControllerStrategy, error) {
 	if IsFalse(t.Enabled) {
 		return nil, nil
diff --git a/pkg/util/test/client.go b/pkg/util/test/client.go
index b4f6db4..7086e05 100644
--- a/pkg/util/test/client.go
+++ b/pkg/util/test/client.go
@@ -19,6 +19,7 @@ package test
 
 import (
 	"context"
+	"fmt"
 	"strings"
 
 	"github.com/apache/camel-k/pkg/apis"
@@ -27,6 +28,7 @@ import (
 	fakecamelclientset "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/fake"
 	camelv1 "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/typed/camel/v1"
 	camelv1alpha1 "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/typed/camel/v1alpha1"
+	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -36,6 +38,9 @@ import (
 	fakeclientset "k8s.io/client-go/kubernetes/fake"
 	clientscheme "k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
+	"k8s.io/client-go/scale"
+	fakescale "k8s.io/client-go/scale/fake"
+	"k8s.io/client-go/testing"
 	controller "sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
@@ -57,11 +62,44 @@ func NewFakeClient(initObjs ...runtime.Object) (client.Client, error) {
 	clientset := fakeclientset.NewSimpleClientset(filterObjects(scheme, initObjs, func(gvk schema.GroupVersionKind) bool {
 		return !strings.Contains(gvk.Group, "camel") && !strings.Contains(gvk.Group, "knative")
 	})...)
+	replicasCount := make(map[string]int32)
+	fakescaleclient := fakescale.FakeScaleClient{}
+	fakescaleclient.AddReactor("update", "*", func(rawAction testing.Action) (handled bool, ret runtime.Object, err error) {
+		action := rawAction.(testing.UpdateAction)       // nolint: forcetypeassert
+		obj := action.GetObject().(*autoscalingv1.Scale) // nolint: forcetypeassert
+		replicas := obj.Spec.Replicas
+		key := fmt.Sprintf("%s:%s:%s/%s", action.GetResource().Group, action.GetResource().Resource, action.GetNamespace(), obj.GetName())
+		replicasCount[key] = replicas
+		return true, &autoscalingv1.Scale{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      obj.Name,
+				Namespace: action.GetNamespace(),
+			},
+			Spec: autoscalingv1.ScaleSpec{
+				Replicas: replicas,
+			},
+		}, nil
+	})
+	fakescaleclient.AddReactor("get", "*", func(rawAction testing.Action) (handled bool, ret runtime.Object, err error) {
+		action := rawAction.(testing.GetAction) // nolint: forcetypeassert
+		key := fmt.Sprintf("%s:%s:%s/%s", action.GetResource().Group, action.GetResource().Resource, action.GetNamespace(), action.GetName())
+		obj := &autoscalingv1.Scale{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      action.GetName(),
+				Namespace: action.GetNamespace(),
+			},
+			Spec: autoscalingv1.ScaleSpec{
+				Replicas: replicasCount[key],
+			},
+		}
+		return true, obj, nil
+	})
 
 	return &FakeClient{
 		Client:    c,
 		Interface: clientset,
 		camel:     camelClientset,
+		scales:    &fakescaleclient,
 	}, nil
 }
 
@@ -82,7 +120,8 @@ func filterObjects(scheme *runtime.Scheme, input []runtime.Object, filter func(g
 type FakeClient struct {
 	controller.Client
 	kubernetes.Interface
-	camel camel.Interface
+	camel  camel.Interface
+	scales *fakescale.FakeScaleClient
 }
 
 func (c *FakeClient) CamelV1() camelv1.CamelV1Interface {
@@ -123,6 +162,10 @@ func (c *FakeClient) ServerOrClientSideApplier() client.ServerOrClientSideApplie
 	}
 }
 
+func (c *FakeClient) ScalesClient() (scale.ScalesGetter, error) {
+	return c.scales, nil
+}
+
 type FakeDiscovery struct {
 	discovery.DiscoveryInterface
 }
diff --git a/resources/traits.yaml b/resources/traits.yaml
index 638418d..fd24ebe 100755
--- a/resources/traits.yaml
+++ b/resources/traits.yaml
@@ -593,10 +593,6 @@ traits:
     type: bool
     description: Enables automatic configuration of the trait. Allows the trait to
       infer KEDA triggers from the Kamelets.
-  - name: camel-case-conversion
-    type: bool
-    description: Convert metadata properties to camelCase (needed because Camel K
-      trait properties use kebab-case from command line). Disabled by default.
   - name: hack-controller-replicas
     type: bool
     description: Set the spec->replicas field on the top level controller to an explicit