Skip to content
10 changes: 10 additions & 0 deletions api/v2/checluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,10 @@ type Deployment struct {
// The pod tolerations of the component pod limit where the pod can run.
// +optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
// List of volumes that can be mounted by containers belonging to the pod.
// Entries are merged by volume name (same semantics as container volumeMounts); unknown names are appended, matching names replace the default volume definition.
// +optional
Volumes []corev1.Volume `json:"volumes,omitempty"`
}

// Container custom settings.
Expand All @@ -783,6 +787,12 @@ type Container struct {
// List of environment variables to set in the container.
// +optional
Env []corev1.EnvVar `json:"env,omitempty"`
// Security options the container should run with. When set, fields are merged into the container security context (same semantics as resources).
// +optional
SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
// Pod volumes to mount into the container's filesystem. Entries are merged by volume mount name (same semantics as env).
// +optional
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
}

// Describes the compute resource requirements.
Expand Down
19 changes: 19 additions & 0 deletions api/v2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ metadata:
categories: Developer Tools
certified: "false"
containerImage: quay.io/eclipse/che-operator:next
createdAt: "2026-04-02T11:48:45Z"
createdAt: "2026-04-07T15:15:43Z"
description: A Kube-native development solution that delivers portable and collaborative
developer workspaces.
features.operators.openshift.io/cnf: "false"
Expand All @@ -108,7 +108,7 @@ metadata:
operatorframework.io/arch.amd64: supported
operatorframework.io/arch.arm64: supported
operatorframework.io/os.linux: supported
name: eclipse-che.v7.117.0-958.next
name: eclipse-che.v7.117.0-970.next
namespace: placeholder
spec:
apiservicedefinitions: {}
Expand Down Expand Up @@ -1144,7 +1144,7 @@ spec:
name: gateway-authorization-sidecar-k8s
- image: quay.io/che-incubator/header-rewrite-proxy:latest
name: gateway-header-sidecar
version: 7.117.0-958.next
version: 7.117.0-970.next
webhookdefinitions:
- admissionReviewVersions:
- v1
Expand Down
13,907 changes: 12,750 additions & 1,157 deletions bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml

Large diffs are not rendered by default.

13,859 changes: 12,704 additions & 1,155 deletions config/crd/bases/org.eclipse.che_checlusters.yaml

Large diffs are not rendered by default.

13,859 changes: 12,704 additions & 1,155 deletions deploy/deployment/kubernetes/combined.yaml

Large diffs are not rendered by default.

Large diffs are not rendered by default.

13,859 changes: 12,704 additions & 1,155 deletions deploy/deployment/openshift/combined.yaml

Large diffs are not rendered by default.

Large diffs are not rendered by default.

13,859 changes: 12,704 additions & 1,155 deletions helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml

Large diffs are not rendered by default.

80 changes: 77 additions & 3 deletions pkg/deploy/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package deploy
import (
"context"
"fmt"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -44,11 +45,10 @@ var DefaultDeploymentDiffOpts = cmp.Options{
cmpopts.IgnoreFields(appsv1.Deployment{}, "TypeMeta", "ObjectMeta", "Status"),
cmpopts.IgnoreFields(appsv1.DeploymentSpec{}, "Replicas", "RevisionHistoryLimit", "ProgressDeadlineSeconds"),
cmpopts.IgnoreFields(appsv1.DeploymentStrategy{}, "RollingUpdate"),
cmpopts.IgnoreFields(corev1.Container{}, "ReadinessProbe", "LivenessProbe", "TerminationMessagePath", "TerminationMessagePolicy", "SecurityContext"),
cmpopts.IgnoreFields(corev1.PodSpec{}, "DNSPolicy", "SchedulerName", "SecurityContext", "DeprecatedServiceAccount"),
cmpopts.IgnoreFields(corev1.Container{}, "ReadinessProbe", "LivenessProbe", "TerminationMessagePath", "TerminationMessagePolicy"),
cmpopts.IgnoreFields(corev1.PodSpec{}, "DNSPolicy", "SchedulerName", "DeprecatedServiceAccount"),
cmpopts.IgnoreFields(corev1.ConfigMapVolumeSource{}, "DefaultMode"),
cmpopts.IgnoreFields(corev1.SecretVolumeSource{}, "DefaultMode"),
cmpopts.IgnoreFields(corev1.VolumeSource{}, "EmptyDir"),
cmp.Comparer(func(x, y resource.Quantity) bool {
return x.Cmp(y) == 0
}),
Expand Down Expand Up @@ -146,6 +146,18 @@ func OverrideDeployment(
deployment.Spec.Template.Spec.Tolerations[i] = t
}
}

// Merge by volume name (same idea as container VolumeMounts) so partial overrides
// do not drop operator-defined volumes (e.g. che-gateway static-config / dynamic-config).
for i := range overrideDeploymentSettings.Volumes {
v := &overrideDeploymentSettings.Volumes[i]
idx := slices.IndexFunc(deployment.Spec.Template.Spec.Volumes, func(vol corev1.Volume) bool { return vol.Name == v.Name })
if idx == -1 {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *v.DeepCopy())
} else {
v.DeepCopyInto(&deployment.Spec.Template.Spec.Volumes[idx])
}
}
}

if !infrastructure.IsOpenShift() {
Expand All @@ -168,6 +180,53 @@ func OverrideDeployment(
return nil
}

func mergeContainerSecurityContext(container *corev1.Container, src *corev1.SecurityContext) {
if src == nil {
return
}
if container.SecurityContext == nil {
container.SecurityContext = &corev1.SecurityContext{}
}
dst := container.SecurityContext
if src.Capabilities != nil {
dst.Capabilities = src.Capabilities.DeepCopy()
}
if src.Privileged != nil {
dst.Privileged = pointer.Bool(*src.Privileged)
}
if src.SELinuxOptions != nil {
dst.SELinuxOptions = src.SELinuxOptions.DeepCopy()
}
if src.WindowsOptions != nil {
dst.WindowsOptions = src.WindowsOptions.DeepCopy()
}
if src.RunAsUser != nil {
dst.RunAsUser = pointer.Int64(*src.RunAsUser)
}
if src.RunAsGroup != nil {
dst.RunAsGroup = pointer.Int64(*src.RunAsGroup)
}
if src.RunAsNonRoot != nil {
dst.RunAsNonRoot = pointer.Bool(*src.RunAsNonRoot)
}
if src.ReadOnlyRootFilesystem != nil {
dst.ReadOnlyRootFilesystem = pointer.Bool(*src.ReadOnlyRootFilesystem)
}
if src.AllowPrivilegeEscalation != nil {
dst.AllowPrivilegeEscalation = pointer.Bool(*src.AllowPrivilegeEscalation)
}
if src.ProcMount != nil {
pm := *src.ProcMount
dst.ProcMount = &pm
}
if src.SeccompProfile != nil {
dst.SeccompProfile = src.SeccompProfile.DeepCopy()
}
if src.AppArmorProfile != nil {
dst.AppArmorProfile = src.AppArmorProfile.DeepCopy()
}
}

func OverrideContainer(
namespace string,
container *corev1.Container,
Expand Down Expand Up @@ -207,6 +266,16 @@ func OverrideContainer(
}
}

// VolumeMounts (merge by mount name, same as env)
for _, vm := range overrideSettings.VolumeMounts {
index := slices.IndexFunc(container.VolumeMounts, func(m corev1.VolumeMount) bool { return m.Name == vm.Name })
if index == -1 {
container.VolumeMounts = append(container.VolumeMounts, vm)
} else {
container.VolumeMounts[index] = vm
}
}

// Resources
if overrideSettings.Resources != nil {
if overrideSettings.Resources.Requests != nil {
Expand Down Expand Up @@ -254,6 +323,11 @@ func OverrideContainer(
}
}

// SecurityContext (merge non-nil fields, same pattern as resources)
if overrideSettings.SecurityContext != nil {
mergeContainerSecurityContext(container, overrideSettings.SecurityContext)
}

return nil
}

Expand Down
167 changes: 167 additions & 0 deletions pkg/deploy/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
)

var (
Expand Down Expand Up @@ -992,6 +993,172 @@ func TestCustomizeDeploymentEnvVar(t *testing.T) {
}
}

func TestCustomizeDeploymentVolumeMounts(t *testing.T) {
initDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test",
VolumeMounts: []corev1.VolumeMount{
{Name: "vm-1", MountPath: "/a"},
{Name: "vm-3", MountPath: "/c"},
},
},
},
},
},
},
}
customization := &chev2.Deployment{
Containers: []chev2.Container{
{
Name: "test",
VolumeMounts: []corev1.VolumeMount{
{Name: "vm-1", MountPath: "/b"},
{Name: "vm-2", MountPath: "/d"},
},
},
},
}
ctx := test.NewCtxBuilder().Build()
err := OverrideDeployment(ctx, initDeployment, customization)
assert.Nil(t, err)

assert.Equal(t, []corev1.VolumeMount{
{Name: "vm-1", MountPath: "/b"},
{Name: "vm-3", MountPath: "/c"},
{Name: "vm-2", MountPath: "/d"},
}, initDeployment.Spec.Template.Spec.Containers[0].VolumeMounts)
}

func TestCustomizeDeploymentVolumes(t *testing.T) {
initDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{Name: "drop-me", VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
}},
},
Containers: []corev1.Container{{Name: "test"}},
},
},
},
}
customization := &chev2.Deployment{
Volumes: []corev1.Volume{
{Name: "custom", VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "cm"},
},
}},
},
Containers: []chev2.Container{{Name: "test"}},
}
ctx := test.NewCtxBuilder().Build()
err := OverrideDeployment(ctx, initDeployment, customization)
assert.Nil(t, err)

assert.Len(t, initDeployment.Spec.Template.Spec.Volumes, 2)
assert.Equal(t, "drop-me", initDeployment.Spec.Template.Spec.Volumes[0].Name)
assert.NotNil(t, initDeployment.Spec.Template.Spec.Volumes[0].EmptyDir)
assert.Equal(t, "custom", initDeployment.Spec.Template.Spec.Volumes[1].Name)
assert.NotNil(t, initDeployment.Spec.Template.Spec.Volumes[1].ConfigMap)
}

func TestCustomizeDeploymentVolumesReplaceByName(t *testing.T) {
initDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{Name: "shared", VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
}},
},
Containers: []corev1.Container{{Name: "test"}},
},
},
},
}
customization := &chev2.Deployment{
Volumes: []corev1.Volume{
{Name: "shared", VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "cm"},
},
}},
},
Containers: []chev2.Container{{Name: "test"}},
}
ctx := test.NewCtxBuilder().Build()
err := OverrideDeployment(ctx, initDeployment, customization)
assert.Nil(t, err)

assert.Len(t, initDeployment.Spec.Template.Spec.Volumes, 1)
assert.Equal(t, "shared", initDeployment.Spec.Template.Spec.Volumes[0].Name)
assert.Nil(t, initDeployment.Spec.Template.Spec.Volumes[0].EmptyDir)
assert.NotNil(t, initDeployment.Spec.Template.Spec.Volumes[0].ConfigMap)
}

func TestCustomizeDeploymentContainerSecurityContext(t *testing.T) {
runAs := int64(1001)
initDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test",
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: pointer.Bool(false),
},
},
},
},
},
},
}
customization := &chev2.Deployment{
Containers: []chev2.Container{
{
Name: "test",
SecurityContext: &corev1.SecurityContext{
RunAsUser: &runAs,
},
},
},
}
ctx := test.NewCtxBuilder().Build()
err := OverrideDeployment(ctx, initDeployment, customization)
assert.Nil(t, err)

sc := initDeployment.Spec.Template.Spec.Containers[0].SecurityContext
assert.NotNil(t, sc.RunAsUser)
assert.Equal(t, runAs, *sc.RunAsUser)
assert.NotNil(t, sc.AllowPrivilegeEscalation)
assert.False(t, *sc.AllowPrivilegeEscalation)
}

func TestShouldNotThrowErrorIfOverrideDeploymentSettingsIsEmpty(t *testing.T) {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Expand Down
Loading