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 aed5f91cc fix(affinity): implement filtering of node affinity labels
based on allowed keys
aed5f91cc is described below
commit aed5f91cc83e5833ede99bb1a23c8866969f8ef0
Author: Harsh Mehta <[email protected]>
AuthorDate: Thu Jun 18 15:44:30 2026 +0530
fix(affinity): implement filtering of node affinity labels based on allowed
keys
Signed-off-by: Harsh Mehta <[email protected]>
---
docs/modules/ROOT/pages/installation/builds.adoc | 4 ++
docs/modules/traits/pages/affinity.adoc | 2 +
pkg/platform/env_platform.go | 21 +++++++++
pkg/platform/env_platform_test.go | 26 +++++++++++
pkg/trait/affinity.go | 39 ++++++++++++++++
pkg/trait/affinity_test.go | 59 ++++++++++++++++++++++++
6 files changed, 151 insertions(+)
diff --git a/docs/modules/ROOT/pages/installation/builds.adoc
b/docs/modules/ROOT/pages/installation/builds.adoc
index 86d1d62b6..965403e14 100644
--- a/docs/modules/ROOT/pages/installation/builds.adoc
+++ b/docs/modules/ROOT/pages/installation/builds.adoc
@@ -47,6 +47,10 @@ Here a quick resume of the parameters you can configure as
environment variables
| Maximum number of builds that can run concurrently.
| `3` if build strategy is `routine`, `10` if `pod`
+| AFFINITY_NODE_LABELS_ALLOWED_KEYS
+| Comma-separated list of label keys that CR authors are permitted to use in
`affinity.nodeAffinityLabels`. When unset or empty all keys are accepted.
Expressions whose key is not in the list are dropped and an info message is
logged. Example: `kubernetes.io/hostname,topology.kubernetes.io/zone`.
+|
+
| BUILDER_TASKS_ENABLED
| Controls whether CR authors are permitted to inject custom pipeline tasks
via the `builder.tasks` trait. Set to `false` to disable custom task injection
for all integrations managed by this operator. When unset or set to any value
other than `false`, custom tasks are allowed (default behavior).
| `true`
diff --git a/docs/modules/traits/pages/affinity.adoc
b/docs/modules/traits/pages/affinity.adoc
index c32543468..288b261b0 100755
--- a/docs/modules/traits/pages/affinity.adoc
+++ b/docs/modules/traits/pages/affinity.adoc
@@ -85,3 +85,5 @@ $ kamel run -t
affinity.pod-anti-affinity-labels="camel.apache.org/integration"
----
More information can be found in the official Kubernetes documentation about
https://kubernetes.io/docs/concepts/configuration/assign-pod-node/[Assigning
Pods to Nodes].
+
+NOTE: Operators can restrict which label keys CR authors are permitted to use
in `affinity.nodeAffinityLabels` by setting the
`AFFINITY_NODE_LABELS_ALLOWED_KEYS` environment variable on the operator
deployment to a comma-separated list of allowed keys (e.g.
`kubernetes.io/hostname,topology.kubernetes.io/zone`). Expressions whose key is
not in the list are dropped and an info message is logged. When the variable is
unset or empty, all keys are accepted (default behavior). See build enviro [...]
diff --git a/pkg/platform/env_platform.go b/pkg/platform/env_platform.go
index 04589b6bc..883e46e6a 100644
--- a/pkg/platform/env_platform.go
+++ b/pkg/platform/env_platform.go
@@ -173,6 +173,27 @@ func publishStrategy()
v1.IntegrationPlatformBuildPublishStrategy {
return DefaultPublishStrategy
}
+// AffinityNodeLabelsAllowList returns the list of label keys that are allowed
to be used in
+// affinity.nodeAffinityLabels. When the list is empty
(AFFINITY_NODE_LABELS_ALLOWED_KEYS is
+// unset or blank), any key is permitted. When the list is non-empty only
expressions whose
+// keys are in the list are accepted; others are dropped and an info message
is logged by the trait.
+func AffinityNodeLabelsAllowList() []string {
+ raw := GetEnvOrDefault("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "")
+ if raw == "" {
+ return nil
+ }
+ parts := strings.Split(raw, ",")
+ result := make([]string, 0, len(parts))
+ for _, p := range parts {
+ p = strings.TrimSpace(p)
+ if p != "" {
+ result = append(result, p)
+ }
+ }
+
+ return result
+}
+
// BuilderTasksEnabled reports whether CR authors are permitted to inject
custom pipeline tasks
// via the builder.tasks trait. Controlled by the BUILDER_TASKS_ENABLED
operator environment
// variable (default true for backward compatibility). Set to "false" to
prevent custom task
diff --git a/pkg/platform/env_platform_test.go
b/pkg/platform/env_platform_test.go
index 0ea541518..476ff1ba3 100644
--- a/pkg/platform/env_platform_test.go
+++ b/pkg/platform/env_platform_test.go
@@ -168,6 +168,32 @@ func TestBuilderNodeSelectorAllowList_MultipleKeys(t
*testing.T) {
assert.Equal(t, []string{"kubernetes.io/hostname",
"node-role.kubernetes.io/worker", "topology.kubernetes.io/zone"}, allowList)
}
+func TestAffinityNodeLabelsAllowList_NotSet(t *testing.T) {
+ allowList := AffinityNodeLabelsAllowList()
+ assert.Nil(t, allowList)
+}
+
+func TestAffinityNodeLabelsAllowList_Empty(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "")
+
+ allowList := AffinityNodeLabelsAllowList()
+ assert.Empty(t, allowList)
+}
+
+func TestAffinityNodeLabelsAllowList_SingleKey(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "kubernetes.io/hostname")
+
+ allowList := AffinityNodeLabelsAllowList()
+ assert.Equal(t, []string{"kubernetes.io/hostname"}, allowList)
+}
+
+func TestAffinityNodeLabelsAllowList_MultipleKeys(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "kubernetes.io/hostname,
topology.kubernetes.io/zone ")
+
+ allowList := AffinityNodeLabelsAllowList()
+ assert.Equal(t, []string{"kubernetes.io/hostname",
"topology.kubernetes.io/zone"}, allowList)
+}
+
func TestBuilderTasksEnabled_NotSet(t *testing.T) {
// env var not set – default is enabled
assert.True(t, BuilderTasksEnabled())
diff --git a/pkg/trait/affinity.go b/pkg/trait/affinity.go
index cba45637b..803c1b6c9 100644
--- a/pkg/trait/affinity.go
+++ b/pkg/trait/affinity.go
@@ -20,6 +20,7 @@ package trait
import (
"errors"
"fmt"
+ "slices"
"strings"
corev1 "k8s.io/api/core/v1"
@@ -30,6 +31,7 @@ import (
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
+ "github.com/apache/camel-k/v2/pkg/platform"
)
const (
@@ -83,6 +85,7 @@ func (t *affinityTrait) Apply(e *Environment) error {
}
func (t *affinityTrait) addNodeAffinity(_ *Environment, podSpec
*corev1.PodSpec) error {
+ t.filterNodeAffinityLabels()
if len(t.NodeAffinityLabels) == 0 {
return nil
}
@@ -260,3 +263,39 @@ func operatorToLabelSelectorOperator(operator
selection.Operator) (metav1.LabelS
return "", fmt.Errorf("unsupported label selector operator: %s",
operator)
}
+
+// filterNodeAffinityLabels removes expressions whose label key is not in the
operator-configured
+// allow list. When AFFINITY_NODE_LABELS_ALLOWED_KEYS is unset or empty all
expressions are kept.
+func (t *affinityTrait) filterNodeAffinityLabels() {
+ allowList := platform.AffinityNodeLabelsAllowList()
+ if len(allowList) == 0 || len(t.NodeAffinityLabels) == 0 {
+ return
+ }
+ kept := make([]string, 0, len(t.NodeAffinityLabels))
+ for _, expr := range t.NodeAffinityLabels {
+ if t.nodeAffinityLabelAllowed(expr, allowList) {
+ kept = append(kept, expr)
+ }
+ }
+ t.NodeAffinityLabels = kept
+}
+
+// nodeAffinityLabelAllowed returns true when every label key in the
expression is in allowList.
+// Malformed expressions are kept so existing error handling in
addNodeAffinity fires as before.
+func (t *affinityTrait) nodeAffinityLabelAllowed(expr string, allowList
[]string) bool {
+ sel, err := labels.Parse(expr)
+ if err != nil {
+ return true
+ }
+ reqs, _ := sel.Requirements()
+ for _, r := range reqs {
+ if !slices.Contains(allowList, r.Key()) {
+ t.L.Info("affinity.nodeAffinityLabels key is not in the
allowed list and will be ignored",
+ "key", r.Key(), "allowedKeys", allowList)
+
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/pkg/trait/affinity_test.go b/pkg/trait/affinity_test.go
index 818eb72af..3b67e946c 100644
--- a/pkg/trait/affinity_test.go
+++ b/pkg/trait/affinity_test.go
@@ -198,3 +198,62 @@ func createNominalAffinityTest() *affinityTrait {
return trait
}
+
+func TestFilterNodeAffinityLabels_NoAllowList(t *testing.T) {
+ trait := createNominalAffinityTest()
+ trait.NodeAffinityLabels = []string{"kubernetes.io/hostname = node-1",
"topology.kubernetes.io/zone = us-east-1a"}
+
+ trait.filterNodeAffinityLabels()
+ assert.Equal(t, []string{"kubernetes.io/hostname = node-1",
"topology.kubernetes.io/zone = us-east-1a"}, trait.NodeAffinityLabels)
+}
+
+func TestFilterNodeAffinityLabels_AllowListFilters(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "kubernetes.io/hostname")
+
+ trait := createNominalAffinityTest()
+ trait.NodeAffinityLabels = []string{"kubernetes.io/hostname = node-1",
"topology.kubernetes.io/zone = us-east-1a"}
+
+ trait.filterNodeAffinityLabels()
+ assert.Equal(t, []string{"kubernetes.io/hostname = node-1"},
trait.NodeAffinityLabels)
+ assert.NotContains(t, trait.NodeAffinityLabels,
"topology.kubernetes.io/zone = us-east-1a")
+}
+
+func TestFilterNodeAffinityLabels_AllAllowed(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS",
"kubernetes.io/hostname,topology.kubernetes.io/zone")
+
+ trait := createNominalAffinityTest()
+ trait.NodeAffinityLabels = []string{"kubernetes.io/hostname = node-1",
"topology.kubernetes.io/zone = us-east-1a"}
+
+ trait.filterNodeAffinityLabels()
+ assert.Len(t, trait.NodeAffinityLabels, 2)
+}
+
+func TestFilterNodeAffinityLabels_AllDropped(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "kubernetes.io/hostname")
+
+ trait := createNominalAffinityTest()
+ trait.NodeAffinityLabels = []string{"topology.kubernetes.io/zone =
us-east-1a"}
+
+ trait.filterNodeAffinityLabels()
+ assert.Empty(t, trait.NodeAffinityLabels)
+}
+
+func TestApplyNodeAffinityLabelsWithAllowList(t *testing.T) {
+ t.Setenv("AFFINITY_NODE_LABELS_ALLOWED_KEYS", "kubernetes.io/hostname")
+
+ affinityTrait := createNominalAffinityTest()
+ affinityTrait.NodeAffinityLabels = []string{
+ "kubernetes.io/hostname = node-1",
+ "topology.kubernetes.io/zone = us-east-1a",
+ }
+
+ environment, deployment := createNominalDeploymentTraitTest()
+ err := affinityTrait.Apply(environment)
+
+ require.NoError(t, err)
+ nodeAffinity := deployment.Spec.Template.Spec.Affinity.NodeAffinity
+ require.NotNil(t, nodeAffinity)
+ terms :=
nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions
+ assert.Len(t, terms, 1)
+ assert.Equal(t, "kubernetes.io/hostname", terms[0].Key)
+}