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: