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

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


The following commit(s) were added to refs/heads/main by this push:
     new 31911d5da Fix #6776: Default IntegrationProfile support (#6855)
31911d5da is described below

commit 31911d5da90456bfeb3d9748ddad20c300cd200e
Author: Keerthan Poojary <[email protected]>
AuthorDate: Mon Nov 9 20:36:00 2026 +0530

    Fix #6776: Default IntegrationProfile support (#6855)
    
    * Fix #6776: Default IntegrationProfile support
    
    * Fix #6776: Add namespaced RBAC and list/watch permissions for namespaces, 
update docs
---
 .../pages/configuration/integrationprofiles.adoc   |  64 +++++-
 helm/camel-k/templates/rbacs-descoped.yaml         |   8 +
 helm/camel-k/templates/rbacs-namespaced.yaml       |   8 +
 pkg/apis/camel/v1/common_types.go                  |   2 +
 pkg/platform/profile.go                            |  38 +++-
 pkg/platform/profile_test.go                       | 214 +++++++++++++++++++++
 .../rbac/descoped/operator-cluster-role.yaml       |   8 +
 .../config/rbac/namespaced/operator-role.yaml      |   8 +
 8 files changed, 343 insertions(+), 7 deletions(-)

diff --git a/docs/modules/ROOT/pages/configuration/integrationprofiles.adoc 
b/docs/modules/ROOT/pages/configuration/integrationprofiles.adoc
index 013896558..22a988564 100644
--- a/docs/modules/ROOT/pages/configuration/integrationprofiles.adoc
+++ b/docs/modules/ROOT/pages/configuration/integrationprofiles.adoc
@@ -1,8 +1,17 @@
 = Integration Profiles
 
-Admin users may add an `IntegrationProfile` resource to any namespace. The 
profile holds custom settings which can be applied to all Integrations.
+Admin users may add an `IntegrationProfile` resource to any namespace. The 
profile holds custom settings which can be applied to Integrations.
 
-The profile must be explicitly selected by an annotation referencing the 
integration profile name (any resource belonging to the "camel.apache.org" 
group can select a particular profile configuration).
+== Profile Resolution Precedence
+
+The operator resolves the `IntegrationProfile` to apply to an `Integration` 
(or `Pipe`) using the following order of precedence:
+
+1. **User-provided profile (via resource annotation):** The resource specifies 
the profile name in the `camel.apache.org/integration-profile.id` annotation.
+2. **Namespace-annotated profile (via namespace annotation):** If the resource 
has no annotation, the operator checks if the Kubernetes `Namespace` has a 
`camel.apache.org/integration-profile.id` annotation designating the default 
profile for that namespace.
+3. **`default` profile convention:** If neither the resource nor the namespace 
is annotated, the operator looks for an `IntegrationProfile` named `default` in 
the same namespace.
+4. **Default operator configuration:** If no profile is found, the operator 
falls back to its baseline configuration configured via operator environment 
variables.
+
+== Explicit Selection (Resource Annotation)
 
 To specify which profile should be used for an Integration, the resource can 
be annotated like in the following example:
 
@@ -36,7 +45,56 @@ spec:
 # ...
 ----
 
-The selection of a IntegrationProfile enables new configuration scenarios, for 
example, sharing global configuration options for groups of Integrations.
+== Namespace Default (Namespace Annotation)
+
+Administrators can set a default profile for all integrations in a namespace 
by annotating the Kubernetes `Namespace`:
+
+[source,yaml]
+----
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: my-team
+  annotations:
+    camel.apache.org/integration-profile.id: my-team-profile
+----
+
+Any Integration created in `my-team` without an explicit profile annotation 
will automatically use `my-team-profile`.
+
+NOTE: The administrator must also create the `IntegrationProfile` named 
`my-team-profile` in that namespace. If it does not exist, the operator will 
fail to resolve the profile and report an error. For example:
+
+[source,yaml]
+----
+kind: IntegrationProfile
+apiVersion: camel.apache.org/v1
+metadata:
+  name: my-team-profile
+  namespace: my-team
+spec:
+  traits:
+    camel:
+      runtimeProvider: plain-quarkus
+----
+
+== Default Profile Convention
+
+Alternatively, an administrator can simply create an `IntegrationProfile` 
named `default` in the namespace:
+
+[source,yaml]
+----
+kind: IntegrationProfile
+apiVersion: camel.apache.org/v1
+metadata:
+  name: default
+spec:
+  traits:
+    camel:
+      runtimeProvider: plain-quarkus
+----
+
+Any Integration deployed into this namespace without an explicit annotation 
will automatically use this `default` profile.
+
+The selection of an IntegrationProfile enables new configuration scenarios, 
for example, sharing global configuration options for groups of Integrations.
 
 == Security
 
diff --git a/helm/camel-k/templates/rbacs-descoped.yaml 
b/helm/camel-k/templates/rbacs-descoped.yaml
index 2a57524c2..f52bdea60 100644
--- a/helm/camel-k/templates/rbacs-descoped.yaml
+++ b/helm/camel-k/templates/rbacs-descoped.yaml
@@ -83,6 +83,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
diff --git a/helm/camel-k/templates/rbacs-namespaced.yaml 
b/helm/camel-k/templates/rbacs-namespaced.yaml
index 2b3bbbb9c..f4e2c5a83 100644
--- a/helm/camel-k/templates/rbacs-namespaced.yaml
+++ b/helm/camel-k/templates/rbacs-namespaced.yaml
@@ -83,6 +83,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
diff --git a/pkg/apis/camel/v1/common_types.go 
b/pkg/apis/camel/v1/common_types.go
index 521641624..6f281d06a 100644
--- a/pkg/apis/camel/v1/common_types.go
+++ b/pkg/apis/camel/v1/common_types.go
@@ -36,6 +36,8 @@ const (
        PlatformSelectorAnnotation = "camel.apache.org/platform.id"
        // IntegrationProfileAnnotation integration profile id annotation label.
        IntegrationProfileAnnotation = "camel.apache.org/integration-profile.id"
+       // DefaultIntegrationProfileName is the default name for an 
IntegrationProfile in a namespace.
+       DefaultIntegrationProfileName = "default"
        // IntegrationProfileNamespaceAnnotation integration profile namespace 
annotation label.
        // NOTE: this is an internal annotation only valid for IntegrationKits 
and Builds.
        IntegrationProfileNamespaceAnnotation = 
"camel.apache.org/integration-profile.namespace"
diff --git a/pkg/platform/profile.go b/pkg/platform/profile.go
index 0d3642494..fea6cc27c 100644
--- a/pkg/platform/profile.go
+++ b/pkg/platform/profile.go
@@ -20,11 +20,12 @@ package platform
 import (
        "context"
 
-       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
-       "github.com/apache/camel-k/v2/pkg/util/kubernetes"
-
+       corev1 "k8s.io/api/core/v1"
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
        k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       "github.com/apache/camel-k/v2/pkg/util/kubernetes"
 )
 
 // ApplyIntegrationProfile resolves integration profile from given object.
@@ -38,11 +39,40 @@ func ApplyIntegrationProfile(ctx context.Context, c 
k8sclient.Reader, o k8sclien
 }
 
 // findIntegrationProfile finds profile from given resource annotations
-// and resolves the profile in given resource namespace.
+// and resolves the profile in given resource namespace, falling back to
+// namespace annotation, "default" profile, or operator defaults.
 func findIntegrationProfile(ctx context.Context, c k8sclient.Reader, o 
k8sclient.Object) (*v1.IntegrationProfile, error) {
+       // 1. User provided profile (via annotation on the resource)
        if profileResource := kubernetes.GetIntegrationProfileResource(o); 
profileResource != "" {
                return kubernetes.GetIntegrationProfile(ctx, c, profileResource)
        }
 
+       namespace := o.GetNamespace()
+
+       // 2. Namespace annotated profile (via namespace annotation)
+       if namespace != "" {
+               var ns corev1.Namespace
+               err := c.Get(ctx, k8sclient.ObjectKey{Name: namespace}, &ns)
+               if err == nil {
+                       if profileName := 
v1.GetAnnotation(v1.IntegrationProfileAnnotation, &ns); profileName != "" {
+                               return kubernetes.GetIntegrationProfile(ctx, c, 
namespace+"/"+profileName)
+                       }
+               } else if !k8serrors.IsNotFound(err) && 
!k8serrors.IsForbidden(err) && !k8serrors.IsUnauthorized(err) {
+                       return nil, err
+               }
+       }
+
+       // 3. "default" profile in the same namespace
+       if namespace != "" {
+               profile, err := kubernetes.GetIntegrationProfile(ctx, c, 
namespace+"/"+v1.DefaultIntegrationProfileName)
+               if err == nil {
+                       return profile, nil
+               }
+               if !k8serrors.IsNotFound(err) {
+                       return nil, err
+               }
+       }
+
+       // 4. Default operator configuration (no profile found)
        return nil, nil
 }
diff --git a/pkg/platform/profile_test.go b/pkg/platform/profile_test.go
index 4f5cdcb4c..6e63bce06 100644
--- a/pkg/platform/profile_test.go
+++ b/pkg/platform/profile_test.go
@@ -23,6 +23,8 @@ import (
 
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       k8serrors "k8s.io/apimachinery/pkg/api/errors"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
        v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
@@ -134,3 +136,215 @@ func TestFindOrApplyIntegrationProfileError(t *testing.T) 
{
        require.Error(t, err)
        assert.Equal(t, "wrong format for IntegrationProfile, expected 
profile-namespace/profile-id", err.Error())
 }
+
+func TestFindIntegrationProfile_UserAnnotationTakesPrecedence(t *testing.T) {
+       userProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "user-profile",
+                       Namespace: "ns",
+               },
+       }
+       nsProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "ns-profile",
+                       Namespace: "ns",
+               },
+       }
+       defaultProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "default",
+                       Namespace: "ns",
+               },
+       }
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+                       Annotations: map[string]string{
+                               v1.IntegrationProfileAnnotation: "ns-profile",
+                       },
+               },
+       }
+
+       c, err := internal.NewFakeClient(&userProfile, &nsProfile, 
&defaultProfile, &namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+                       Annotations: map[string]string{
+                               v1.IntegrationProfileAnnotation: "user-profile",
+                       },
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.NoError(t, err)
+       require.NotNil(t, found)
+       assert.Equal(t, "user-profile", found.Name)
+}
+
+func 
TestFindIntegrationProfile_NamespaceAnnotationTakesPrecedenceOverDefault(t 
*testing.T) {
+       nsProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "ns-profile",
+                       Namespace: "ns",
+               },
+       }
+       defaultProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "default",
+                       Namespace: "ns",
+               },
+       }
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+                       Annotations: map[string]string{
+                               v1.IntegrationProfileAnnotation: "ns-profile",
+                       },
+               },
+       }
+
+       c, err := internal.NewFakeClient(&nsProfile, &defaultProfile, 
&namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.NoError(t, err)
+       require.NotNil(t, found)
+       assert.Equal(t, "ns-profile", found.Name)
+}
+
+func TestFindIntegrationProfile_DefaultProfileUsedWhenNoAnnotations(t 
*testing.T) {
+       defaultProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "default",
+                       Namespace: "ns",
+               },
+       }
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+               },
+       }
+
+       c, err := internal.NewFakeClient(&defaultProfile, &namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.NoError(t, err)
+       require.NotNil(t, found)
+       assert.Equal(t, "default", found.Name)
+}
+
+func TestFindIntegrationProfile_OperatorDefaultWhenNoProfile(t *testing.T) {
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+               },
+       }
+
+       c, err := internal.NewFakeClient(&namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.NoError(t, err)
+       assert.Nil(t, found)
+}
+
+func TestFindIntegrationProfile_MissingUserAnnotatedProfileReturnsError(t 
*testing.T) {
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+               },
+       }
+
+       c, err := internal.NewFakeClient(&namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+                       Annotations: map[string]string{
+                               v1.IntegrationProfileAnnotation: 
"missing-user-profile",
+                       },
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.Error(t, err)
+       assert.True(t, k8serrors.IsNotFound(err))
+       assert.Nil(t, found)
+}
+
+func TestFindIntegrationProfile_MissingNamespaceAnnotatedProfileReturnsError(t 
*testing.T) {
+       namespace := corev1.Namespace{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: "ns",
+                       Annotations: map[string]string{
+                               v1.IntegrationProfileAnnotation: 
"missing-ns-profile",
+                       },
+               },
+       }
+
+       c, err := internal.NewFakeClient(&namespace)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+               },
+       }
+
+       found, err := findIntegrationProfile(context.TODO(), c, &integration)
+       require.Error(t, err)
+       assert.True(t, k8serrors.IsNotFound(err))
+       assert.Nil(t, found)
+}
+
+func TestApplyIntegrationProfile(t *testing.T) {
+       defaultProfile := v1.IntegrationProfile{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "default",
+                       Namespace: "ns",
+               },
+       }
+
+       c, err := internal.NewFakeClient(&defaultProfile)
+       require.NoError(t, err)
+
+       integration := v1.Integration{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "ns",
+               },
+       }
+
+       found, err := ApplyIntegrationProfile(context.TODO(), c, &integration)
+       require.NoError(t, err)
+       require.NotNil(t, found)
+       assert.Equal(t, "default", found.Name)
+}
diff --git a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml 
b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
index a21bef406..5b3dfe5f6 100644
--- a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
+++ b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
@@ -83,6 +83,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
diff --git a/pkg/resources/config/rbac/namespaced/operator-role.yaml 
b/pkg/resources/config/rbac/namespaced/operator-role.yaml
index 0999ba5b0..0511643fe 100644
--- a/pkg/resources/config/rbac/namespaced/operator-role.yaml
+++ b/pkg/resources/config/rbac/namespaced/operator-role.yaml
@@ -83,6 +83,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:

Reply via email to