You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2023/05/11 07:25:55 UTC

[camel-k] branch release-1.10.x updated: fix: Consistent integration platform settings hierarchy (backport #4317)

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

pcongiusti pushed a commit to branch release-1.10.x
in repository https://gitbox.apache.org/repos/asf/camel-k.git


The following commit(s) were added to refs/heads/release-1.10.x by this push:
     new b479dbe9e fix: Consistent integration platform settings hierarchy (backport #4317)
b479dbe9e is described below

commit b479dbe9e9c5cb198fc0d8e76e2138ee38ad6814
Author: Christoph Deppisch <cd...@redhat.com>
AuthorDate: Thu May 4 12:01:08 2023 +0200

    fix: Consistent integration platform settings hierarchy (backport #4317)
    
    - Make sure to apply global integration platform settings when creating namespace local platform
    - Always derive local platform from operator global platform settings
    - Global platform settings resist when the setting is not explicitly overwritten in the given user namespaced integration platform
---
 e2e/global/common/local_platform_test.go | 103 +++++++++++++++++
 e2e/support/test_support.go              |  20 +++-
 pkg/platform/defaults.go                 | 137 ++++++++++++++++++++++
 pkg/platform/defaults_test.go            | 187 +++++++++++++++++++++++++++++++
 4 files changed, 443 insertions(+), 4 deletions(-)

diff --git a/e2e/global/common/local_platform_test.go b/e2e/global/common/local_platform_test.go
new file mode 100644
index 000000000..86f9799aa
--- /dev/null
+++ b/e2e/global/common/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 common
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+
+	. "github.com/apache/camel-k/e2e/support"
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	traitv1 "github.com/apache/camel-k/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 f9d23f1b3..5ad93021a 100644
--- a/e2e/support/test_support.go
+++ b/e2e/support/test_support.go
@@ -91,7 +91,7 @@ const kubeConfigEnvVar = "KUBECONFIG"
 
 var TestTimeoutShort = 1 * time.Minute
 var TestTimeoutMedium = 5 * time.Minute
-var TestTimeoutLong = 10 * time.Minute
+var TestTimeoutLong = 15 * time.Minute
 
 // TestTimeoutVeryLong should be used only for testing native builds.
 var TestTimeoutVeryLong = 60 * time.Minute
@@ -112,6 +112,7 @@ func setTestLocus(t *testing.T) {
 // using the test locus to error out and fail now.
 func failTest(err error) {
 	if testLocus != nil {
+		testLocus.Helper()
 		testLocus.Error(err)
 		testLocus.FailNow()
 	} else {
@@ -390,9 +391,9 @@ func MakeWithContext(ctx context.Context, rule string, args ...string) *exec.Cmd
 	return exec.Command("make", args...)
 }
 
-/*
-	Curryied utility functions for testing
-*/
+// =============================================================================
+// Curried utility functions for testing
+// =============================================================================
 
 func IntegrationLogs(ns, name string) func() string {
 	return func() string {
@@ -455,6 +456,7 @@ func StructuredLogs(ns, podName string, options corev1.PodLogOptions, ignorePars
 		err := json.Unmarshal([]byte(t), &entry)
 		if err != nil {
 			if ignoreParseErrors {
+				fmt.Printf("Warning: Ignoring parse error for logging line: %q\n", t)
 				continue
 			} else {
 				log.Errorf(err, "Unable to parse structured content: %s", t)
@@ -1432,6 +1434,16 @@ func PlatformPhase(ns string) func() v1.IntegrationPlatformPhase {
 	}
 }
 
+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 PlatformProfile(ns string) func() v1.TraitProfile {
 	return func() v1.TraitProfile {
 		p := Platform(ns)()
diff --git a/pkg/platform/defaults.go b/pkg/platform/defaults.go
index b38541210..c71571e37 100644
--- a/pkg/platform/defaults.go
+++ b/pkg/platform/defaults.go
@@ -53,8 +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 [%s]: setting cluster status", p.Name, p.Namespace)
 		// determine the kind of cluster the platform is installed into
 		isOpenShift, err := openshift.IsOpenShift(c)
 		switch {
@@ -68,6 +74,7 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
 	}
 
 	if 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.Cluster == v1.IntegrationPlatformClusterOpenShift {
 			p.Status.Build.PublishStrategy = v1.IntegrationPlatformBuildPublishStrategyS2I
 		} else {
@@ -76,6 +83,7 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
 	}
 
 	if p.Status.Build.BuildStrategy == "" {
+		log.Debugf("Integration Platform %s [%s]: setting build strategy %s", p.Name, p.Namespace, p.Status.Build.BuildStrategy)
 		// Use the fastest strategy that they support (routine when possible)
 		if p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategyS2I ||
 			p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategySpectrum {
@@ -114,6 +122,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 [%s]: creating build service account", p.Name, p.Namespace)
 	sa := corev1.ServiceAccount{}
 	key := ctrl.ObjectKey{
 		Name:      BuilderServiceAccount,
@@ -132,6 +141,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 [%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"
 
@@ -140,10 +150,12 @@ func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPl
 		if err != nil {
 			return err
 		}
+		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 [%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 {
@@ -174,23 +186,141 @@ func configureRegistry(ctx context.Context, c client.Client, p *v1.IntegrationPl
 			p.Status.Build.Registry.Address = *address
 		}
 	}
+
+	log.Debugf("Final Registry Address: %s", p.Status.Build.Registry.Address)
 	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
+	}
+
+	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 [%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 [%s]: setting runtime version", p.Name, p.Namespace)
 		p.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
 	}
 	if p.Status.Build.BaseImage == "" {
+		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 [%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 [%s]: setting CLI options", p.Name, p.Namespace)
 		p.Status.Build.Maven.CLIOptions = []string{
 			"-V",
 			"--no-transfer-progress",
@@ -198,6 +328,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 		}
 	}
 	if _, ok := p.Status.Build.PublishStrategyOptions[builder.KanikoPVCName]; !ok {
+		log.Debugf("Integration Platform %s [%s]: setting publish strategy options", p.Name, p.Namespace)
 		p.Status.Build.PublishStrategyOptions[builder.KanikoPVCName] = p.Name
 	}
 
@@ -208,6 +339,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 [%s]: setting build timeout", p.Name, p.Namespace)
 		p.Status.Build.Timeout = &metav1.Duration{
 			Duration: d,
 		}
@@ -219,6 +351,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 	}
 
 	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 {
@@ -239,6 +372,7 @@ func setPlatformDefaults(p *v1.IntegrationPlatform, verbose bool) error {
 	}
 
 	if len(p.Status.Kamelet.Repositories) == 0 {
+		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,
 		})
@@ -257,11 +391,14 @@ 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 [%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 [%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/platform/defaults_test.go b/pkg/platform/defaults_test.go
new file mode 100644
index 000000000..2639420d5
--- /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/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/apis/camel/v1/trait"
+	"github.com/apache/camel-k/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"])
+}