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

commit 654a83f3c47137a9c31bd4e905a737edea3a8e03
Author: Æ״óÀë <[email protected]>
AuthorDate: Fri Jul 17 18:23:51 2026 +0800

    Fix #6508: Add resource request/limit support for init and sidecar 
containers
    
    Co-Authored-By: Claude Code on behalf of 641-git641
---
 docs/modules/traits/pages/init-containers.adoc |  10 +-
 pkg/apis/camel/v1/trait/init_containers.go     |  10 +-
 pkg/trait/init_containers.go                   | 128 +++++++++++++---
 pkg/trait/init_containers_test.go              | 199 ++++++++++++++++++++++++-
 4 files changed, 319 insertions(+), 28 deletions(-)

diff --git a/docs/modules/traits/pages/init-containers.adoc 
b/docs/modules/traits/pages/init-containers.adoc
index e825bce93..4eb21bd2f 100644
--- a/docs/modules/traits/pages/init-containers.adoc
+++ b/docs/modules/traits/pages/init-containers.adoc
@@ -29,11 +29,17 @@ The following configuration options are available:
 
 | init-containers.initTasks
 | []string
-| A list of init tasks to be executed with format 
`<name>;<container-image>;<container-command>`.
+| A list of init tasks to be executed.
+Each task accepts the format `<name>;<container-image>;<container-command>` or 
key=value format
+`name=<name>;image=<image>;command=<command>;request-cpu=<quantity>;limit-cpu=<quantity>;request-memory=<quantity>;limit-memory=<quantity>`.
+Resource keys (request-cpu, limit-cpu, request-memory, limit-memory) are 
optional and accept Kubernetes resource quantities.
 
 | init-containers.sideCarTasks
 | []string
-| A list of sidecar tasks to be executed with format 
`<name>;<container-image>;<container-command>`.
+| A list of sidecar tasks to be executed.
+Each task accepts the format `<name>;<container-image>;<container-command>` or 
key=value format
+`name=<name>;image=<image>;command=<command>;request-cpu=<quantity>;limit-cpu=<quantity>;request-memory=<quantity>;limit-memory=<quantity>`.
+Resource keys (request-cpu, limit-cpu, request-memory, limit-memory) are 
optional and accept Kubernetes resource quantities.
 
 |===
 
diff --git a/pkg/apis/camel/v1/trait/init_containers.go 
b/pkg/apis/camel/v1/trait/init_containers.go
index 8e9a67c5f..e6adc1978 100644
--- a/pkg/apis/camel/v1/trait/init_containers.go
+++ b/pkg/apis/camel/v1/trait/init_containers.go
@@ -25,8 +25,14 @@ package trait
 type InitContainersTrait struct {
        Trait `json:",inline" property:",squash"`
 
-       // A list of init tasks to be executed with format 
`<name>;<container-image>;<container-command>`.
+       // A list of init tasks to be executed.
+       // Each task accepts the format 
`<name>;<container-image>;<container-command>` or key=value format
+       // 
`name=<name>;image=<image>;command=<command>;request-cpu=<quantity>;limit-cpu=<quantity>;request-memory=<quantity>;limit-memory=<quantity>`.
+       // Resource keys (request-cpu, limit-cpu, request-memory, limit-memory) 
are optional and accept Kubernetes resource quantities.
        InitTasks []string `json:"initTasks,omitempty" property:"init-tasks"`
-       // A list of sidecar tasks to be executed with format 
`<name>;<container-image>;<container-command>`.
+       // A list of sidecar tasks to be executed.
+       // Each task accepts the format 
`<name>;<container-image>;<container-command>` or key=value format
+       // 
`name=<name>;image=<image>;command=<command>;request-cpu=<quantity>;limit-cpu=<quantity>;request-memory=<quantity>;limit-memory=<quantity>`.
+       // Resource keys (request-cpu, limit-cpu, request-memory, limit-memory) 
are optional and accept Kubernetes resource quantities.
        SidecarTasks []string `json:"sideCarTasks,omitempty" 
property:"sidecar-tasks"`
 }
diff --git a/pkg/trait/init_containers.go b/pkg/trait/init_containers.go
index 57330f111..5e72da24f 100644
--- a/pkg/trait/init_containers.go
+++ b/pkg/trait/init_containers.go
@@ -29,6 +29,7 @@ import (
 
        traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
        "github.com/apache/camel-k/v2/pkg/util/defaults"
+       "k8s.io/apimachinery/pkg/api/resource"
        "k8s.io/utils/ptr"
 )
 
@@ -38,11 +39,15 @@ const (
 )
 
 type containerTask struct {
-       name      string
-       image     string
-       command   string
-       isSidecar bool
-       env       []corev1.EnvVar
+       name          string
+       image         string
+       command       string
+       isSidecar     bool
+       requestCPU    string
+       requestMemory string
+       limitCPU      string
+       limitMemory   string
+       env           []corev1.EnvVar
 }
 
 type initContainersTrait struct {
@@ -183,6 +188,24 @@ func (t *initContainersTrait) 
configureContainers(containers *[]corev1.Container
                if task.isSidecar {
                        initCont.RestartPolicy = 
ptr.To(corev1.ContainerRestartPolicyAlways)
                }
+               if task.requestCPU != "" || task.requestMemory != "" || 
task.limitCPU != "" || task.limitMemory != "" {
+                       initCont.Resources = corev1.ResourceRequirements{
+                               Requests: corev1.ResourceList{},
+                               Limits:   corev1.ResourceList{},
+                       }
+                       if task.requestCPU != "" {
+                               initCont.Resources.Requests[corev1.ResourceCPU] 
= resource.MustParse(task.requestCPU)
+                       }
+                       if task.requestMemory != "" {
+                               
initCont.Resources.Requests[corev1.ResourceMemory] = 
resource.MustParse(task.requestMemory)
+                       }
+                       if task.limitCPU != "" {
+                               initCont.Resources.Limits[corev1.ResourceCPU] = 
resource.MustParse(task.limitCPU)
+                       }
+                       if task.limitMemory != "" {
+                               
initCont.Resources.Limits[corev1.ResourceMemory] = 
resource.MustParse(task.limitMemory)
+                       }
+               }
                *containers = append(*containers, initCont)
        }
 }
@@ -194,31 +217,90 @@ func (t *initContainersTrait) parseTasks() (bool, error) {
        t.tasks = make([]containerTask, len(t.InitTasks)+len(t.SidecarTasks))
        i := 0
        for _, task := range t.InitTasks {
-               split := strings.SplitN(task, ";", 3)
-               if len(split) != 3 {
-                       return false, fmt.Errorf(`could not parse init 
container task "%s": format expected "name;container-image;command"`, task)
-               }
-               t.tasks[i] = containerTask{
-                       name:      split[0],
-                       image:     split[1],
-                       command:   split[2],
-                       isSidecar: false,
+               parsed, err := parseSingleTask(task, false)
+               if err != nil {
+                       return false, fmt.Errorf("could not parse init 
container task %q: %w", task, err)
                }
+               t.tasks[i] = parsed
                i++
        }
        for _, task := range t.SidecarTasks {
-               split := strings.SplitN(task, ";", 3)
-               if len(split) != 3 {
-                       return false, fmt.Errorf(`could not parse sidecar 
container task "%s": format expected "name;container-image;command"`, task)
-               }
-               t.tasks[i] = containerTask{
-                       name:      split[0],
-                       image:     split[1],
-                       command:   split[2],
-                       isSidecar: true,
+               parsed, err := parseSingleTask(task, true)
+               if err != nil {
+                       return false, fmt.Errorf("could not parse sidecar 
container task %q: %w", task, err)
                }
+               t.tasks[i] = parsed
                i++
        }
 
        return true, nil
 }
+
+func parseSingleTask(task string, isSidecar bool) (containerTask, error) {
+       segments := strings.Split(task, ";")
+
+       var result containerTask
+       result.isSidecar = isSidecar
+
+       var commandParts []string
+
+       for _, seg := range segments {
+               if len(strings.TrimSpace(seg)) == 0 {
+                       continue
+               }
+               if strings.Contains(seg, "=") {
+                       kv := strings.SplitN(seg, "=", 2)
+                       key, value := strings.TrimSpace(kv[0]), 
strings.TrimSpace(kv[1])
+                       switch key {
+                       case "name":
+                               result.name = value
+                       case "image":
+                               result.image = value
+                       case "command":
+                               commandParts = append(commandParts, value)
+                       case "request-cpu":
+                               if _, err := resource.ParseQuantity(value); err 
!= nil {
+                                       return containerTask{}, 
fmt.Errorf("invalid request-cpu value %q: %w", value, err)
+                               }
+                               result.requestCPU = value
+                       case "request-memory":
+                               if _, err := resource.ParseQuantity(value); err 
!= nil {
+                                       return containerTask{}, 
fmt.Errorf("invalid request-memory value %q: %w", value, err)
+                               }
+                               result.requestMemory = value
+                       case "limit-cpu":
+                               if _, err := resource.ParseQuantity(value); err 
!= nil {
+                                       return containerTask{}, 
fmt.Errorf("invalid limit-cpu value %q: %w", value, err)
+                               }
+                               result.limitCPU = value
+                       case "limit-memory":
+                               if _, err := resource.ParseQuantity(value); err 
!= nil {
+                                       return containerTask{}, 
fmt.Errorf("invalid limit-memory value %q: %w", value, err)
+                               }
+                               result.limitMemory = value
+                       default:
+                               // Forward compatibility: unknown keys are 
appended to command
+                               commandParts = append(commandParts, seg)
+                       }
+               } else {
+                       // Positional segment: fill first unset field
+                       if result.name == "" {
+                               result.name = seg
+                       } else if result.image == "" {
+                               result.image = seg
+                       } else {
+                               commandParts = append(commandParts, seg)
+                       }
+               }
+       }
+
+       if len(commandParts) > 0 {
+               result.command = strings.Join(commandParts, ";")
+       }
+
+       if result.name == "" || result.image == "" {
+               return containerTask{}, fmt.Errorf("name and image are required 
(format: %q)", "name;image;command or name=...;image=...")
+       }
+
+       return result, nil
+}
diff --git a/pkg/trait/init_containers_test.go 
b/pkg/trait/init_containers_test.go
index 42d154572..e705a0bb1 100644
--- a/pkg/trait/init_containers_test.go
+++ b/pkg/trait/init_containers_test.go
@@ -31,6 +31,7 @@ import (
        "github.com/stretchr/testify/require"
        appsv1 "k8s.io/api/apps/v1"
        corev1 "k8s.io/api/core/v1"
+       "k8s.io/apimachinery/pkg/api/resource"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/utils/ptr"
 )
@@ -66,7 +67,7 @@ func TestParseInitContainerShouldFail(t *testing.T) {
        assert.False(t, configured, "Should not be configured, there's an 
error")
        assert.Nil(t, condition)
        assert.NotNil(t, err)
-       assert.Equal(t, `could not parse init container task "not a valid 
format": format expected "name;container-image;command"`, err.Error())
+       assert.Contains(t, err.Error(), `could not parse init container task 
"not a valid format"`)
 }
 
 func TestParseInitContainerOK(t *testing.T) {
@@ -609,3 +610,199 @@ func TestApplyInitContainerWithBaseTruststore(t 
*testing.T) {
        assert.Contains(t, commandStr, 
"/etc/camel/conf.d/_secrets/my-ca/ca.crt")
        assert.Contains(t, commandStr, "&&")
 }
+
+func TestParseInitContainerWithResources(t *testing.T) {
+       trait := &initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       InitTasks: []string{
+                               
"name=myinit;image=myimg;command=mycmd;request-cpu=100m;limit-cpu=200m;request-memory=128Mi;limit-memory=256Mi",
+                       },
+               },
+       }
+
+       configured, err := trait.parseTasks()
+       assert.True(t, configured)
+       require.Nil(t, err)
+       require.Len(t, trait.tasks, 1)
+
+       task := trait.tasks[0]
+       assert.Equal(t, "myinit", task.name)
+       assert.Equal(t, "myimg", task.image)
+       assert.Equal(t, "mycmd", task.command)
+       assert.False(t, task.isSidecar)
+       assert.Equal(t, "100m", task.requestCPU)
+       assert.Equal(t, "200m", task.limitCPU)
+       assert.Equal(t, "128Mi", task.requestMemory)
+       assert.Equal(t, "256Mi", task.limitMemory)
+}
+
+func TestParseSidecarContainerWithResources(t *testing.T) {
+       trait := &initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       SidecarTasks: []string{
+                               
"name=logger;image=fluentd;command=start;request-cpu=250m;limit-cpu=500m;request-memory=64Mi;limit-memory=128Mi",
+                       },
+               },
+       }
+
+       configured, err := trait.parseTasks()
+       assert.True(t, configured)
+       require.Nil(t, err)
+       require.Len(t, trait.tasks, 1)
+
+       task := trait.tasks[0]
+       assert.Equal(t, "logger", task.name)
+       assert.Equal(t, "fluentd", task.image)
+       assert.Equal(t, "start", task.command)
+       assert.True(t, task.isSidecar)
+       assert.Equal(t, "250m", task.requestCPU)
+       assert.Equal(t, "500m", task.limitCPU)
+       assert.Equal(t, "64Mi", task.requestMemory)
+       assert.Equal(t, "128Mi", task.limitMemory)
+}
+
+func TestParseInitContainerPartialResources(t *testing.T) {
+       trait := &initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       InitTasks: []string{
+                               
"name=myinit;image=myimg;command=mycmd;request-cpu=100m;limit-memory=256Mi",
+                       },
+               },
+       }
+
+       configured, err := trait.parseTasks()
+       assert.True(t, configured)
+       require.Nil(t, err)
+       require.Len(t, trait.tasks, 1)
+
+       task := trait.tasks[0]
+       assert.Equal(t, "100m", task.requestCPU)
+       assert.Equal(t, "", task.limitCPU)
+       assert.Equal(t, "", task.requestMemory)
+       assert.Equal(t, "256Mi", task.limitMemory)
+}
+
+func TestParseInitContainerInvalidResource(t *testing.T) {
+       trait := &initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       InitTasks: []string{
+                               
"name=myinit;image=myimg;command=mycmd;request-cpu=abc",
+                       },
+               },
+       }
+
+       configured, err := trait.parseTasks()
+       assert.False(t, configured)
+       assert.NotNil(t, err)
+       assert.Contains(t, err.Error(), "invalid request-cpu value")
+       assert.Contains(t, err.Error(), `"abc"`)
+}
+
+func TestApplyInitContainerWithResources(t *testing.T) {
+       environment := Environment{
+               Catalog:   NewCatalog(nil),
+               Resources: kubernetes.NewCollection(),
+               Integration: &v1.Integration{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "my-it",
+                       },
+                       Status: v1.IntegrationStatus{
+                               Phase: v1.IntegrationPhaseRunning,
+                       },
+               },
+       }
+       environment.Resources.Add(&appsv1.Deployment{
+               ObjectMeta: metav1.ObjectMeta{
+                       Labels: map[string]string{
+                               v1.IntegrationLabel: "my-it",
+                       },
+               },
+               Spec: appsv1.DeploymentSpec{
+                       Template: corev1.PodTemplateSpec{
+                               Spec: corev1.PodSpec{},
+                       },
+               },
+       })
+       initCont := initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       InitTasks: []string{
+                               
"name=myinit;image=myimg;command=mycmd;request-cpu=100m;limit-cpu=200m;request-memory=128Mi;limit-memory=256Mi",
+                       },
+               },
+       }
+       configured, condition, err := initCont.Configure(&environment)
+       assert.True(t, configured)
+       assert.Nil(t, condition)
+       require.Nil(t, err)
+       err = initCont.Apply(&environment)
+       require.Nil(t, err)
+
+       deploy := 
environment.Resources.GetDeploymentForIntegration(environment.Integration)
+       require.NotNil(t, deploy)
+       require.Len(t, deploy.Spec.Template.Spec.InitContainers, 1)
+
+       container := deploy.Spec.Template.Spec.InitContainers[0]
+       assert.Equal(t, "myinit", container.Name)
+
+       expectedResources := corev1.ResourceRequirements{
+               Requests: corev1.ResourceList{
+                       corev1.ResourceCPU:    resource.MustParse("100m"),
+                       corev1.ResourceMemory: resource.MustParse("128Mi"),
+               },
+               Limits: corev1.ResourceList{
+                       corev1.ResourceCPU:    resource.MustParse("200m"),
+                       corev1.ResourceMemory: resource.MustParse("256Mi"),
+               },
+       }
+       assert.Equal(t, expectedResources, container.Resources)
+}
+
+func TestApplyInitContainerWithoutResources(t *testing.T) {
+       environment := Environment{
+               Catalog:   NewCatalog(nil),
+               Resources: kubernetes.NewCollection(),
+               Integration: &v1.Integration{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "my-it",
+                       },
+                       Status: v1.IntegrationStatus{
+                               Phase: v1.IntegrationPhaseRunning,
+                       },
+               },
+       }
+       environment.Resources.Add(&appsv1.Deployment{
+               ObjectMeta: metav1.ObjectMeta{
+                       Labels: map[string]string{
+                               v1.IntegrationLabel: "my-it",
+                       },
+               },
+               Spec: appsv1.DeploymentSpec{
+                       Template: corev1.PodTemplateSpec{
+                               Spec: corev1.PodSpec{},
+                       },
+               },
+       })
+       initCont := initContainersTrait{
+               InitContainersTrait: trait.InitContainersTrait{
+                       InitTasks: []string{
+                               "myinit;myimg;mycmd",
+                       },
+               },
+       }
+       configured, condition, err := initCont.Configure(&environment)
+       assert.True(t, configured)
+       assert.Nil(t, condition)
+       require.Nil(t, err)
+       err = initCont.Apply(&environment)
+       require.Nil(t, err)
+
+       deploy := 
environment.Resources.GetDeploymentForIntegration(environment.Integration)
+       require.NotNil(t, deploy)
+       require.Len(t, deploy.Spec.Template.Spec.InitContainers, 1)
+
+       container := deploy.Spec.Template.Spec.InitContainers[0]
+       assert.Equal(t, "myinit", container.Name)
+       // Old format without resources should result in nil/empty Resources
+       assert.Nil(t, container.Resources.Requests)
+       assert.Nil(t, container.Resources.Limits)
+}

Reply via email to