Repository: apache/camel-k
Pull request #4317 (2023-05-03 13:21:13)

fix: Consistent integration platform settings hierarchy
Users are allowed to create namespace local IntegrationPlatform resources in order to overwrite settings used in the operator.

These namespace local platform settings should derive from the IntegrationPlatform being used by the operator by default. This means that only explicitly specified settings do overwrite the platform defaults used in the operator.

This PR makes sure that we use a consistent platform settings hierarchy where the global operator platform settings always represent the basis for user specified platform settings.

The changes in this PR:
- Makes sure to apply global integration platform settings when creating namespace local platform
- Global platform settings resist when the setting is not explicitly overwritten in the given user namespace integration platform

**Release Note**
```release-note
fix: Consistent integration platform settings hierarchy
```


squakez: Please, mind the conflict. Some other PR changing the same file was merged in the while.
christophd: @squakez thx, rebase is done. Now we need to run the full workflow jobs once again 😅 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54813544e..a678f89f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - Maximum number of running builds \(0\) exceeded [\#4290](https://github.com/apache/camel-k/issues/4290)
 - Should uninstall --all remove PersistentVolumeClaim? [\#4285](https://github.com/apache/camel-k/issues/4285)
 - Add statical check if integration refers to components supported by Camel K [\#4283](https://github.com/apache/camel-k/issues/4283)
+- Camel K 2.0 release process [\#4282](https://github.com/apache/camel-k/issues/4282)
 - Camel-k install and uninstall on a fresh kind cluster in Linux [\#4277](https://github.com/apache/camel-k/issues/4277)
 - Kamelet redis-source not working [\#4276](https://github.com/apache/camel-k/issues/4276)
 - Failed to fetch data from Mysql database using camel-k [\#4264](https://github.com/apache/camel-k/issues/4264)
@@ -39,12 +40,14 @@
 - Unused parameter [\#4152](https://github.com/apache/camel-k/issues/4152)
 - + [\#4151](https://github.com/apache/camel-k/issues/4151)
 - Kamel run sample integration is failing to run [\#4150](https://github.com/apache/camel-k/issues/4150)
+- Bring in startup probes to the Health trait [\#4146](https://github.com/apache/camel-k/issues/4146)
 - Bring an option to force an image build to the kamel run command [\#4126](https://github.com/apache/camel-k/issues/4126)
 - How to use JVM classpaths in Native mode? [\#4124](https://github.com/apache/camel-k/issues/4124)
 - Remove deprecated linters [\#4061](https://github.com/apache/camel-k/issues/4061)
 - Expose runtime details in `kamel version -a -v` [\#3836](https://github.com/apache/camel-k/issues/3836)
 - Remove KEP cryptic message when installing [\#3829](https://github.com/apache/camel-k/issues/3829)
 - Improve error reporting in case of knative is required but not installed [\#3803](https://github.com/apache/camel-k/issues/3803)
+- Misleading message when updating an unchanged `Integration` [\#3705](https://github.com/apache/camel-k/issues/3705)
 - Private Registry With TLS  Can Not Work [\#3672](https://github.com/apache/camel-k/issues/3672)
 - Wrong KameletBinding definition leads to operator panic [\#3586](https://github.com/apache/camel-k/issues/3586)
 - \[E2E\] Add tests for enabling traits at IntegrationPlatform [\#3476](https://github.com/apache/camel-k/issues/3476)
diff --git a/addons/strimzi/duck/client/internalclientset/fake/register.go b/addons/strimzi/duck/client/internalclientset/fake/register.go
index e6ac15499..53a4309e7 100644
--- a/addons/strimzi/duck/client/internalclientset/fake/register.go
+++ b/addons/strimzi/duck/client/internalclientset/fake/register.go
@@ -38,14 +38,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//	import (
-//	  "k8s.io/client-go/kubernetes"
-//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//	)
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
 //
-//	kclientset, _ := kubernetes.NewForConfig(c)
-//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/addons/strimzi/duck/client/internalclientset/scheme/register.go b/addons/strimzi/duck/client/internalclientset/scheme/register.go
index 6abeca247..11f233b92 100644
--- a/addons/strimzi/duck/client/internalclientset/scheme/register.go
+++ b/addons/strimzi/duck/client/internalclientset/scheme/register.go
@@ -38,14 +38,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//	import (
-//	  "k8s.io/client-go/kubernetes"
-//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//	)
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
 //
-//	kclientset, _ := kubernetes.NewForConfig(c)
-//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/pkg/client/camel/clientset/versioned/fake/register.go b/pkg/client/camel/clientset/versioned/fake/register.go
index d55e6d595..249a2b9e3 100644
--- a/pkg/client/camel/clientset/versioned/fake/register.go
+++ b/pkg/client/camel/clientset/versioned/fake/register.go
@@ -40,14 +40,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//	import (
-//	  "k8s.io/client-go/kubernetes"
-//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//	)
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
 //
-//	kclientset, _ := kubernetes.NewForConfig(c)
-//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/pkg/client/camel/clientset/versioned/scheme/register.go b/pkg/client/camel/clientset/versioned/scheme/register.go
index 493d56e4c..9366fb20c 100644
--- a/pkg/client/camel/clientset/versioned/scheme/register.go
+++ b/pkg/client/camel/clientset/versioned/scheme/register.go
@@ -40,14 +40,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//	import (
-//	  "k8s.io/client-go/kubernetes"
-//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//	)
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
 //
-//	kclientset, _ := kubernetes.NewForConfig(c)
-//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/pkg/platform/defaults.go b/pkg/platform/defaults.go
index 4f7da9f57..8dbf8a29d 100644
--- a/pkg/platform/defaults.go
+++ b/pkg/platform/defaults.go
@@ -53,9 +53,14 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
 	// Reset the state to initial values
 	p.ResyncStatusFullConfig()
 
+	// Apply settings from global integration platform that is bound to this operator
+	if err := applyGlobalPlatformDefaults(ctx, c, p); err != nil {
+		return err
+	}
+
 	// update missing fields in the resource
 	if p.Status.Cluster == "" {
-		log.Debugf("Integration Platform [%s]: setting cluster status", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting cluster status", p.Name, p.Namespace)
 		// determine the kind of cluster the platform is installed into
 		isOpenShift, err := openshift.IsOpenShift(c)
 		switch {
@@ -74,12 +79,12 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
 		} else {
 			p.Status.Build.PublishStrategy = v1.IntegrationPlatformBuildPublishStrategySpectrum
 		}
-		log.Debugf("Integration Platform [%s]: setting publishing strategy %s", p.Namespace, p.Status.Build.PublishStrategy)
+		log.Debugf("Integration Platform %s [%s]: setting publishing strategy %s", p.Name, p.Namespace, p.Status.Build.PublishStrategy)
 	}
 
 	if p.Status.Build.BuildStrategy == "" {
 		p.Status.Build.BuildStrategy = v1.BuildStrategyPod
-		log.Debugf("Integration Platform [%s]: setting build strategy %s", p.Namespace, p.Status.Build.BuildStrategy)
+		log.Debugf("Integration Platform %s [%s]: setting build strategy %s", p.Name, p.Namespace, p.Status.Build.BuildStrategy)
 	}
 
 	err := setPlatformDefaults(p, verbose)
@@ -110,7 +115,7 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
 }
 
 func CreateBuilderServiceAccount(ctx context.Context, client client.Client, p *v1.IntegrationPlatform) error {
-	log.Debugf("Integration Platform [%s]: creating build service account", p.Namespace)
+	log.Debugf("Integration Platform %s [%s]: creating build service account", p.Name, p.Namespace)
 	sa := corev1.ServiceAccount{}
 	key := ctrl.ObjectKey{
 		Name:      BuilderServiceAccount,
@@ -129,7 +134,7 @@ func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPl
 	if p.Status.Cluster == v1.IntegrationPlatformClusterOpenShift &&
 		p.Status.Build.PublishStrategy != v1.IntegrationPlatformBuildPublishStrategyS2I &&
 		p.Status.Build.Registry.Address == "" {
-		log.Debugf("Integration Platform [%s]: setting registry address", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting registry address", p.Name, p.Namespace)
 		// Default to using OpenShift internal container images registry when using a strategy other than S2I
 		p.Status.Build.Registry.Address = "image-registry.openshift-image-registry.svc:5000"
 
@@ -138,12 +143,12 @@ func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPl
 		if err != nil {
 			return err
 		}
-		log.Debugf("Integration Platform [%s]: setting registry certificate authority", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting registry certificate authority", p.Name, p.Namespace)
 		p.Status.Build.Registry.CA = cm.Name
 
 		// Default to using the registry secret that's configured for the builder service account
 		if p.Status.Build.Registry.Secret == "" {
-			log.Debugf("Integration Platform [%s]: setting registry secret", p.Namespace)
+			log.Debugf("Integration Platform %s [%s]: setting registry secret", p.Name, p.Namespace)
 			// Bind the required role to push images to the registry
 			err := createBuilderRegistryRoleBinding(ctx, c, p)
 			if err != nil {
@@ -179,25 +184,142 @@ func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPl
 	return nil
 }
 
+func applyGlobalPlatformDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPlatform) error {
+	operatorNamespace := GetOperatorNamespace()
+	if operatorNamespace != "" && operatorNamespace != p.Namespace {
+		operatorID := defaults.OperatorID()
+		if operatorID != "" {
+			if globalPlatform, err := get(ctx, c, operatorNamespace, operatorID); err != nil && !k8serrors.IsNotFound(err) {
+				return err
+			} else if globalPlatform != nil {
+				applyPlatformSpec(globalPlatform, p)
+				return nil
+			}
+		}
+
+		if globalPlatform, err := findLocal(ctx, c, operatorNamespace, true); err != nil && !k8serrors.IsNotFound(err) {
+			return err
+		} else if globalPlatform != nil {
+			applyPlatformSpec(globalPlatform, p)
+		}
+	}
+
+	return nil
+}
+
+func applyPlatformSpec(source *v1.IntegrationPlatform, target *v1.IntegrationPlatform) {
+	if target.Status.Cluster == "" {
+		target.Status.Cluster = source.Status.Cluster
+	}
+	if target.Status.Profile == "" {
+		log.Debugf("Integration Platform %s [%s]: setting profile", target.Name, target.Namespace)
+		target.Status.Profile = source.Status.Profile
+	}
+
+	if target.Status.Build.PublishStrategy == "" {
+		target.Status.Build.PublishStrategy = source.Status.Build.PublishStrategy
+	}
+	if target.Status.Build.PublishStrategyOptions == nil {
+		log.Debugf("Integration Platform %s [%s]: setting publish strategy options", target.Name, target.Namespace)
+		target.Status.Build.PublishStrategyOptions = source.Status.Build.PublishStrategyOptions
+	}
+	if target.Status.Build.BuildStrategy == "" {
+		target.Status.Build.BuildStrategy = source.Status.Build.BuildStrategy
+	}
+
+	if target.Status.Build.RuntimeVersion == "" {
+		log.Debugf("Integration Platform %s [%s]: setting runtime version", target.Name, target.Namespace)
+		target.Status.Build.RuntimeVersion = source.Status.Build.RuntimeVersion
+	}
+	if target.Status.Build.BaseImage == "" {
+		log.Debugf("Integration Platform %s [%s]: setting base image", target.Name, target.Namespace)
+		target.Status.Build.BaseImage = source.Status.Build.BaseImage
+	}
+
+	if target.Status.Build.Maven.LocalRepository == "" {
+		log.Debugf("Integration Platform %s [%s]: setting local repository", target.Name, target.Namespace)
+		target.Status.Build.Maven.LocalRepository = source.Status.Build.Maven.LocalRepository
+	}
+
+	if len(source.Status.Build.Maven.CLIOptions) > 0 && len(target.Status.Build.Maven.CLIOptions) == 0 {
+		log.Debugf("Integration Platform %s [%s]: setting CLI options", target.Name, target.Namespace)
+		target.Status.Build.Maven.CLIOptions = make([]string, len(source.Status.Build.Maven.CLIOptions))
+		copy(target.Status.Build.Maven.CLIOptions, source.Status.Build.Maven.CLIOptions)
+	}
+
+	if len(source.Status.Build.Maven.Properties) > 0 {
+		log.Debugf("Integration Platform %s [%s]: setting Maven properties", target.Name, target.Namespace)
+		if len(target.Status.Build.Maven.Properties) == 0 {
+			target.Status.Build.Maven.Properties = make(map[string]string, len(source.Status.Build.Maven.Properties))
+		}
+
+		for key, val := range source.Status.Build.Maven.Properties {
+			// only set unknown properties on target
+			if _, ok := target.Status.Build.Maven.Properties[key]; !ok {
+				target.Status.Build.Maven.Properties[key] = val
+			}
+		}
+	}
+
+	if len(source.Status.Build.Maven.Extension) > 0 && len(target.Status.Build.Maven.Extension) == 0 {
+		log.Debugf("Integration Platform %s [%s]: setting Maven extensions", target.Name, target.Namespace)
+		target.Status.Build.Maven.Extension = make([]v1.MavenArtifact, len(source.Status.Build.Maven.Extension))
+		copy(target.Status.Build.Maven.Extension, source.Status.Build.Maven.Extension)
+	}
+
+	if target.Status.Build.Registry.Address == "" && source.Status.Build.Registry.Address != "" {
+		log.Debugf("Integration Platform %s [%s]: setting registry", target.Name, target.Namespace)
+		source.Status.Build.Registry.DeepCopyInto(&target.Status.Build.Registry)
+	}
+
+	if err := target.Status.Traits.Merge(source.Status.Traits); err != nil {
+		log.Errorf(err, "Integration Platform %s [%s]: failed to merge traits", target.Name, target.Namespace)
+	} else if err := target.Status.Traits.Merge(target.Spec.Traits); err != nil {
+		log.Errorf(err, "Integration Platform %s [%s]: failed to merge traits", target.Name, target.Namespace)
+	}
+
+	// Build timeout
+	if target.Status.Build.Timeout == nil {
+		log.Debugf("Integration Platform %s [%s]: setting build timeout", target.Name, target.Namespace)
+		target.Status.Build.Timeout = source.Status.Build.Timeout
+	}
+
+	// Catalog tools build timeout
+	if target.Status.Build.BuildCatalogToolTimeout == nil {
+		log.Debugf("Integration Platform %s [%s]: setting build camel catalog tool timeout", target.Name, target.Namespace)
+		target.Status.Build.BuildCatalogToolTimeout = source.Status.Build.BuildCatalogToolTimeout
+	}
+
+	if target.Status.Build.MaxRunningBuilds <= 0 {
+		log.Debugf("Integration Platform %s [%s]: setting max running builds", target.Name, target.Namespace)
+		target.Status.Build.MaxRunningBuilds = source.Status.Build.MaxRunningBuilds
+	}
+
+	if len(target.Status.Kamelet.Repositories) == 0 {
+		log.Debugf("Integration Platform %s [%s]: setting kamelet repositories", target.Name, target.Namespace)
+		target.Status.Kamelet.Repositories = source.Status.Kamelet.Repositories
+	}
+}
+
 func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 	if p.Status.Build.PublishStrategyOptions == nil {
-		log.Debugf("Integration Platform [%s]: setting publish strategy options", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting publish strategy options", p.Name, p.Namespace)
 		p.Status.Build.PublishStrategyOptions = map[string]string{}
 	}
 	if p.Status.Build.RuntimeVersion == "" {
-		log.Debugf("Integration Platform [%s]: setting runtime version", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting runtime version", p.Name, p.Namespace)
 		p.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
 	}
 	if p.Status.Build.BaseImage == "" {
-		log.Debugf("Integration Platform [%s]: setting base image", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting base image", p.Name, p.Namespace)
 		p.Status.Build.BaseImage = defaults.BaseImage()
 	}
 	if p.Status.Build.Maven.LocalRepository == "" {
-		log.Debugf("Integration Platform [%s]: setting local repository", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting local repository", p.Name, p.Namespace)
 		p.Status.Build.Maven.LocalRepository = defaults.LocalRepository
 	}
 	if len(p.Status.Build.Maven.CLIOptions) == 0 {
-		log.Debugf("Integration Platform [%s]: setting CLI options", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting CLI options", p.Name, p.Namespace)
 		p.Status.Build.Maven.CLIOptions = []string{
 			"-V",
 			"--no-transfer-progress",
@@ -205,7 +327,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 		}
 	}
 	if _, ok := p.Status.Build.PublishStrategyOptions[builder.KanikoPVCName]; !ok {
-		log.Debugf("Integration Platform [%s]: setting publish strategy options", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting publish strategy options", p.Name, p.Namespace)
 		p.Status.Build.PublishStrategyOptions[builder.KanikoPVCName] = p.Name
 	}
 
@@ -221,7 +343,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 			log.Log.Infof("Build timeout minimum unit is sec (configured: %s, truncated: %s)", p.Status.Build.GetTimeout().Duration, d)
 		}
 
-		log.Debugf("Integration Platform [%s]: setting build timeout", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting build timeout", p.Name, p.Namespace)
 		p.Status.Build.Timeout = &metav1.Duration{
 			Duration: d,
 		}
@@ -229,7 +351,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 
 	// Catalog tools build timeout
 	if p.Status.Build.GetBuildCatalogToolTimeout().Duration == 0 {
-		log.Debugf("Integration Platform [%s]: setting default build camel catalog tool timeout (1 minute)", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting default build camel catalog tool timeout (1 minute)", p.Name, p.Namespace)
 		p.Status.Build.BuildCatalogToolTimeout = &metav1.Duration{
 			Duration: 1 * time.Minute,
 		}
@@ -240,13 +362,14 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 			log.Log.Infof("Build catalog tools timeout minimum unit is sec (configured: %s, truncated: %s)", p.Status.Build.GetBuildCatalogToolTimeout().Duration, d)
 		}
 
-		log.Debugf("Integration Platform [%s]: setting build catalog tools timeout", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting build catalog tools timeout", p.Name, p.Namespace)
 		p.Status.Build.BuildCatalogToolTimeout = &metav1.Duration{
 			Duration: d,
 		}
 	}
 
 	if p.Status.Build.MaxRunningBuilds <= 0 {
+		log.Debugf("Integration Platform %s [%s]: setting max running builds", p.Name, p.Namespace)
 		if p.Status.Build.BuildStrategy == v1.BuildStrategyRoutine {
 			p.Status.Build.MaxRunningBuilds = 3
 		} else if p.Status.Build.BuildStrategy == v1.BuildStrategyPod {
@@ -267,7 +390,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 	}
 
 	if len(p.Status.Kamelet.Repositories) == 0 {
-		log.Debugf("Integration Platform [%s]: setting kamelet repositories", p.Namespace)
+		log.Debugf("Integration Platform %s [%s]: setting kamelet repositories", p.Name, p.Namespace)
 		p.Status.Kamelet.Repositories = append(p.Status.Kamelet.Repositories, v1.IntegrationPlatformKameletRepositorySpec{
 			URI: repository.DefaultRemoteRepository,
 		})
@@ -287,13 +410,13 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 func setStatusAdditionalInfo(platform *v1.IntegrationPlatform) {
 	platform.Status.Info = make(map[string]string)
 
-	log.Debugf("Integration Platform [%s]: setting build publish strategy", platform.Namespace)
+	log.Debugf("Integration Platform %s [%s]: setting build publish strategy", platform.Name, platform.Namespace)
 	if platform.Spec.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategyBuildah {
 		platform.Status.Info["buildahVersion"] = defaults.BuildahVersion
 	} else if platform.Spec.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategyKaniko {
 		platform.Status.Info["kanikoVersion"] = defaults.KanikoVersion
 	}
-	log.Debugf("Integration Platform [%s]: setting status info", platform.Namespace)
+	log.Debugf("Integration Platform %s [%s]: setting status info", platform.Name, platform.Namespace)
 	platform.Status.Info["goVersion"] = runtime.Version()
 	platform.Status.Info["goOS"] = runtime.GOOS
 	platform.Status.Info["gitCommit"] = defaults.GitCommit
diff --git a/pkg/util/defaults/defaults.go b/pkg/util/defaults/defaults.go
index c20ed0e4f..08f99b1fa 100644
--- a/pkg/util/defaults/defaults.go
+++ b/pkg/util/defaults/defaults.go
@@ -26,7 +26,7 @@ const (
 	Version = "2.0.0-SNAPSHOT"
 
 	// DefaultRuntimeVersion --
-	DefaultRuntimeVersion = "3.20.1-SNAPSHOT"
+	DefaultRuntimeVersion = "2.16.0-SNAPSHOT"
 
 	// BuildahVersion --
 	BuildahVersion = "1.23.3"


Test patch:
diff --git a/e2e/commonwithcustominstall/local_platform_test.go b/e2e/commonwithcustominstall/local_platform_test.go
new file mode 100644
index 000000000..98f99924c
--- /dev/null
+++ b/e2e/commonwithcustominstall/local_platform_test.go
@@ -0,0 +1,103 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+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 commonwithcustominstall
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+
+	. "github.com/apache/camel-k/v2/e2e/support"
+	v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+	traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
+)
+
+func TestLocalPlatform(t *testing.T) {
+	WithNewTestNamespace(t, func(ns string) {
+		operatorID := "camel-k-platform-local"
+		Expect(KamelInstallWithID(operatorID, ns, "--global", "--force").Execute()).To(Succeed())
+		Eventually(PlatformPhase(ns), TestTimeoutMedium).Should(Equal(v1.IntegrationPlatformPhaseReady))
+
+		pl := Platform(ns)()
+		pl.Spec.Build.Maven.Properties = make(map[string]string)
+		pl.Spec.Build.Maven.Properties["build-global-prop1"] = "build-global-value1"
+		// set maximum number of running builds
+		pl.Spec.Build.MaxRunningBuilds = 1
+		if err := TestClient().Update(TestContext, pl); err != nil {
+			t.Error(err)
+			t.FailNow()
+		}
+
+		Eventually(PlatformHas(ns, func(pl *v1.IntegrationPlatform) bool {
+			return pl.Status.Build.MaxRunningBuilds == 1
+		}), TestTimeoutMedium).Should(BeTrue())
+
+		WithNewTestNamespace(t, func(ns1 string) {
+			localPlatform := v1.NewIntegrationPlatform(ns1, operatorID)
+			localPlatform.Spec.Build.Maven.Properties = make(map[string]string)
+			localPlatform.Spec.Build.Maven.Properties["build-local-prop1"] = "build-local-value1"
+			localPlatform.SetOperatorID(operatorID)
+
+			localPlatform.Spec.Traits.Container = &traitv1.ContainerTrait{
+				LimitCPU: "0.1",
+			}
+
+			if err := TestClient().Create(TestContext, &localPlatform); err != nil {
+				t.Error(err)
+				t.FailNow()
+			}
+			Eventually(PlatformPhase(ns1), TestTimeoutMedium).Should(Equal(v1.IntegrationPlatformPhaseReady))
+			Eventually(PlatformHas(ns1, func(pl *v1.IntegrationPlatform) bool {
+				return pl.Status.Cluster != ""
+			}), TestTimeoutShort).Should(BeTrue())
+
+			Eventually(PlatformHas(ns1, func(pl *v1.IntegrationPlatform) bool {
+				return pl.Status.Build.MaxRunningBuilds == 1
+			}), TestTimeoutShort).Should(BeTrue())
+
+			local := Platform(ns1)()
+			Expect(local.Status.Build.PublishStrategy).To(Equal(pl.Status.Build.PublishStrategy))
+			Expect(local.Status.Build.BuildStrategy).To(Equal(pl.Status.Build.BuildStrategy))
+			Expect(local.Status.Build.Maven.LocalRepository).To(Equal(pl.Status.Build.Maven.LocalRepository))
+			Expect(local.Status.Build.Maven.CLIOptions).To(ContainElements(pl.Status.Build.Maven.CLIOptions))
+			Expect(local.Status.Build.Maven.Extension).To(BeEmpty())
+			Expect(local.Status.Build.Maven.Properties).To(HaveLen(2))
+			Expect(local.Status.Build.Maven.Properties["build-global-prop1"]).To(Equal("build-global-value1"))
+			Expect(local.Status.Build.Maven.Properties["build-local-prop1"]).To(Equal("build-local-value1"))
+
+			Expect(KamelRunWithID(operatorID, ns1, "--name", "local-integration", "files/yaml.yaml").Execute()).To(Succeed())
+			Eventually(IntegrationPod(ns1, "local-integration"), TestTimeoutMedium).Should(Not(BeNil()))
+			Eventually(IntegrationPodHas(ns1, "local-integration", func(pod *corev1.Pod) bool {
+				if len(pod.Spec.Containers) != 1 {
+					return false
+				}
+				cpuLimits := pod.Spec.Containers[0].Resources.Limits.Cpu()
+				return cpuLimits != nil && cpuLimits.AsApproximateFloat64() > 0
+			}), TestTimeoutShort).Should(BeTrue())
+
+			// Clean up
+			Expect(Kamel("delete", "--all", "-n", ns1).Execute()).To(Succeed())
+		})
+	})
+}
diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go
index 5cd425710..c70725ff0 100644
--- a/e2e/support/test_support.go
+++ b/e2e/support/test_support.go
@@ -1799,6 +1799,16 @@ func SelectedPlatformPhase(ns string, name string) func() v1.IntegrationPlatform
 	}
 }
 
+func PlatformHas(ns string, predicate func(pl *v1.IntegrationPlatform) bool) func() bool {
+	return func() bool {
+		pl := Platform(ns)()
+		if pl == nil {
+			return false
+		}
+		return predicate(pl)
+	}
+}
+
 func PlatformCondition(
 	ns string,
 	conditionType v1.IntegrationPlatformConditionType,
diff --git a/pkg/platform/defaults_test.go b/pkg/platform/defaults_test.go
new file mode 100644
index 000000000..d5754ba1c
--- /dev/null
+++ b/pkg/platform/defaults_test.go
@@ -0,0 +1,187 @@
+/*
+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 platform
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
+	"github.com/apache/camel-k/v2/pkg/util/test"
+)
+
+func TestApplyGlobalPlatformSpec(t *testing.T) {
+	global := v1.IntegrationPlatform{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      DefaultPlatformName,
+			Namespace: "ns",
+		},
+		Spec: v1.IntegrationPlatformSpec{
+			Build: v1.IntegrationPlatformBuildSpec{
+				BuildStrategy: v1.BuildStrategyRoutine,
+				Maven: v1.MavenSpec{
+					Properties: map[string]string{
+						"global_prop1": "global_value1",
+						"global_prop2": "global_value2",
+					},
+				},
+			},
+			Traits: v1.Traits{
+				Logging: &trait.LoggingTrait{
+					Level: "DEBUG",
+				},
+				Container: &trait.ContainerTrait{
+					ImagePullPolicy: corev1.PullAlways,
+					LimitCPU:        "0.1",
+				},
+			},
+			Cluster: v1.IntegrationPlatformClusterOpenShift,
+			Profile: v1.TraitProfileOpenShift,
+		},
+	}
+
+	c, err := test.NewFakeClient(&global)
+	assert.Nil(t, err)
+
+	err = ConfigureDefaults(context.TODO(), c, &global, false)
+	assert.Nil(t, err)
+
+	ip := v1.IntegrationPlatform{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "local-camel-k",
+			Namespace: "ns",
+		},
+	}
+
+	ip.ResyncStatusFullConfig()
+
+	applyPlatformSpec(&global, &ip)
+
+	assert.Equal(t, v1.IntegrationPlatformClusterOpenShift, ip.Status.Cluster)
+	assert.Equal(t, v1.TraitProfileOpenShift, ip.Status.Profile)
+	assert.Equal(t, v1.BuildStrategyRoutine, ip.Status.Build.BuildStrategy)
+
+	assert.True(t, ip.Status.Build.MaxRunningBuilds == 3) // default for build strategy routine
+
+	assert.Equal(t, len(global.Status.Build.Maven.CLIOptions), len(ip.Status.Build.Maven.CLIOptions))
+	assert.Equal(t, global.Status.Build.Maven.CLIOptions, ip.Status.Build.Maven.CLIOptions)
+
+	assert.NotNil(t, ip.Status.Traits)
+	assert.NotNil(t, ip.Status.Traits.Logging)
+	assert.Equal(t, "DEBUG", ip.Status.Traits.Logging.Level)
+	assert.NotNil(t, ip.Status.Traits.Container)
+	assert.Equal(t, corev1.PullAlways, ip.Status.Traits.Container.ImagePullPolicy)
+	assert.Equal(t, "0.1", ip.Status.Traits.Container.LimitCPU)
+
+	assert.Equal(t, 2, len(ip.Status.Build.Maven.Properties))
+	assert.Equal(t, "global_value1", ip.Status.Build.Maven.Properties["global_prop1"])
+	assert.Equal(t, "global_value2", ip.Status.Build.Maven.Properties["global_prop2"])
+}
+
+func TestRetainLocalPlatformSpec(t *testing.T) {
+	global := v1.IntegrationPlatform{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      DefaultPlatformName,
+			Namespace: "ns",
+		},
+		Spec: v1.IntegrationPlatformSpec{
+			Build: v1.IntegrationPlatformBuildSpec{
+				BuildStrategy: v1.BuildStrategyRoutine,
+				Maven: v1.MavenSpec{
+					Properties: map[string]string{
+						"global_prop1": "global_value1",
+						"global_prop2": "global_value2",
+					},
+				},
+			},
+			Traits: v1.Traits{
+				Logging: &trait.LoggingTrait{
+					Level: "DEBUG",
+				},
+				Container: &trait.ContainerTrait{
+					ImagePullPolicy: corev1.PullIfNotPresent,
+					LimitCPU:        "0.1",
+				},
+			},
+			Cluster: v1.IntegrationPlatformClusterOpenShift,
+			Profile: v1.TraitProfileOpenShift,
+		},
+	}
+
+	c, err := test.NewFakeClient(&global)
+	assert.Nil(t, err)
+
+	err = ConfigureDefaults(context.TODO(), c, &global, false)
+	assert.Nil(t, err)
+
+	ip := v1.IntegrationPlatform{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "local-camel-k",
+			Namespace: "ns",
+		},
+		Spec: v1.IntegrationPlatformSpec{
+			Build: v1.IntegrationPlatformBuildSpec{
+				BuildStrategy:    v1.BuildStrategyPod,
+				MaxRunningBuilds: 1,
+				Maven: v1.MavenSpec{
+					Properties: map[string]string{
+						"local_prop1":  "local_value1",
+						"global_prop2": "local_value2",
+					},
+				},
+			},
+			Traits: v1.Traits{
+				Container: &trait.ContainerTrait{
+					ImagePullPolicy: corev1.PullAlways,
+				},
+			},
+			Cluster: v1.IntegrationPlatformClusterKubernetes,
+			Profile: v1.TraitProfileKnative,
+		},
+	}
+
+	ip.ResyncStatusFullConfig()
+
+	applyPlatformSpec(&global, &ip)
+
+	assert.Equal(t, v1.IntegrationPlatformClusterKubernetes, ip.Status.Cluster)
+	assert.Equal(t, v1.TraitProfileKnative, ip.Status.Profile)
+	assert.Equal(t, v1.BuildStrategyPod, ip.Status.Build.BuildStrategy)
+
+	assert.True(t, ip.Status.Build.MaxRunningBuilds == 1)
+
+	assert.Equal(t, len(global.Status.Build.Maven.CLIOptions), len(ip.Status.Build.Maven.CLIOptions))
+	assert.Equal(t, global.Status.Build.Maven.CLIOptions, ip.Status.Build.Maven.CLIOptions)
+
+	assert.NotNil(t, ip.Status.Traits)
+	assert.NotNil(t, ip.Status.Traits.Logging)
+	assert.Equal(t, "DEBUG", ip.Status.Traits.Logging.Level)
+	assert.NotNil(t, ip.Status.Traits.Container)
+	assert.Equal(t, corev1.PullAlways, ip.Status.Traits.Container.ImagePullPolicy)
+	assert.Equal(t, "0.1", ip.Status.Traits.Container.LimitCPU)
+
+	assert.Equal(t, 3, len(ip.Status.Build.Maven.Properties))
+	assert.Equal(t, "global_value1", ip.Status.Build.Maven.Properties["global_prop1"])
+	assert.Equal(t, "local_value2", ip.Status.Build.Maven.Properties["global_prop2"])
+	assert.Equal(t, "local_value1", ip.Status.Build.Maven.Properties["local_prop1"])
+}