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)
+}

Reply via email to