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