From 73a202da96b741ed664dbb488230f70fa606177f Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Sun, 21 Jun 2026 18:01:47 +0200 Subject: [PATCH 1/4] feat: add configurable field-level restrictions for container and pod overrides Signed-off-by: Anatolii Bazko --- .../devworkspaceoperatorconfig_types.go | 23 +- .../v1alpha1/zz_generated.deepcopy.go | 30 + .../workspace/devworkspace_controller.go | 2 + ...evfile.io_devworkspaceoperatorconfigs.yaml | 22 + deploy/deployment/kubernetes/combined.yaml | 22 + ...r.devfile.io.CustomResourceDefinition.yaml | 22 + deploy/deployment/openshift/combined.yaml | 22 + ...r.devfile.io.CustomResourceDefinition.yaml | 22 + ...evfile.io_devworkspaceoperatorconfigs.yaml | 22 + docs/dwo-configuration.md | 79 + pkg/config/common_test.go | 3 +- pkg/config/defaults.go | 40 +- pkg/config/sync.go | 26 +- pkg/library/container/container.go | 7 +- pkg/library/container/container_test.go | 4 +- .../overrides/container_restrictions.go | 354 ++++ .../overrides/container_restrictions_test.go | 1218 +++++++++++++ pkg/library/overrides/containers.go | 37 +- pkg/library/overrides/containers_test.go | 4 +- pkg/library/overrides/pod_restrictions.go | 419 +++++ .../overrides/pod_restrictions_test.go | 1510 +++++++++++++++++ pkg/library/overrides/pods.go | 29 +- pkg/library/overrides/restrictions.go | 186 ++ ...ontainer-cannot-set-restricted-fields.yaml | 2 +- pkg/provision/storage/commonStorage.go | 19 +- pkg/provision/storage/perWorkspaceStorage.go | 19 +- 26 files changed, 4070 insertions(+), 73 deletions(-) create mode 100644 pkg/library/overrides/container_restrictions.go create mode 100644 pkg/library/overrides/container_restrictions_test.go create mode 100644 pkg/library/overrides/pod_restrictions.go create mode 100644 pkg/library/overrides/pod_restrictions_test.go create mode 100644 pkg/library/overrides/restrictions.go diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 3368d03d8..680a2abd0 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -146,6 +146,24 @@ type RoutingConfig struct { TLSCertificateConfigmapRef *ConfigmapReference `json:"tlsCertificateConfigmapRef,omitempty"` } +// OverrideConfig defines configuration options for controlling which fields are restricted +// in `container-overrides` and `pod-overrides` DevWorkspace attributes. +// Entries support value-level restrictions: "fieldName" restricts the field entirely, +// while "fieldName=value" restricts only that specific value (other values remain allowed). +type OverrideConfig struct { + // RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + // in `container-overrides` attributes. Note that the following fields are always implicitly + // restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + // +kubebuilder:validation:Optional + RestrictedContainerOverrideFields []string `json:"restrictedContainerOverrideFields,omitempty"` + + // RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + // in `pod-overrides` attributes. Note that the following fields are always implicitly + // restricted and cannot be permitted: `containers`, `initContainers`. + // +kubebuilder:validation:Optional + RestrictedPodOverrideFields []string `json:"restrictedPodOverrideFields,omitempty"` +} + type WorkspaceConfig struct { // ProjectCloneConfig defines configuration related to the project clone init container // that is used to clone git projects into the DevWorkspace. @@ -264,6 +282,9 @@ type WorkspaceConfig struct { // InitContainers defines a list of Kubernetes init containers that are automatically injected into all workspace pods. // Typical uses cases include injecting organization tools/configs, initializing persistent home, etc. InitContainers []corev1.Container `json:"initContainers,omitempty"` + // Overrides defines configuration options for `container-overrides` and + // `pod-overrides` DevWorkspace attributes. + Overrides *OverrideConfig `json:"overrides,omitempty"` } type WebhookConfig struct { diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index f795f3d43..0c3e6e4f9 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -515,6 +515,31 @@ func (in *OrasConfig) DeepCopy() *OrasConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OverrideConfig) DeepCopyInto(out *OverrideConfig) { + *out = *in + if in.RestrictedContainerOverrideFields != nil { + in, out := &in.RestrictedContainerOverrideFields, &out.RestrictedContainerOverrideFields + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RestrictedPodOverrideFields != nil { + in, out := &in.RestrictedPodOverrideFields, &out.RestrictedPodOverrideFields + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OverrideConfig. +func (in *OverrideConfig) DeepCopy() *OverrideConfig { + if in == nil { + return nil + } + out := new(OverrideConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PersistentHomeConfig) DeepCopyInto(out *PersistentHomeConfig) { *out = *in @@ -939,6 +964,11 @@ func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Overrides != nil { + in, out := &in.Overrides, &out.Overrides + *out = new(OverrideConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index 6a25994da..3bd84c0d3 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -23,6 +23,7 @@ import ( "time" "github.com/devfile/devworkspace-operator/pkg/library/initcontainers" + "github.com/devfile/devworkspace-operator/pkg/library/overrides" "github.com/devfile/devworkspace-operator/pkg/library/ssh" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -339,6 +340,7 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request workspace.Config.Workspace.DefaultContainerResources, workspace.Config.Workspace.ContainerResourceCaps, workspace.Config.Workspace.PostStartTimeout, + overrides.GetRestrictedContainerOverrideFields(workspace), postStartDebugTrapSleepDuration, ) if err != nil { diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 62a2b4b85..342be3774 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4012,6 +4012,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index dbaffa512..50775dfc4 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -4213,6 +4213,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index c20a7ec63..3178c6b4f 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4213,6 +4213,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index fd0318841..d1e887886 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -4213,6 +4213,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index c20a7ec63..3178c6b4f 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4213,6 +4213,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index f9748de3b..dde5f880b 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4211,6 +4211,28 @@ spec: - name type: object type: array + overrides: + description: |- + Overrides defines configuration options for `container-overrides` and + `pod-overrides` DevWorkspace attributes. + properties: + restrictedContainerOverrideFields: + description: |- + RestrictedContainerOverrideFields defines a list of container-level fields that are restricted + in `container-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `name`, `image`, `command`, `args`, `ports`, `env`. + items: + type: string + type: array + restrictedPodOverrideFields: + description: |- + RestrictedPodOverrideFields defines a list of pod-level fields that are restricted + in `pod-overrides` attributes. Note that the following fields are always implicitly + restricted and cannot be permitted: `containers`, `initContainers`. + items: + type: string + type: array + type: object persistUserHome: description: |- PersistUserHome defines configuration options for persisting the `/home/user/` diff --git a/docs/dwo-configuration.md b/docs/dwo-configuration.md index c7c5f8611..85436d389 100644 --- a/docs/dwo-configuration.md +++ b/docs/dwo-configuration.md @@ -310,3 +310,82 @@ config: ### Execution Order Custom init containers are injected after the project-clone init container in the order they are defined in the configuration. The `init-persistent-home` container runs in this sequence along with other custom init containers. + +## Restricting override fields + +The DevWorkspace Operator allows cluster administrators to restrict which fields +can be set via `pod-overrides` and `container-overrides` attributes. + +The restrict list supports two formats: + +- `"fieldName"` -- restricts the field entirely, regardless of value +- `"fieldName=value"` -- restricts only a specific value for the field + +For nested fields such as securityContext or volumes, use dot notation: `securityContext.privileged=true`. + +On Kubernetes, the operator ships with default restricted fields that align +with the Pod Security Standards baseline profile. +On OpenShift, no fields are restricted by default since Security Context Constraints (SCC) +already enforce security policies at the admission level. + +**Important:** Configuring `restrictedContainerOverrideFields` or `restrictedPodOverrideFields` +**replaces** the platform defaults entirely. Admins who want to extend the default +restrict list must re-include the default entries alongside any additional restrictions. + +**Limitation for plain boolean fields in pod overrides:** +Some `PodSpec` fields such as `hostNetwork`, `hostPID`, and `hostIPC` are plain `bool` +types in the Kubernetes API (not `*bool` pointers). Because Go zero-initializes +unset `bool` fields to `false`, the operator cannot distinguish between a field that +was explicitly set to `false` and one that was simply omitted. As a result, using the +bare field name format (e.g. `"hostNetwork"`) to restrict these fields entirely will reject +**all** pod overrides, including those that never mention the field. To avoid this, +use the value-specific format instead (e.g. `"hostNetwork=true"`). The default restricted +fields already follow this pattern. This limitation does not affect `*bool` pointer +fields (e.g. `automountServiceAccountToken`, `shareProcessNamespace`, `hostUsers`) +or non-boolean fields. + +For example, on Kubernetes, to add `volumeMounts` and `lifecycle` restrictions +while keeping the default restricted container fields: + +```yaml +apiVersion: controller.devfile.io/v1alpha1 +kind: DevWorkspaceOperatorConfig +metadata: + name: devworkspace-operator-config +config: + workspace: + overrides: + restrictedContainerOverrideFields: + # Default Kubernetes restricted fields (must be re-listed to retain them) + - "securityContext.privileged=true" + - "securityContext.runAsNonRoot=false" + - "securityContext.runAsUser=0" + - "securityContext.allowPrivilegeEscalation=true" + - "securityContext.procMount=Unmasked" + - "securityContext.capabilities.add" + # Additional restrictions + - "volumeMounts" + - "lifecycle" +``` + +Similarly, to extend the default restricted pod override fields on Kubernetes: + +```yaml +apiVersion: controller.devfile.io/v1alpha1 +kind: DevWorkspaceOperatorConfig +metadata: + name: devworkspace-operator-config +config: + workspace: + overrides: + restrictedPodOverrideFields: + # Default Kubernetes restricted fields (must be re-listed to retain them) + - "hostNetwork=true" + - "hostPID=true" + - "hostIPC=true" + - "securityContext.runAsNonRoot=false" + - "securityContext.runAsUser=0" + - "volumes.hostPath" + # Additional restrictions + - "hostUsers=false" +``` diff --git a/pkg/config/common_test.go b/pkg/config/common_test.go index 0bbbb4550..7a161b074 100644 --- a/pkg/config/common_test.go +++ b/pkg/config/common_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -56,6 +56,7 @@ func setupForTest(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) setDefaultPodSecurityContext() setDefaultContainerSecurityContext() + setDefaultOverrideConfig() configNamespace = testNamespace originalDefaultConfig := defaultConfig.DeepCopy() t.Cleanup(func() { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index bfb31b034..7b299ac38 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -116,7 +116,31 @@ var ( } defaultKubernetesContainerSecurityContext = &corev1.SecurityContext{} defaultOpenShiftPodSecurityContext = &corev1.PodSecurityContext{} - defaultOpenShiftContainerSecurityContext = &corev1.SecurityContext{ + + defaultOpenShiftOverrideConfig = &v1alpha1.OverrideConfig{ + RestrictedContainerOverrideFields: []string{}, + RestrictedPodOverrideFields: []string{}, + } + defaultKubernetesOverrideConfig = &v1alpha1.OverrideConfig{ + RestrictedContainerOverrideFields: []string{ + "securityContext.privileged=true", + "securityContext.runAsNonRoot=false", + "securityContext.runAsUser=0", + "securityContext.allowPrivilegeEscalation=true", + "securityContext.procMount=Unmasked", + "securityContext.capabilities.add", + }, + RestrictedPodOverrideFields: []string{ + "hostNetwork=true", + "hostPID=true", + "hostIPC=true", + "securityContext.runAsNonRoot=false", + "securityContext.runAsUser=0", + "volumes.hostPath", + }, + } + + defaultOpenShiftContainerSecurityContext = &corev1.SecurityContext{ ReadOnlyRootFilesystem: pointer.Bool(false), RunAsNonRoot: pointer.Bool(true), AllowPrivilegeEscalation: pointer.Bool(false), @@ -157,3 +181,15 @@ func setDefaultContainerSecurityContext() error { } return nil } + +func setDefaultOverrideConfig() error { + if !infrastructure.IsInitialized() { + return fmt.Errorf("can not set default override config, infrastructure not detected") + } + if infrastructure.IsOpenShift() { + defaultConfig.Workspace.Overrides = defaultOpenShiftOverrideConfig + } else { + defaultConfig.Workspace.Overrides = defaultKubernetesOverrideConfig + } + return nil +} diff --git a/pkg/config/sync.go b/pkg/config/sync.go index bf474e986..7ae068007 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -101,6 +101,7 @@ func SetGlobalConfigForTesting(testConfig *controller.OperatorConfiguration) { defer configMutex.Unlock() setDefaultPodSecurityContext() setDefaultContainerSecurityContext() + setDefaultOverrideConfig() internalConfig = defaultConfig.DeepCopy() mergeConfig(testConfig, internalConfig) } @@ -115,6 +116,9 @@ func SetupControllerConfig(client crclient.Client) error { if err := setDefaultContainerSecurityContext(); err != nil { return err } + if err := setDefaultOverrideConfig(); err != nil { + return err + } internalConfig = &controller.OperatorConfiguration{} @@ -506,6 +510,18 @@ func mergeConfig(from, to *controller.OperatorConfiguration) { } to.Workspace.InitContainers = initContainersCopy } + + if from.Workspace.Overrides != nil { + if to.Workspace.Overrides == nil { + to.Workspace.Overrides = &controller.OverrideConfig{} + } + if from.Workspace.Overrides.RestrictedContainerOverrideFields != nil { + to.Workspace.Overrides.RestrictedContainerOverrideFields = from.Workspace.Overrides.RestrictedContainerOverrideFields + } + if from.Workspace.Overrides.RestrictedPodOverrideFields != nil { + to.Workspace.Overrides.RestrictedPodOverrideFields = from.Workspace.Overrides.RestrictedPodOverrideFields + } + } } } @@ -777,6 +793,14 @@ func GetCurrentConfigString(currConfig *controller.OperatorConfiguration) string if workspace.HostUsers != nil { config = append(config, fmt.Sprintf("workspace.hostUsers=%t", *workspace.HostUsers)) } + if workspace.Overrides != nil { + if workspace.Overrides.RestrictedContainerOverrideFields != nil { + config = append(config, fmt.Sprintf("workspace.overrides.deniedContainerOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedContainerOverrideFields, ", "))) + } + if workspace.Overrides.RestrictedPodOverrideFields != nil { + config = append(config, fmt.Sprintf("workspace.overrides.deniedPodOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedPodOverrideFields, ", "))) + } + } if len(workspace.InitContainers) > 0 { initContainerNames := make([]string, len(workspace.InitContainers)) for i, container := range workspace.InitContainers { diff --git a/pkg/library/container/container.go b/pkg/library/container/container.go index 88bfd9854..4b4330b7d 100644 --- a/pkg/library/container/container.go +++ b/pkg/library/container/container.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -53,6 +53,7 @@ func GetKubeContainersFromDevfile( defaultResources *corev1.ResourceRequirements, resourceCaps *corev1.ResourceRequirements, postStartTimeout string, + restrictedFields []string, postStartDebugTrapSleepDuration string, ) (*v1alpha1.PodAdditions, error) { if !flatten.DevWorkspaceIsFlattened(workspace, nil) { @@ -77,7 +78,7 @@ func GetKubeContainersFromDevfile( return nil, err } if overrides.NeedsContainerOverride(&component) { - patchedContainer, err := overrides.ApplyContainerOverrides(&component, k8sContainer) + patchedContainer, err := overrides.ApplyContainerOverrides(&component, k8sContainer, restrictedFields) if err != nil { return nil, err } @@ -111,7 +112,7 @@ func GetKubeContainersFromDevfile( return nil, err } if overrides.NeedsContainerOverride(&initComponent) { - patchedContainer, err := overrides.ApplyContainerOverrides(&initComponent, k8sContainer) + patchedContainer, err := overrides.ApplyContainerOverrides(&initComponent, k8sContainer, restrictedFields) if err != nil { return nil, err } diff --git a/pkg/library/container/container_test.go b/pkg/library/container/container_test.go index 9f1d56e62..9ec58240c 100644 --- a/pkg/library/container/container_test.go +++ b/pkg/library/container/container_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -87,7 +87,7 @@ func TestGetKubeContainersFromDevfile(t *testing.T) { t.Run(tt.Name, func(t *testing.T) { // sanity check that file is read correctly. assert.True(t, len(tt.Input.Components) > 0, "Input defines no components") - gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources, nil, "", "") + gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources, nil, "", nil, "") if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { diff --git a/pkg/library/overrides/container_restrictions.go b/pkg/library/overrides/container_restrictions.go new file mode 100644 index 000000000..2c338cf65 --- /dev/null +++ b/pkg/library/overrides/container_restrictions.go @@ -0,0 +1,354 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package overrides + +import ( + "fmt" + "reflect" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +func getContainerRestrictionErr(msg string) error { + return fmt.Errorf("cannot use container-overrides to override container %s", msg) +} + +func restrictContainerOverride(override *corev1.Container, restrictedFields []string) error { + if override.Name != "" { + return getContainerRestrictionErr("name") + } + if override.Image != "" { + return getContainerRestrictionErr("image") + } + if override.Command != nil { + return getContainerRestrictionErr("command") + } + if override.Args != nil { + return getContainerRestrictionErr("args") + } + if override.Ports != nil { + return getContainerRestrictionErr("ports") + } + if override.Env != nil { + return getContainerRestrictionErr("env") + } + + for _, restrictedField := range restrictedFields { + fieldName, fieldValue, _ := strings.Cut(restrictedField, "=") + if fieldName == "" { + continue + } + + root, remaining, _ := strings.Cut(fieldName, ".") + + restriction := &FieldRestriction{ + name: fieldName, + restrictedValue: fieldValue, + getRestrictionErr: getContainerRestrictionErr, + } + + if err := checkContainer(override, root, remaining, restriction); err != nil { + return err + } + } + + return nil +} + +func checkContainer(override *corev1.Container, root string, remaining string, restriction *FieldRestriction) error { + if remaining == "" { + switch root { + case "workingDir": + return restriction.checkString(&override.WorkingDir) + case "restartPolicy": + return restriction.checkString((*string)(override.RestartPolicy)) + case "terminationMessagePath": + return restriction.checkString(&override.TerminationMessagePath) + case "terminationMessagePolicy": + return restriction.checkString((*string)(&override.TerminationMessagePolicy)) + case "imagePullPolicy": + return restriction.checkString((*string)(&override.ImagePullPolicy)) + case "stdin": + return restriction.checkBool(&override.Stdin) + case "stdinOnce": + return restriction.checkBool(&override.StdinOnce) + case "tty": + return restriction.checkBool(&override.TTY) + case "envFrom": + if len(override.EnvFrom) != 0 { + return restriction.checkAny() + } + case "restartPolicyRules": + if len(override.RestartPolicyRules) != 0 { + return restriction.checkAny() + } + case "resizePolicy": + if len(override.ResizePolicy) != 0 { + return restriction.checkAny() + } + case "readinessProbe": + if override.ReadinessProbe != nil { + return restriction.checkAny() + } + case "startupProbe": + if override.StartupProbe != nil { + return restriction.checkAny() + } + case "livenessProbe": + if override.LivenessProbe != nil { + return restriction.checkAny() + } + case "lifecycle": + if override.Lifecycle != nil { + return restriction.checkAny() + } + case "resources": + if !reflect.DeepEqual(override.Resources, corev1.ResourceRequirements{}) { + return restriction.checkAny() + } + case "volumeMounts": + if len(override.VolumeMounts) > 0 { + return restriction.checkAny() + } + case "volumeDevices": + if len(override.VolumeDevices) > 0 { + return restriction.checkAny() + } + case "securityContext": + if override.SecurityContext != nil { + return restriction.checkAny() + } + } + + return nil + } + + switch root { + case "envFrom": + return checkEnvsFrom(override.EnvFrom, remaining, restriction) + case "resources": + return checkResources(&override.Resources, remaining, restriction) + case "volumeMounts": + return checkVolumeMounts(override.VolumeMounts, remaining, restriction) + case "volumeDevices": + return checkVolumeDevices(override.VolumeDevices, remaining, restriction) + case "securityContext": + return checkContainerSecurityContext(override.SecurityContext, remaining, restriction) + } + + return nil +} + +func checkEnvsFrom(envsFrom []corev1.EnvFromSource, field string, restriction *FieldRestriction) error { + if len(envsFrom) == 0 { + return nil + } + + root, remaining, _ := strings.Cut(field, ".") + + for _, envFrom := range envsFrom { + if err := checkEnvFrom(envFrom, root, remaining, restriction); err != nil { + return err + } + } + + return nil +} + +func checkEnvFrom(envFrom corev1.EnvFromSource, root string, remaining string, restriction *FieldRestriction) error { + if remaining == "" { + switch root { + case "configMapRef": + if envFrom.ConfigMapRef != nil { + return restriction.checkAny() + } + case "secretRef": + if envFrom.SecretRef != nil { + return restriction.checkAny() + } + } + return nil + } + + switch root { + case "configMapRef": + return checkConfigMapRef(envFrom.ConfigMapRef, remaining, restriction) + case "secretRef": + return checkSecretRef(envFrom.SecretRef, remaining, restriction) + } + + return nil +} + +func checkConfigMapRef(cmRef *corev1.ConfigMapEnvSource, field string, restriction *FieldRestriction) error { + if cmRef == nil { + return nil + } + + switch field { + case "name": + return restriction.checkString(&cmRef.Name) + } + + return nil +} + +func checkSecretRef(secretRef *corev1.SecretEnvSource, field string, restriction *FieldRestriction) error { + if secretRef == nil { + return nil + } + + switch field { + case "name": + return restriction.checkString(&secretRef.Name) + } + + return nil +} + +func checkVolumeMounts(mounts []corev1.VolumeMount, field string, restriction *FieldRestriction) error { + if len(mounts) == 0 { + return nil + } + + for _, mount := range mounts { + if err := checkVolumeMount(mount, field, restriction); err != nil { + return err + } + } + + return nil +} + +func checkVolumeMount(mount corev1.VolumeMount, field string, restriction *FieldRestriction) error { + switch field { + case "readOnly": + return restriction.checkBool(&mount.ReadOnly) + case "recursiveReadOnly": + return restriction.checkString((*string)(mount.RecursiveReadOnly)) + case "mountPropagation": + return restriction.checkString((*string)(mount.MountPropagation)) + case "name": + return restriction.checkString(&mount.Name) + case "mountPath": + return restriction.checkString(&mount.MountPath) + case "subPath": + return restriction.checkString(&mount.SubPath) + case "subPathExpr": + return restriction.checkString(&mount.SubPathExpr) + } + + return nil +} + +func checkVolumeDevices(devices []corev1.VolumeDevice, field string, restriction *FieldRestriction) error { + if len(devices) == 0 { + return nil + } + + for _, device := range devices { + if err := checkVolumeDevice(device, field, restriction); err != nil { + return err + } + } + + return nil +} + +func checkVolumeDevice(device corev1.VolumeDevice, field string, restriction *FieldRestriction) error { + switch field { + case "name": + return restriction.checkString(&device.Name) + case "devicePath": + return restriction.checkString(&device.DevicePath) + } + + return nil +} + +func checkContainerSecurityContext(sc *corev1.SecurityContext, field string, restriction *FieldRestriction) error { + if sc == nil { + return nil + } + + root, remaining, _ := strings.Cut(field, ".") + + if remaining == "" { + switch root { + case "capabilities": + if sc.Capabilities != nil { + return restriction.checkAny() + } + case "privileged": + return restriction.checkBool(sc.Privileged) + case "seLinuxOptions": + if sc.SELinuxOptions != nil { + return restriction.checkAny() + } + case "runAsUser": + return restriction.checkInt64(sc.RunAsUser) + case "runAsGroup": + return restriction.checkInt64(sc.RunAsGroup) + case "runAsNonRoot": + return restriction.checkBool(sc.RunAsNonRoot) + case "readOnlyRootFilesystem": + return restriction.checkBool(sc.ReadOnlyRootFilesystem) + case "allowPrivilegeEscalation": + return restriction.checkBool(sc.AllowPrivilegeEscalation) + case "procMount": + return restriction.checkString((*string)(sc.ProcMount)) + case "seccompProfile": + if sc.SeccompProfile != nil { + return restriction.checkAny() + } + case "appArmorProfile": + if sc.AppArmorProfile != nil { + return restriction.checkAny() + } + } + + return nil + } + + switch root { + case "capabilities": + return checkCapabilities(sc.Capabilities, remaining, restriction) + } + + return nil +} + +func checkCapabilities(caps *corev1.Capabilities, field string, restriction *FieldRestriction) error { + if caps == nil { + return nil + } + + switch field { + case "add": + for _, capAdd := range caps.Add { + if err := restriction.checkString((*string)(&capAdd)); err != nil { + return err + } + } + case "drop": + for _, capDrop := range caps.Drop { + if err := restriction.checkString((*string)(&capDrop)); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/library/overrides/container_restrictions_test.go b/pkg/library/overrides/container_restrictions_test.go new file mode 100644 index 000000000..483978b39 --- /dev/null +++ b/pkg/library/overrides/container_restrictions_test.go @@ -0,0 +1,1218 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package overrides + +import ( + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/ptr" +) + +func TestRestrictContainerOverride(t *testing.T) { + tests := []struct { + Name string + + Override corev1.Container + RestrictedFields []string + + IsErrorExpected bool + ErrField string + }{ + { + Name: "no restricted fields allows everything", + Override: corev1.Container{}, + }, + { + Name: "name always restricted", + Override: corev1.Container{Name: "test"}, + IsErrorExpected: true, + ErrField: "name", + }, + { + Name: "image always restricted", + Override: corev1.Container{Image: "test"}, + IsErrorExpected: true, + ErrField: "image", + }, + { + Name: "command always restricted", + Override: corev1.Container{Command: []string{}}, + IsErrorExpected: true, + ErrField: "command", + }, + { + Name: "args always restricted", + Override: corev1.Container{Args: []string{}}, + IsErrorExpected: true, + ErrField: "args", + }, + { + Name: "ports always restricted", + Override: corev1.Container{Ports: []corev1.ContainerPort{{}}}, + IsErrorExpected: true, + ErrField: "ports", + }, + { + Name: "env always restricted", + Override: corev1.Container{Env: []corev1.EnvVar{{}}}, + IsErrorExpected: true, + ErrField: "env", + }, + // ----------- WorkingDir ----------- + { + Name: "workingDir empty value not restricted", + Override: corev1.Container{WorkingDir: ""}, + RestrictedFields: []string{"workingDir"}, + IsErrorExpected: false, + }, + { + Name: "workingDir restricted by specific value match", + Override: corev1.Container{WorkingDir: "/tmp"}, + RestrictedFields: []string{"workingDir=/tmp"}, + IsErrorExpected: true, + ErrField: "workingDir=/tmp", + }, + { + Name: "workingDir restricted by any value", + Override: corev1.Container{WorkingDir: "/workspace"}, + RestrictedFields: []string{"workingDir"}, + IsErrorExpected: true, + ErrField: "workingDir", + }, + { + Name: "workingDir allowed when restricted value does not match", + Override: corev1.Container{WorkingDir: "/tmp"}, + RestrictedFields: []string{"workingDir=/root"}, + IsErrorExpected: false, + }, + // ----------- RestartPolicy ----------- + { + Name: "restartPolicy nil value not restricted", + Override: corev1.Container{RestartPolicy: nil}, + RestrictedFields: []string{"restartPolicy"}, + IsErrorExpected: false, + }, + { + Name: "restartPolicy restricted by specific value match", + Override: corev1.Container{RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways)}, + RestrictedFields: []string{"restartPolicy=Always"}, + IsErrorExpected: true, + ErrField: "restartPolicy=Always", + }, + { + Name: "restartPolicy restricted by any value", + Override: corev1.Container{RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways)}, + RestrictedFields: []string{"restartPolicy"}, + IsErrorExpected: true, + ErrField: "restartPolicy", + }, + { + Name: "restartPolicy allowed when restricted value does not match", + Override: corev1.Container{RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways)}, + RestrictedFields: []string{"restartPolicy=Never"}, + IsErrorExpected: false, + }, + // ----------- TerminationMessagePath ----------- + { + Name: "terminationMessagePath empty value not restricted", + Override: corev1.Container{TerminationMessagePath: ""}, + RestrictedFields: []string{"terminationMessagePath"}, + IsErrorExpected: false, + }, + { + Name: "terminationMessagePath restricted by specific value match", + Override: corev1.Container{TerminationMessagePath: "/dev/termination-log"}, + RestrictedFields: []string{"terminationMessagePath=/dev/termination-log"}, + IsErrorExpected: true, + ErrField: "terminationMessagePath=/dev/termination-log", + }, + { + Name: "terminationMessagePath restricted by any value", + Override: corev1.Container{TerminationMessagePath: "/dev/termination-log"}, + RestrictedFields: []string{"terminationMessagePath"}, + IsErrorExpected: true, + ErrField: "terminationMessagePath", + }, + { + Name: "terminationMessagePath allowed when restricted value does not match", + Override: corev1.Container{TerminationMessagePath: "/dev/termination-log"}, + RestrictedFields: []string{"terminationMessagePath=/other/path"}, + IsErrorExpected: false, + }, + // ----------- TerminationMessagePolicy ----------- + { + Name: "terminationMessagePolicy empty value not restricted", + Override: corev1.Container{TerminationMessagePolicy: ""}, + RestrictedFields: []string{"terminationMessagePolicy"}, + IsErrorExpected: false, + }, + { + Name: "terminationMessagePolicy restricted by specific value match", + Override: corev1.Container{TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError}, + RestrictedFields: []string{"terminationMessagePolicy=FallbackToLogsOnError"}, + IsErrorExpected: true, + ErrField: "terminationMessagePolicy=FallbackToLogsOnError", + }, + { + Name: "terminationMessagePolicy restricted by any value", + Override: corev1.Container{TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError}, + RestrictedFields: []string{"terminationMessagePolicy"}, + IsErrorExpected: true, + ErrField: "terminationMessagePolicy", + }, + { + Name: "terminationMessagePolicy allowed when restricted value does not match", + Override: corev1.Container{TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError}, + RestrictedFields: []string{"terminationMessagePolicy=File"}, + IsErrorExpected: false, + }, + // ----------- ImagePullPolicy ----------- + { + Name: "imagePullPolicy empty value not restricted", + Override: corev1.Container{ImagePullPolicy: ""}, + RestrictedFields: []string{"imagePullPolicy"}, + IsErrorExpected: false, + }, + { + Name: "imagePullPolicy restricted by specific value match", + Override: corev1.Container{ImagePullPolicy: corev1.PullAlways}, + RestrictedFields: []string{"imagePullPolicy=Always"}, + IsErrorExpected: true, + ErrField: "imagePullPolicy=Always", + }, + { + Name: "imagePullPolicy restricted by any value", + Override: corev1.Container{ImagePullPolicy: corev1.PullAlways}, + RestrictedFields: []string{"imagePullPolicy"}, + IsErrorExpected: true, + ErrField: "imagePullPolicy", + }, + { + Name: "imagePullPolicy allowed when restricted value does not match", + Override: corev1.Container{ImagePullPolicy: corev1.PullAlways}, + RestrictedFields: []string{"imagePullPolicy=Never"}, + IsErrorExpected: false, + }, + // ----------- Stdin ----------- + { + Name: "stdin false value restricted by any value", + Override: corev1.Container{Stdin: false}, + RestrictedFields: []string{"stdin"}, + IsErrorExpected: true, + ErrField: "stdin", + }, + { + Name: "stdin restricted by specific value match", + Override: corev1.Container{Stdin: true}, + RestrictedFields: []string{"stdin=true"}, + IsErrorExpected: true, + ErrField: "stdin=true", + }, + { + Name: "stdin restricted by any value", + Override: corev1.Container{Stdin: true}, + RestrictedFields: []string{"stdin"}, + IsErrorExpected: true, + ErrField: "stdin", + }, + { + Name: "stdin allowed when restricted value does not match", + Override: corev1.Container{Stdin: true}, + RestrictedFields: []string{"stdin=false"}, + IsErrorExpected: false, + }, + // ----------- StdinOnce ----------- + { + Name: "stdinOnce false value restricted by any value", + Override: corev1.Container{StdinOnce: false}, + RestrictedFields: []string{"stdinOnce"}, + IsErrorExpected: true, + ErrField: "stdinOnce", + }, + { + Name: "stdinOnce restricted by specific value match", + Override: corev1.Container{StdinOnce: true}, + RestrictedFields: []string{"stdinOnce=true"}, + IsErrorExpected: true, + ErrField: "stdinOnce=true", + }, + { + Name: "stdinOnce restricted by any value", + Override: corev1.Container{StdinOnce: true}, + RestrictedFields: []string{"stdinOnce"}, + IsErrorExpected: true, + ErrField: "stdinOnce", + }, + { + Name: "stdinOnce allowed when restricted value does not match", + Override: corev1.Container{StdinOnce: true}, + RestrictedFields: []string{"stdinOnce=false"}, + IsErrorExpected: false, + }, + // ----------- TTY ----------- + { + Name: "tty false value restricted by any value", + Override: corev1.Container{TTY: false}, + RestrictedFields: []string{"tty"}, + IsErrorExpected: true, + ErrField: "tty", + }, + { + Name: "tty restricted by specific value match", + Override: corev1.Container{TTY: true}, + RestrictedFields: []string{"tty=true"}, + IsErrorExpected: true, + ErrField: "tty=true", + }, + { + Name: "tty restricted by any value", + Override: corev1.Container{TTY: true}, + RestrictedFields: []string{"tty"}, + IsErrorExpected: true, + ErrField: "tty", + }, + { + Name: "tty allowed when restricted value does not match", + Override: corev1.Container{TTY: true}, + RestrictedFields: []string{"tty=false"}, + IsErrorExpected: false, + }, + // ----------- EnvFrom ----------- + { + Name: "envFrom empty slice not restricted", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{}}, + RestrictedFields: []string{"envFrom"}, + IsErrorExpected: false, + }, + { + Name: "envFrom restricted by any value", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{}}}, + RestrictedFields: []string{"envFrom"}, + IsErrorExpected: true, + ErrField: "envFrom", + }, + { + Name: "envFrom.configMapRef nil value not restricted", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: nil}}}, + RestrictedFields: []string{"envFrom.configMapRef"}, + IsErrorExpected: false, + }, + { + Name: "envFrom.secretRef nil value not restricted", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{SecretRef: nil}}}, + RestrictedFields: []string{"envFrom.secretRef"}, + IsErrorExpected: false, + }, + { + Name: "envFrom.configMapRef restricted by any value", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{}}}}, + RestrictedFields: []string{"envFrom.configMapRef"}, + IsErrorExpected: true, + ErrField: "envFrom.configMapRef", + }, + { + Name: "envFrom.secretRef restricted by any value", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{}}}}, + RestrictedFields: []string{"envFrom.secretRef"}, + IsErrorExpected: true, + ErrField: "envFrom.secretRef", + }, + { + Name: "envFrom.configMapRef.name restricted by any value", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}}, + }}}, + RestrictedFields: []string{"envFrom.configMapRef.name"}, + IsErrorExpected: true, + ErrField: "envFrom.configMapRef.name", + }, + { + Name: "envFrom.secretRef.name restricted by any value", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}, + }}}, + RestrictedFields: []string{"envFrom.secretRef.name"}, + IsErrorExpected: true, + ErrField: "envFrom.secretRef.name", + }, + { + Name: "envFrom.configMapRef.name restricted by specific value match", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}}, + }}}, + RestrictedFields: []string{"envFrom.configMapRef.name=my-config"}, + IsErrorExpected: true, + ErrField: "envFrom.configMapRef.name=my-config", + }, + { + Name: "envFrom.secretRef.name restricted by specific value match", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}, + }}}, + RestrictedFields: []string{"envFrom.secretRef.name=my-secret"}, + IsErrorExpected: true, + ErrField: "envFrom.secretRef.name=my-secret", + }, + { + Name: "envFrom.configMapRef.name allowed when restricted value does not match", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}}, + }}}, + RestrictedFields: []string{"envFrom.configMapRef.name=other-config"}, + IsErrorExpected: false, + }, + { + Name: "envFrom.secretRef.name allowed when restricted value does not match", + Override: corev1.Container{EnvFrom: []corev1.EnvFromSource{{ + SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}, + }}}, + RestrictedFields: []string{"envFrom.secretRef.name=other-secret"}, + IsErrorExpected: false, + }, + // ----------- RestartPolicyRules ----------- + { + Name: "restartPolicyRules empty slice not restricted", + Override: corev1.Container{RestartPolicyRules: nil}, + RestrictedFields: []string{"restartPolicyRules"}, + IsErrorExpected: false, + }, + { + Name: "restartPolicyRules restricted by any value", + Override: corev1.Container{RestartPolicyRules: []corev1.ContainerRestartRule{{}}}, + RestrictedFields: []string{"restartPolicyRules"}, + IsErrorExpected: true, + ErrField: "restartPolicyRules", + }, + // ----------- ResizePolicy ----------- + { + Name: "resizePolicy empty slice not restricted", + Override: corev1.Container{ResizePolicy: nil}, + RestrictedFields: []string{"resizePolicy"}, + IsErrorExpected: false, + }, + { + Name: "resizePolicy restricted by any value", + Override: corev1.Container{ResizePolicy: []corev1.ContainerResizePolicy{{}}}, + RestrictedFields: []string{"resizePolicy"}, + IsErrorExpected: true, + ErrField: "resizePolicy", + }, + // ----------- ReadinessProbe ----------- + { + Name: "readinessProbe nil not restricted", + Override: corev1.Container{ReadinessProbe: nil}, + RestrictedFields: []string{"readinessProbe"}, + IsErrorExpected: false, + }, + { + Name: "readinessProbe restricted by any value", + Override: corev1.Container{ReadinessProbe: &corev1.Probe{}}, + RestrictedFields: []string{"readinessProbe"}, + IsErrorExpected: true, + ErrField: "readinessProbe", + }, + // ----------- StartupProbe ----------- + { + Name: "startupProbe nil not restricted", + Override: corev1.Container{StartupProbe: nil}, + RestrictedFields: []string{"startupProbe"}, + IsErrorExpected: false, + }, + { + Name: "startupProbe restricted by any value", + Override: corev1.Container{StartupProbe: &corev1.Probe{}}, + RestrictedFields: []string{"startupProbe"}, + IsErrorExpected: true, + ErrField: "startupProbe", + }, + // ----------- LivenessProbe ----------- + { + Name: "livenessProbe nil not restricted", + Override: corev1.Container{LivenessProbe: nil}, + RestrictedFields: []string{"livenessProbe"}, + IsErrorExpected: false, + }, + { + Name: "livenessProbe restricted by any value", + Override: corev1.Container{LivenessProbe: &corev1.Probe{}}, + RestrictedFields: []string{"livenessProbe"}, + IsErrorExpected: true, + ErrField: "livenessProbe", + }, + // ----------- Lifecycle ----------- + { + Name: "lifecycle nil not restricted", + Override: corev1.Container{Lifecycle: nil}, + RestrictedFields: []string{"lifecycle"}, + IsErrorExpected: false, + }, + { + Name: "lifecycle restricted by any value", + Override: corev1.Container{Lifecycle: &corev1.Lifecycle{}}, + RestrictedFields: []string{"lifecycle"}, + IsErrorExpected: true, + ErrField: "lifecycle", + }, + // ----------- Resources ----------- + { + Name: "resources empty not restricted", + Override: corev1.Container{Resources: corev1.ResourceRequirements{}}, + RestrictedFields: []string{"resources"}, + IsErrorExpected: false, + }, + { + Name: "resources restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{}}}, + RestrictedFields: []string{"resources"}, + IsErrorExpected: true, + ErrField: "resources", + }, + // ----------- Resources.Limits ----------- + { + Name: "resources.limits nil not restricted", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Limits: nil}}, + RestrictedFields: []string{"resources.limits"}, + IsErrorExpected: false, + }, + { + Name: "resources.limits restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{}}}, + RestrictedFields: []string{"resources.limits"}, + IsErrorExpected: true, + ErrField: "resources.limits", + }, + // ----------- Resources.Requests ----------- + { + Name: "resources.requests nil not restricted", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Requests: nil}}, + RestrictedFields: []string{"resources.requests"}, + IsErrorExpected: false, + }, + { + Name: "resources.requests restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{}}}, + RestrictedFields: []string{"resources.requests"}, + IsErrorExpected: true, + ErrField: "resources.requests", + }, + // ----------- Resources.Claims ----------- + { + Name: "resources.claims empty not restricted", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Requests: nil}}, + RestrictedFields: []string{"resources.claims"}, + IsErrorExpected: false, + }, + { + Name: "resources.claims restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{{Name: "gpu"}}}}, + RestrictedFields: []string{"resources.claims"}, + IsErrorExpected: true, + ErrField: "resources.claims", + }, + // ----------- Resources.Limits.CPU ----------- + { + Name: "resources.limits.cpu restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")}}}, + RestrictedFields: []string{"resources.limits.cpu"}, + IsErrorExpected: true, + ErrField: "resources.limits.cpu", + }, + // ----------- Resources.Limits.Memory ----------- + { + Name: "resources.limits.memory restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("512Mi")}}}, + RestrictedFields: []string{"resources.limits.memory"}, + IsErrorExpected: true, + ErrField: "resources.limits.memory", + }, + // ----------- Resources.Requests.CPU ----------- + { + Name: "resources.requests.cpu restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("50m")}}}, + RestrictedFields: []string{"resources.requests.cpu"}, + IsErrorExpected: true, + ErrField: "resources.requests.cpu", + }, + // ----------- Resources.Requests.Memory ----------- + { + Name: "resources.requests.memory restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("128Mi")}}}, + RestrictedFields: []string{"resources.requests.memory"}, + IsErrorExpected: true, + ErrField: "resources.requests.memory", + }, + // ----------- Resources.Claims.Request ----------- + { + Name: "resources.claims.request empty claims not restricted", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{}}}, + RestrictedFields: []string{"resources.claims.request"}, + IsErrorExpected: false, + }, + { + Name: "resources.claims.request restricted by specific value match", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{{Name: "gpu", Request: "gpu-partition"}}}}, + RestrictedFields: []string{"resources.claims.request=gpu-partition"}, + IsErrorExpected: true, + ErrField: "resources.claims.request=gpu-partition", + }, + { + Name: "resources.claims.request restricted by any value", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{{Name: "gpu", Request: "gpu-partition"}}}}, + RestrictedFields: []string{"resources.claims.request"}, + IsErrorExpected: true, + ErrField: "resources.claims.request", + }, + { + Name: "resources.claims.request allowed when restricted value does not match", + Override: corev1.Container{Resources: corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{{Name: "gpu", Request: "gpu-partition"}}}}, + RestrictedFields: []string{"resources.claims.request=other-partition"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts ----------- + { + Name: "volumeMounts empty slice not restricted", + Override: corev1.Container{VolumeMounts: nil}, + RestrictedFields: []string{"volumeMounts"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{}}}, + RestrictedFields: []string{"volumeMounts"}, + IsErrorExpected: true, + ErrField: "volumeMounts", + }, + // ----------- VolumeMounts.ReadOnly ----------- + { + Name: "volumeMounts.readOnly false value restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{ReadOnly: false}}}, + RestrictedFields: []string{"volumeMounts.readOnly"}, + IsErrorExpected: true, + ErrField: "volumeMounts.readOnly", + }, + { + Name: "volumeMounts.readOnly restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{ReadOnly: true}}}, + RestrictedFields: []string{"volumeMounts.readOnly=true"}, + IsErrorExpected: true, + ErrField: "volumeMounts.readOnly=true", + }, + { + Name: "volumeMounts.readOnly restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{ReadOnly: true}}}, + RestrictedFields: []string{"volumeMounts.readOnly"}, + IsErrorExpected: true, + ErrField: "volumeMounts.readOnly", + }, + { + Name: "volumeMounts.readOnly allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{ReadOnly: true}}}, + RestrictedFields: []string{"volumeMounts.readOnly=false"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.RecursiveReadOnly ----------- + { + Name: "volumeMounts.recursiveReadOnly nil value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{RecursiveReadOnly: nil}}}, + RestrictedFields: []string{"volumeMounts.recursiveReadOnly"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.recursiveReadOnly restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{RecursiveReadOnly: ptr.To(corev1.RecursiveReadOnlyEnabled)}}}, + RestrictedFields: []string{"volumeMounts.recursiveReadOnly=Enabled"}, + IsErrorExpected: true, + ErrField: "volumeMounts.recursiveReadOnly=Enabled", + }, + { + Name: "volumeMounts.recursiveReadOnly restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{RecursiveReadOnly: ptr.To(corev1.RecursiveReadOnlyEnabled)}}}, + RestrictedFields: []string{"volumeMounts.recursiveReadOnly"}, + IsErrorExpected: true, + ErrField: "volumeMounts.recursiveReadOnly", + }, + { + Name: "volumeMounts.recursiveReadOnly allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{RecursiveReadOnly: ptr.To(corev1.RecursiveReadOnlyEnabled)}}}, + RestrictedFields: []string{"volumeMounts.recursiveReadOnly=Disabled"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.MountPropagation ----------- + { + Name: "volumeMounts.mountPropagation nil value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPropagation: nil}}}, + RestrictedFields: []string{"volumeMounts.mountPropagation"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.mountPropagation restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPropagation: ptr.To(corev1.MountPropagationBidirectional)}}}, + RestrictedFields: []string{"volumeMounts.mountPropagation=Bidirectional"}, + IsErrorExpected: true, + ErrField: "volumeMounts.mountPropagation=Bidirectional", + }, + { + Name: "volumeMounts.mountPropagation restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPropagation: ptr.To(corev1.MountPropagationBidirectional)}}}, + RestrictedFields: []string{"volumeMounts.mountPropagation"}, + IsErrorExpected: true, + ErrField: "volumeMounts.mountPropagation", + }, + { + Name: "volumeMounts.mountPropagation allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPropagation: ptr.To(corev1.MountPropagationBidirectional)}}}, + RestrictedFields: []string{"volumeMounts.mountPropagation=HostToContainer"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.Name ----------- + { + Name: "volumeMounts.name empty value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{Name: ""}}}, + RestrictedFields: []string{"volumeMounts.name"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.name restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumeMounts.name=my-volume"}, + IsErrorExpected: true, + ErrField: "volumeMounts.name=my-volume", + }, + { + Name: "volumeMounts.name restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumeMounts.name"}, + IsErrorExpected: true, + ErrField: "volumeMounts.name", + }, + { + Name: "volumeMounts.name allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumeMounts.name=other-volume"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.MountPath ----------- + { + Name: "volumeMounts.mountPath empty value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPath: ""}}}, + RestrictedFields: []string{"volumeMounts.mountPath"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.mountPath restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPath: "/data"}}}, + RestrictedFields: []string{"volumeMounts.mountPath=/data"}, + IsErrorExpected: true, + ErrField: "volumeMounts.mountPath=/data", + }, + { + Name: "volumeMounts.mountPath restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPath: "/data"}}}, + RestrictedFields: []string{"volumeMounts.mountPath"}, + IsErrorExpected: true, + ErrField: "volumeMounts.mountPath", + }, + { + Name: "volumeMounts.mountPath allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{MountPath: "/data"}}}, + RestrictedFields: []string{"volumeMounts.mountPath=/other"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.SubPath ----------- + { + Name: "volumeMounts.subPath empty value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPath: ""}}}, + RestrictedFields: []string{"volumeMounts.subPath"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.subPath restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPath: "config"}}}, + RestrictedFields: []string{"volumeMounts.subPath=config"}, + IsErrorExpected: true, + ErrField: "volumeMounts.subPath=config", + }, + { + Name: "volumeMounts.subPath restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPath: "config"}}}, + RestrictedFields: []string{"volumeMounts.subPath"}, + IsErrorExpected: true, + ErrField: "volumeMounts.subPath", + }, + { + Name: "volumeMounts.subPath allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPath: "config"}}}, + RestrictedFields: []string{"volumeMounts.subPath=other"}, + IsErrorExpected: false, + }, + // ----------- VolumeMounts.SubPathExpr ----------- + { + Name: "volumeMounts.subPathExpr empty value not restricted", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPathExpr: ""}}}, + RestrictedFields: []string{"volumeMounts.subPathExpr"}, + IsErrorExpected: false, + }, + { + Name: "volumeMounts.subPathExpr restricted by specific value match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPathExpr: "$(POD_NAME)"}}}, + RestrictedFields: []string{"volumeMounts.subPathExpr=$(POD_NAME)"}, + IsErrorExpected: true, + ErrField: "volumeMounts.subPathExpr=$(POD_NAME)", + }, + { + Name: "volumeMounts.subPathExpr restricted by any value", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPathExpr: "$(POD_NAME)"}}}, + RestrictedFields: []string{"volumeMounts.subPathExpr"}, + IsErrorExpected: true, + ErrField: "volumeMounts.subPathExpr", + }, + { + Name: "volumeMounts.subPathExpr allowed when restricted value does not match", + Override: corev1.Container{VolumeMounts: []corev1.VolumeMount{{SubPathExpr: "$(POD_NAME)"}}}, + RestrictedFields: []string{"volumeMounts.subPathExpr=$(OTHER)"}, + IsErrorExpected: false, + }, + // ----------- VolumeDevices ----------- + { + Name: "volumeDevices empty slice not restricted", + Override: corev1.Container{VolumeDevices: nil}, + RestrictedFields: []string{"volumeDevices"}, + IsErrorExpected: false, + }, + { + Name: "volumeDevices restricted by any value", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{}}}, + RestrictedFields: []string{"volumeDevices"}, + IsErrorExpected: true, + ErrField: "volumeDevices", + }, + // ----------- VolumeDevices.Name ----------- + { + Name: "volumeDevices.name empty value not restricted", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{Name: ""}}}, + RestrictedFields: []string{"volumeDevices.name"}, + IsErrorExpected: false, + }, + { + Name: "volumeDevices.name restricted by specific value match", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{Name: "my-device"}}}, + RestrictedFields: []string{"volumeDevices.name=my-device"}, + IsErrorExpected: true, + ErrField: "volumeDevices.name=my-device", + }, + { + Name: "volumeDevices.name restricted by any value", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{Name: "my-device"}}}, + RestrictedFields: []string{"volumeDevices.name"}, + IsErrorExpected: true, + ErrField: "volumeDevices.name", + }, + { + Name: "volumeDevices.name allowed when restricted value does not match", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{Name: "my-device"}}}, + RestrictedFields: []string{"volumeDevices.name=other-device"}, + IsErrorExpected: false, + }, + // ----------- VolumeDevices.DevicePath ----------- + { + Name: "volumeDevices.devicePath empty value not restricted", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{DevicePath: ""}}}, + RestrictedFields: []string{"volumeDevices.devicePath"}, + IsErrorExpected: false, + }, + { + Name: "volumeDevices.devicePath restricted by specific value match", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{DevicePath: "/dev/sda"}}}, + RestrictedFields: []string{"volumeDevices.devicePath=/dev/sda"}, + IsErrorExpected: true, + ErrField: "volumeDevices.devicePath=/dev/sda", + }, + { + Name: "volumeDevices.devicePath restricted by any value", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{DevicePath: "/dev/sda"}}}, + RestrictedFields: []string{"volumeDevices.devicePath"}, + IsErrorExpected: true, + ErrField: "volumeDevices.devicePath", + }, + { + Name: "volumeDevices.devicePath allowed when restricted value does not match", + Override: corev1.Container{VolumeDevices: []corev1.VolumeDevice{{DevicePath: "/dev/sda"}}}, + RestrictedFields: []string{"volumeDevices.devicePath=/dev/sdb"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext ----------- + { + Name: "securityContext nil not restricted", + Override: corev1.Container{SecurityContext: nil}, + RestrictedFields: []string{"securityContext"}, + IsErrorExpected: false, + }, + { + Name: "securityContext restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{}}, + RestrictedFields: []string{"securityContext"}, + IsErrorExpected: true, + ErrField: "securityContext", + }, + // ----------- SecurityContext.SELinuxOptions ----------- + { + Name: "securityContext.seLinuxOptions nil not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{SELinuxOptions: nil}}, + RestrictedFields: []string{"securityContext.seLinuxOptions"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.seLinuxOptions restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{SELinuxOptions: &corev1.SELinuxOptions{}}}, + RestrictedFields: []string{"securityContext.seLinuxOptions"}, + IsErrorExpected: true, + ErrField: "securityContext.seLinuxOptions", + }, + // ----------- SecurityContext.SeccompProfile ----------- + { + Name: "securityContext.seccompProfile nil not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{SeccompProfile: nil}}, + RestrictedFields: []string{"securityContext.seccompProfile"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.seccompProfile restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{}}}, + RestrictedFields: []string{"securityContext.seccompProfile"}, + IsErrorExpected: true, + ErrField: "securityContext.seccompProfile", + }, + // ----------- SecurityContext.AppArmorProfile ----------- + { + Name: "securityContext.appArmorProfile nil not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AppArmorProfile: nil}}, + RestrictedFields: []string{"securityContext.appArmorProfile"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.appArmorProfile restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AppArmorProfile: &corev1.AppArmorProfile{}}}, + RestrictedFields: []string{"securityContext.appArmorProfile"}, + IsErrorExpected: true, + ErrField: "securityContext.appArmorProfile", + }, + // ----------- SecurityContext.Privileged ----------- + { + Name: "securityContext.privileged nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Privileged: nil}}, + RestrictedFields: []string{"securityContext.privileged"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.privileged restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Privileged: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.privileged=true"}, + IsErrorExpected: true, + ErrField: "securityContext.privileged=true", + }, + { + Name: "securityContext.privileged restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Privileged: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.privileged"}, + IsErrorExpected: true, + ErrField: "securityContext.privileged", + }, + { + Name: "securityContext.privileged allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Privileged: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.privileged=false"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.RunAsNonRoot ----------- + { + Name: "securityContext.runAsNonRoot nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsNonRoot: nil}}, + RestrictedFields: []string{"securityContext.runAsNonRoot"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsNonRoot restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot=true"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsNonRoot=true", + }, + { + Name: "securityContext.runAsNonRoot restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsNonRoot", + }, + { + Name: "securityContext.runAsNonRoot allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot=false"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.ReadOnlyRootFilesystem ----------- + { + Name: "securityContext.readOnlyRootFilesystem nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ReadOnlyRootFilesystem: nil}}, + RestrictedFields: []string{"securityContext.readOnlyRootFilesystem"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.readOnlyRootFilesystem restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ReadOnlyRootFilesystem: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.readOnlyRootFilesystem=true"}, + IsErrorExpected: true, + ErrField: "securityContext.readOnlyRootFilesystem=true", + }, + { + Name: "securityContext.readOnlyRootFilesystem restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ReadOnlyRootFilesystem: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.readOnlyRootFilesystem"}, + IsErrorExpected: true, + ErrField: "securityContext.readOnlyRootFilesystem", + }, + { + Name: "securityContext.readOnlyRootFilesystem allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ReadOnlyRootFilesystem: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.readOnlyRootFilesystem=false"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.AllowPrivilegeEscalation ----------- + { + Name: "securityContext.allowPrivilegeEscalation nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AllowPrivilegeEscalation: nil}}, + RestrictedFields: []string{"securityContext.allowPrivilegeEscalation"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.allowPrivilegeEscalation restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AllowPrivilegeEscalation: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.allowPrivilegeEscalation=true"}, + IsErrorExpected: true, + ErrField: "securityContext.allowPrivilegeEscalation=true", + }, + { + Name: "securityContext.allowPrivilegeEscalation restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AllowPrivilegeEscalation: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.allowPrivilegeEscalation"}, + IsErrorExpected: true, + ErrField: "securityContext.allowPrivilegeEscalation", + }, + { + Name: "securityContext.allowPrivilegeEscalation allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{AllowPrivilegeEscalation: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.allowPrivilegeEscalation=false"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.ProcMount ----------- + { + Name: "securityContext.procMount nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ProcMount: nil}}, + RestrictedFields: []string{"securityContext.procMount"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.procMount restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ProcMount: ptr.To(corev1.UnmaskedProcMount)}}, + RestrictedFields: []string{"securityContext.procMount=Unmasked"}, + IsErrorExpected: true, + ErrField: "securityContext.procMount=Unmasked", + }, + { + Name: "securityContext.procMount restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ProcMount: ptr.To(corev1.UnmaskedProcMount)}}, + RestrictedFields: []string{"securityContext.procMount"}, + IsErrorExpected: true, + ErrField: "securityContext.procMount", + }, + { + Name: "securityContext.procMount allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{ProcMount: ptr.To(corev1.UnmaskedProcMount)}}, + RestrictedFields: []string{"securityContext.procMount=Default"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.RunAsUser ----------- + { + Name: "securityContext.runAsUser nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsUser: nil}}, + RestrictedFields: []string{"securityContext.runAsUser"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsUser restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser=1000"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsUser=1000", + }, + { + Name: "securityContext.runAsUser restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsUser", + }, + { + Name: "securityContext.runAsUser allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser=2000"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.RunAsGroup ----------- + { + Name: "securityContext.runAsGroup nil value not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsGroup: nil}}, + RestrictedFields: []string{"securityContext.runAsGroup"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsGroup restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup=500"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsGroup=500", + }, + { + Name: "securityContext.runAsGroup restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsGroup", + }, + { + Name: "securityContext.runAsGroup allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup=1000"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.Capabilities ----------- + { + Name: "securityContext.capabilities nil not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: nil}}, + RestrictedFields: []string{"securityContext.capabilities"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.capabilities restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{}}}, + RestrictedFields: []string{"securityContext.capabilities"}, + IsErrorExpected: true, + ErrField: "securityContext.capabilities", + }, + // ----------- SecurityContext.Capabilities.Add ----------- + { + Name: "securityContext.capabilities.add nil capabilities not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Add: []corev1.Capability{}}}}, + RestrictedFields: []string{"securityContext.capabilities.add"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.capabilities.add restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Add: []corev1.Capability{"NET_ADMIN"}}}}, + RestrictedFields: []string{"securityContext.capabilities.add=NET_ADMIN"}, + IsErrorExpected: true, + ErrField: "securityContext.capabilities.add=NET_ADMIN", + }, + { + Name: "securityContext.capabilities.add restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Add: []corev1.Capability{"NET_ADMIN"}}}}, + RestrictedFields: []string{"securityContext.capabilities.add"}, + IsErrorExpected: true, + ErrField: "securityContext.capabilities.add", + }, + { + Name: "securityContext.capabilities.add allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Add: []corev1.Capability{"NET_ADMIN"}}}}, + RestrictedFields: []string{"securityContext.capabilities.add=SYS_ADMIN"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.Capabilities.Drop ----------- + { + Name: "securityContext.capabilities.drop nil capabilities not restricted", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{}}}}, + RestrictedFields: []string{"securityContext.capabilities.drop"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.capabilities.drop restricted by specific value match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}}}, + RestrictedFields: []string{"securityContext.capabilities.drop=ALL"}, + IsErrorExpected: true, + ErrField: "securityContext.capabilities.drop=ALL", + }, + { + Name: "securityContext.capabilities.drop restricted by any value", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}}}, + RestrictedFields: []string{"securityContext.capabilities.drop"}, + IsErrorExpected: true, + ErrField: "securityContext.capabilities.drop", + }, + { + Name: "securityContext.capabilities.drop allowed when restricted value does not match", + Override: corev1.Container{SecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}}}, + RestrictedFields: []string{"securityContext.capabilities.drop=NET_RAW"}, + IsErrorExpected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + err := restrictContainerOverride(&tt.Override, tt.RestrictedFields) + + if tt.IsErrorExpected { + assert.Error(t, err) + assert.Equal(t, getContainerRestrictionErr(tt.ErrField).Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestApplyContainerOverridesStripsUnknownFields(t *testing.T) { + overrideJSON := `{"workingDir":"/workspace","futureSecurityField":"malicious-value","unknownNested":{"key":"val"}}` + + component := &dw.Component{ + Name: "test-component", + Attributes: attributes.Attributes{ + constants.ContainerOverridesAttribute: apiext.JSON{Raw: []byte(overrideJSON)}, + }, + ComponentUnion: dw.ComponentUnion{ + Container: &dw.ContainerComponent{ + Container: dw.Container{Image: "test-image"}, + }, + }, + } + container := &corev1.Container{ + Name: "test-component", + Image: "test-image", + } + + patched, err := ApplyContainerOverrides(component, container, nil) + assert.NoError(t, err) + assert.Equal(t, "/workspace", patched.WorkingDir) + + patchedBytes, err := json.Marshal(patched) + assert.NoError(t, err) + assert.NotContains(t, string(patchedBytes), "futureSecurityField") + assert.NotContains(t, string(patchedBytes), "unknownNested") +} diff --git a/pkg/library/overrides/containers.go b/pkg/library/overrides/containers.go index 2e68b26b9..0e2fa3ebe 100644 --- a/pkg/library/overrides/containers.go +++ b/pkg/library/overrides/containers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -38,13 +38,13 @@ func NeedsContainerOverride(component *dw.Component) bool { return component.Container != nil && component.Attributes.Exists(constants.ContainerOverridesAttribute) } -func ApplyContainerOverrides(component *dw.Component, container *corev1.Container) (*corev1.Container, error) { +func ApplyContainerOverrides(component *dw.Component, container *corev1.Container, restrictedFields []string) (*corev1.Container, error) { override := &corev1.Container{} if err := component.Attributes.GetInto(constants.ContainerOverridesAttribute, override); err != nil { return nil, fmt.Errorf("failed to parse %s attribute on component %s: %w", constants.ContainerOverridesAttribute, component.Name, err) } - if err := restrictContainerOverride(override); err != nil { - return nil, fmt.Errorf("failed to parse %s attribute on component %s: %w", constants.ContainerOverridesAttribute, component.Name, err) + if err := restrictContainerOverride(override, restrictedFields); err != nil { + return nil, fmt.Errorf("invalid %s attribute on component %s: %w", constants.ContainerOverridesAttribute, component.Name, err) } overrideJSON := component.Attributes[constants.ContainerOverridesAttribute] @@ -74,35 +74,6 @@ func ApplyContainerOverrides(component *dw.Component, container *corev1.Containe return patched, nil } -// restrictContainerOverride unsets fields on a container that should not be -// considered for container overrides. These fields are generally available to -// set as fields on the container component itself. -func restrictContainerOverride(override *corev1.Container) error { - invalidField := "" - if override.Name != "" { - invalidField = "name" - } - if override.Image != "" { - invalidField = "image" - } - if override.Command != nil { - invalidField = "command" - } - if override.Args != nil { - invalidField = "args" - } - if override.Ports != nil { - invalidField = "ports" - } - if override.Env != nil { - invalidField = "env" - } - if invalidField != "" { - return fmt.Errorf("cannot use container-overrides to override container %s", invalidField) - } - return nil -} - // handleDefaultedContainerFields fills partially-filled structs with defaulted fields // in a container. This is required to avoid repeatedly reconciling a container where e.g. // diff --git a/pkg/library/overrides/containers_test.go b/pkg/library/overrides/containers_test.go index 2a86a820e..6c8c49844 100644 --- a/pkg/library/overrides/containers_test.go +++ b/pkg/library/overrides/containers_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -30,7 +30,7 @@ func TestApplyContainerOverrides(t *testing.T) { tests := loadAllContainerTestCasesOrPanic(t, "testdata/container-overrides") for _, tt := range tests { t.Run(fmt.Sprintf("%s (%s)", tt.Name, tt.originalFilename), func(t *testing.T) { - outContainer, err := ApplyContainerOverrides(tt.Input.Component, tt.Input.Container) + outContainer, err := ApplyContainerOverrides(tt.Input.Component, tt.Input.Container, []string{}) if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { diff --git a/pkg/library/overrides/pod_restrictions.go b/pkg/library/overrides/pod_restrictions.go new file mode 100644 index 000000000..7deb861c7 --- /dev/null +++ b/pkg/library/overrides/pod_restrictions.go @@ -0,0 +1,419 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package overrides + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +func getPodRestrictionErr(msg string) error { + return fmt.Errorf("cannot use pod-overrides to override pod %s", msg) +} + +func restrictPodOverride(override *corev1.PodSpec, restrictedFields []string) error { + if override.Containers != nil { + return getPodRestrictionErr("containers") + } + if override.InitContainers != nil { + return getPodRestrictionErr("initContainers") + } + + for _, field := range restrictedFields { + fieldName, fieldValue, _ := strings.Cut(field, "=") + if fieldName == "" { + continue + } + + root, remaining, _ := strings.Cut(fieldName, ".") + + restriction := &FieldRestriction{ + name: fieldName, + restrictedValue: fieldValue, + getRestrictionErr: getPodRestrictionErr, + } + + if err := checkPodField(override, root, remaining, restriction); err != nil { + return err + } + } + + return nil +} + +func checkPodField(override *corev1.PodSpec, root string, remaining string, restriction *FieldRestriction) error { + if remaining == "" { + switch root { + case "restartPolicy": + return restriction.checkString((*string)(&override.RestartPolicy)) + case "terminationGracePeriodSeconds": + return restriction.checkInt64(override.TerminationGracePeriodSeconds) + case "activeDeadlineSeconds": + return restriction.checkInt64(override.ActiveDeadlineSeconds) + case "dnsPolicy": + return restriction.checkString((*string)(&override.DNSPolicy)) + case "nodeSelector": + if len(override.NodeSelector) > 0 { + return restriction.checkAny() + } + case "serviceAccountName": + return restriction.checkString(&override.ServiceAccountName) + case "deprecatedServiceAccount": + return restriction.checkString(&override.DeprecatedServiceAccount) + case "automountServiceAccountToken": + return restriction.checkBool(override.AutomountServiceAccountToken) + case "nodeName": + return restriction.checkString(&override.NodeName) + case "hostIPC": + return restriction.checkBool(&override.HostIPC) + case "hostPID": + return restriction.checkBool(&override.HostPID) + case "hostNetwork": + return restriction.checkBool(&override.HostNetwork) + case "shareProcessNamespace": + return restriction.checkBool(override.ShareProcessNamespace) + case "hostname": + return restriction.checkString(&override.Hostname) + case "subdomain": + return restriction.checkString(&override.Subdomain) + case "schedulerName": + return restriction.checkString(&override.SchedulerName) + case "priorityClassName": + return restriction.checkString(&override.PriorityClassName) + case "priority": + return restriction.checkInt32(override.Priority) + case "runtimeClassName": + return restriction.checkString(override.RuntimeClassName) + case "enableServiceLinks": + return restriction.checkBool(override.EnableServiceLinks) + case "preemptionPolicy": + return restriction.checkString((*string)(override.PreemptionPolicy)) + case "setHostnameAsFQDN": + return restriction.checkBool(override.SetHostnameAsFQDN) + case "hostUsers": + return restriction.checkBool(override.HostUsers) + case "hostnameOverride": + return restriction.checkString(override.HostnameOverride) + case "ephemeralContainers": + if len(override.EphemeralContainers) > 0 { + return restriction.checkAny() + } + case "affinity": + if override.Affinity != nil { + return restriction.checkAny() + } + case "tolerations": + if len(override.Tolerations) > 0 { + return restriction.checkAny() + } + case "hostAliases": + if len(override.HostAliases) > 0 { + return restriction.checkAny() + } + case "dnsConfig": + if override.DNSConfig != nil { + return restriction.checkAny() + } + case "readinessGates": + if len(override.ReadinessGates) > 0 { + return restriction.checkAny() + } + case "topologySpreadConstraints": + if len(override.TopologySpreadConstraints) > 0 { + return restriction.checkAny() + } + case "os": + if override.OS != nil { + return restriction.checkAny() + } + case "schedulingGates": + if len(override.SchedulingGates) > 0 { + return restriction.checkAny() + } + case "workloadRef": + if override.WorkloadRef != nil { + return restriction.checkAny() + } + case "volumes": + if len(override.Volumes) > 0 { + return restriction.checkAny() + } + case "securityContext": + if override.SecurityContext != nil { + return restriction.checkAny() + } + case "imagePullSecrets": + if len(override.ImagePullSecrets) > 0 { + return restriction.checkAny() + } + case "overhead": + if len(override.Overhead) > 0 { + return restriction.checkAny() + } + case "resourceClaims": + if len(override.ResourceClaims) > 0 { + return restriction.checkAny() + } + case "resources": + if override.Resources != nil { + return restriction.checkAny() + } + } + + return nil + } + + switch root { + case "volumes": + return checkVolumes(override.Volumes, remaining, restriction) + case "securityContext": + return checkPodSecurityContext(override.SecurityContext, remaining, restriction) + case "imagePullSecrets": + return checkImagePullSecrets(override.ImagePullSecrets, remaining, restriction) + case "overhead": + return checkResourceList(override.Overhead, remaining, restriction) + case "resourceClaims": + return checkResourceClaims(override.ResourceClaims, remaining, restriction) + case "resources": + return checkResources(override.Resources, remaining, restriction) + } + + return nil +} + +func checkVolumes(volumes []corev1.Volume, field string, restriction *FieldRestriction) error { + if len(volumes) == 0 { + return nil + } + + for _, volume := range volumes { + if err := checkVolume(volume, field, restriction); err != nil { + return err + } + } + + return nil +} + +func checkVolume(volume corev1.Volume, field string, restriction *FieldRestriction) error { + switch field { + case "name": + return restriction.checkString(&volume.Name) + default: + volType := volumeSourceType(&volume) + if volType != "" && volType == field { + return restriction.checkAny() + } + } + + return nil +} + +func checkPodSecurityContext(sc *corev1.PodSecurityContext, field string, restriction *FieldRestriction) error { + if sc == nil { + return nil + } + + root, remaining, _ := strings.Cut(field, ".") + + switch root { + case "seLinuxOptions": + if sc.SELinuxOptions != nil { + return restriction.checkAny() + } + case "runAsUser": + return restriction.checkInt64(sc.RunAsUser) + case "runAsGroup": + return restriction.checkInt64(sc.RunAsGroup) + case "runAsNonRoot": + return restriction.checkBool(sc.RunAsNonRoot) + case "supplementalGroups": + return checkSupplementalGroups(sc.SupplementalGroups, remaining, restriction) + case "supplementalGroupsPolicy": + return restriction.checkString((*string)(sc.SupplementalGroupsPolicy)) + case "fsGroup": + return restriction.checkInt64(sc.FSGroup) + case "sysctls": + return checkSysctls(sc.Sysctls, remaining, restriction) + case "fsGroupChangePolicy": + return restriction.checkString((*string)(sc.FSGroupChangePolicy)) + case "seccompProfile": + if sc.SeccompProfile != nil { + return restriction.checkAny() + } + case "appArmorProfile": + if sc.AppArmorProfile != nil { + return restriction.checkAny() + } + case "seLinuxChangePolicy": + return restriction.checkString((*string)(sc.SELinuxChangePolicy)) + } + + return nil +} + +func checkSysctls(sysctls []corev1.Sysctl, field string, restriction *FieldRestriction) error { + if len(sysctls) == 0 { + return nil + } + + if err := restriction.checkAny(); err != nil { + return err + } + + for _, sysctl := range sysctls { + switch field { + case "name": + if err := restriction.checkString(&sysctl.Name); err != nil { + return err + } + } + } + + return nil +} + +func checkSupplementalGroups(supplementalGroups []int64, field string, restriction *FieldRestriction) error { + if len(supplementalGroups) == 0 { + return nil + } + + if err := restriction.checkAny(); err != nil { + return err + } + + for _, supplementalGroup := range supplementalGroups { + if field == "" { + if err := restriction.checkInt64(&supplementalGroup); err != nil { + return err + } + } + } + + return nil +} + +func checkResourceClaims(claims []corev1.PodResourceClaim, field string, restriction *FieldRestriction) error { + if len(claims) == 0 { + return nil + } + + for _, claim := range claims { + if err := checkResourceClaim(claim, field, restriction); err != nil { + return err + } + } + + return nil +} + +func checkResourceClaim(claim corev1.PodResourceClaim, field string, restriction *FieldRestriction) error { + switch field { + case "name": + return restriction.checkString(&claim.Name) + case "resourceClaimName": + return restriction.checkString(claim.ResourceClaimName) + case "resourceClaimTemplateName": + return restriction.checkString(claim.ResourceClaimTemplateName) + } + + return nil +} + +func checkImagePullSecrets(secrets []corev1.LocalObjectReference, field string, restriction *FieldRestriction) error { + if len(secrets) == 0 { + return nil + } + + for _, secret := range secrets { + switch field { + case "name": + if err := restriction.checkString(&secret.Name); err != nil { + return err + } + } + } + + return nil +} + +func volumeSourceType(vol *corev1.Volume) string { + src := vol.VolumeSource + switch { + case src.HostPath != nil: + return "hostPath" + case src.EmptyDir != nil: + return "emptyDir" + case src.Secret != nil: + return "secret" + case src.ConfigMap != nil: + return "configMap" + case src.PersistentVolumeClaim != nil: + return "persistentVolumeClaim" + case src.Projected != nil: + return "projected" + case src.DownwardAPI != nil: + return "downwardAPI" + case src.CSI != nil: + return "csi" + case src.Ephemeral != nil: + return "ephemeral" + case src.NFS != nil: + return "nfs" + case src.FC != nil: + return "fc" + case src.Image != nil: + return "image" + case src.ISCSI != nil: + return "iscsi" + case src.GCEPersistentDisk != nil: + return "gcePersistentDisk" + case src.AWSElasticBlockStore != nil: + return "awsElasticBlockStore" + case src.GitRepo != nil: + return "gitRepo" + case src.Glusterfs != nil: + return "glusterfs" + case src.RBD != nil: + return "rbd" + case src.FlexVolume != nil: + return "flexVolume" + case src.Cinder != nil: + return "cinder" + case src.CephFS != nil: + return "cephfs" + case src.Flocker != nil: + return "flocker" + case src.AzureFile != nil: + return "azureFile" + case src.VsphereVolume != nil: + return "vsphereVolume" + case src.Quobyte != nil: + return "quobyte" + case src.AzureDisk != nil: + return "azureDisk" + case src.PhotonPersistentDisk != nil: + return "photonPersistentDisk" + case src.PortworxVolume != nil: + return "portworxVolume" + case src.ScaleIO != nil: + return "scaleIO" + case src.StorageOS != nil: + return "storageos" + default: + return "" + } +} diff --git a/pkg/library/overrides/pod_restrictions_test.go b/pkg/library/overrides/pod_restrictions_test.go new file mode 100644 index 000000000..8cb6fb0ec --- /dev/null +++ b/pkg/library/overrides/pod_restrictions_test.go @@ -0,0 +1,1510 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package overrides + +import ( + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/ptr" + + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" +) + +func TestRestrictPodOverride(t *testing.T) { + tests := []struct { + Name string + RestrictedFields []string + Override corev1.PodSpec + IsErrorExpected bool + ErrField string + }{ + { + Name: "no denied fields allows everything", + Override: corev1.PodSpec{}, + }, + { + Name: "containers always denied", + Override: corev1.PodSpec{Containers: []corev1.Container{{}}}, + IsErrorExpected: true, + ErrField: "containers", + }, + { + Name: "initContainers always denied", + Override: corev1.PodSpec{InitContainers: []corev1.Container{{}}}, + IsErrorExpected: true, + ErrField: "initContainers", + }, + // ----------- RestartPolicy ----------- + { + Name: "restartPolicy empty value not restricted", + Override: corev1.PodSpec{RestartPolicy: ""}, + RestrictedFields: []string{"restartPolicy"}, + IsErrorExpected: false, + }, + { + Name: "restartPolicy restricted by specific value match", + Override: corev1.PodSpec{RestartPolicy: corev1.RestartPolicyAlways}, + RestrictedFields: []string{"restartPolicy=Always"}, + IsErrorExpected: true, + ErrField: "restartPolicy=Always", + }, + { + Name: "restartPolicy restricted by any value", + Override: corev1.PodSpec{RestartPolicy: corev1.RestartPolicyAlways}, + RestrictedFields: []string{"restartPolicy"}, + IsErrorExpected: true, + ErrField: "restartPolicy", + }, + { + Name: "restartPolicy allowed when restricted value does not match", + Override: corev1.PodSpec{RestartPolicy: corev1.RestartPolicyAlways}, + RestrictedFields: []string{"restartPolicy=Never"}, + IsErrorExpected: false, + }, + // ----------- TerminationGracePeriodSeconds ----------- + { + Name: "terminationGracePeriodSeconds nil value not restricted", + Override: corev1.PodSpec{TerminationGracePeriodSeconds: nil}, + RestrictedFields: []string{"terminationGracePeriodSeconds"}, + IsErrorExpected: false, + }, + { + Name: "terminationGracePeriodSeconds restricted by specific value match", + Override: corev1.PodSpec{TerminationGracePeriodSeconds: ptr.To(int64(30))}, + RestrictedFields: []string{"terminationGracePeriodSeconds=30"}, + IsErrorExpected: true, + ErrField: "terminationGracePeriodSeconds=30", + }, + { + Name: "terminationGracePeriodSeconds restricted by any value", + Override: corev1.PodSpec{TerminationGracePeriodSeconds: ptr.To(int64(30))}, + RestrictedFields: []string{"terminationGracePeriodSeconds"}, + IsErrorExpected: true, + ErrField: "terminationGracePeriodSeconds", + }, + { + Name: "terminationGracePeriodSeconds allowed when restricted value does not match", + Override: corev1.PodSpec{TerminationGracePeriodSeconds: ptr.To(int64(30))}, + RestrictedFields: []string{"terminationGracePeriodSeconds=60"}, + IsErrorExpected: false, + }, + // ----------- ActiveDeadlineSeconds ----------- + { + Name: "activeDeadlineSeconds nil value not restricted", + Override: corev1.PodSpec{ActiveDeadlineSeconds: nil}, + RestrictedFields: []string{"activeDeadlineSeconds"}, + IsErrorExpected: false, + }, + { + Name: "activeDeadlineSeconds restricted by specific value match", + Override: corev1.PodSpec{ActiveDeadlineSeconds: ptr.To(int64(600))}, + RestrictedFields: []string{"activeDeadlineSeconds=600"}, + IsErrorExpected: true, + ErrField: "activeDeadlineSeconds=600", + }, + { + Name: "activeDeadlineSeconds restricted by any value", + Override: corev1.PodSpec{ActiveDeadlineSeconds: ptr.To(int64(600))}, + RestrictedFields: []string{"activeDeadlineSeconds"}, + IsErrorExpected: true, + ErrField: "activeDeadlineSeconds", + }, + { + Name: "activeDeadlineSeconds allowed when restricted value does not match", + Override: corev1.PodSpec{ActiveDeadlineSeconds: ptr.To(int64(600))}, + RestrictedFields: []string{"activeDeadlineSeconds=300"}, + IsErrorExpected: false, + }, + // ----------- DNSPolicy ----------- + { + Name: "dnsPolicy empty value not restricted", + Override: corev1.PodSpec{DNSPolicy: ""}, + RestrictedFields: []string{"dnsPolicy"}, + IsErrorExpected: false, + }, + { + Name: "dnsPolicy restricted by specific value match", + Override: corev1.PodSpec{DNSPolicy: corev1.DNSClusterFirst}, + RestrictedFields: []string{"dnsPolicy=ClusterFirst"}, + IsErrorExpected: true, + ErrField: "dnsPolicy=ClusterFirst", + }, + { + Name: "dnsPolicy restricted by any value", + Override: corev1.PodSpec{DNSPolicy: corev1.DNSClusterFirst}, + RestrictedFields: []string{"dnsPolicy"}, + IsErrorExpected: true, + ErrField: "dnsPolicy", + }, + { + Name: "dnsPolicy allowed when restricted value does not match", + Override: corev1.PodSpec{DNSPolicy: corev1.DNSClusterFirst}, + RestrictedFields: []string{"dnsPolicy=Default"}, + IsErrorExpected: false, + }, + // ----------- NodeSelector ----------- + { + Name: "nodeSelector empty map not restricted", + Override: corev1.PodSpec{NodeSelector: map[string]string{}}, + RestrictedFields: []string{"nodeSelector"}, + IsErrorExpected: false, + }, + { + Name: "nodeSelector restricted by any value", + Override: corev1.PodSpec{NodeSelector: map[string]string{"disktype": "ssd"}}, + RestrictedFields: []string{"nodeSelector"}, + IsErrorExpected: true, + ErrField: "nodeSelector", + }, + // ----------- ServiceAccountName ----------- + { + Name: "serviceAccountName empty value not restricted", + Override: corev1.PodSpec{ServiceAccountName: ""}, + RestrictedFields: []string{"serviceAccountName"}, + IsErrorExpected: false, + }, + { + Name: "serviceAccountName restricted by specific value match", + Override: corev1.PodSpec{ServiceAccountName: "my-sa"}, + RestrictedFields: []string{"serviceAccountName=my-sa"}, + IsErrorExpected: true, + ErrField: "serviceAccountName=my-sa", + }, + { + Name: "serviceAccountName restricted by any value", + Override: corev1.PodSpec{ServiceAccountName: "my-sa"}, + RestrictedFields: []string{"serviceAccountName"}, + IsErrorExpected: true, + ErrField: "serviceAccountName", + }, + { + Name: "serviceAccountName allowed when restricted value does not match", + Override: corev1.PodSpec{ServiceAccountName: "my-sa"}, + RestrictedFields: []string{"serviceAccountName=other-sa"}, + IsErrorExpected: false, + }, + // ----------- DeprecatedServiceAccount ----------- + { + Name: "deprecatedServiceAccount empty value not restricted", + Override: corev1.PodSpec{DeprecatedServiceAccount: ""}, + RestrictedFields: []string{"deprecatedServiceAccount"}, + IsErrorExpected: false, + }, + { + Name: "deprecatedServiceAccount restricted by specific value match", + Override: corev1.PodSpec{DeprecatedServiceAccount: "my-sa"}, + RestrictedFields: []string{"deprecatedServiceAccount=my-sa"}, + IsErrorExpected: true, + ErrField: "deprecatedServiceAccount=my-sa", + }, + { + Name: "deprecatedServiceAccount restricted by any value", + Override: corev1.PodSpec{DeprecatedServiceAccount: "my-sa"}, + RestrictedFields: []string{"deprecatedServiceAccount"}, + IsErrorExpected: true, + ErrField: "deprecatedServiceAccount", + }, + { + Name: "deprecatedServiceAccount allowed when restricted value does not match", + Override: corev1.PodSpec{DeprecatedServiceAccount: "my-sa"}, + RestrictedFields: []string{"deprecatedServiceAccount=other-sa"}, + IsErrorExpected: false, + }, + // ----------- AutomountServiceAccountToken ----------- + { + Name: "automountServiceAccountToken nil value not restricted", + Override: corev1.PodSpec{AutomountServiceAccountToken: nil}, + RestrictedFields: []string{"automountServiceAccountToken"}, + IsErrorExpected: false, + }, + { + Name: "automountServiceAccountToken restricted by specific value match", + Override: corev1.PodSpec{AutomountServiceAccountToken: ptr.To(true)}, + RestrictedFields: []string{"automountServiceAccountToken=true"}, + IsErrorExpected: true, + ErrField: "automountServiceAccountToken=true", + }, + { + Name: "automountServiceAccountToken restricted by any value", + Override: corev1.PodSpec{AutomountServiceAccountToken: ptr.To(true)}, + RestrictedFields: []string{"automountServiceAccountToken"}, + IsErrorExpected: true, + ErrField: "automountServiceAccountToken", + }, + { + Name: "automountServiceAccountToken allowed when restricted value does not match", + Override: corev1.PodSpec{AutomountServiceAccountToken: ptr.To(true)}, + RestrictedFields: []string{"automountServiceAccountToken=false"}, + IsErrorExpected: false, + }, + // ----------- NodeName ----------- + { + Name: "nodeName empty value not restricted", + Override: corev1.PodSpec{NodeName: ""}, + RestrictedFields: []string{"nodeName"}, + IsErrorExpected: false, + }, + { + Name: "nodeName restricted by specific value match", + Override: corev1.PodSpec{NodeName: "node-1"}, + RestrictedFields: []string{"nodeName=node-1"}, + IsErrorExpected: true, + ErrField: "nodeName=node-1", + }, + { + Name: "nodeName restricted by any value", + Override: corev1.PodSpec{NodeName: "node-1"}, + RestrictedFields: []string{"nodeName"}, + IsErrorExpected: true, + ErrField: "nodeName", + }, + { + Name: "nodeName allowed when restricted value does not match", + Override: corev1.PodSpec{NodeName: "node-1"}, + RestrictedFields: []string{"nodeName=node-2"}, + IsErrorExpected: false, + }, + // ----------- HostIPC ----------- + { + Name: "hostIPC false value restricted by any value", + Override: corev1.PodSpec{HostIPC: false}, + RestrictedFields: []string{"hostIPC"}, + IsErrorExpected: true, + ErrField: "hostIPC", + }, + { + Name: "hostIPC restricted by specific value match", + Override: corev1.PodSpec{HostIPC: true}, + RestrictedFields: []string{"hostIPC=true"}, + IsErrorExpected: true, + ErrField: "hostIPC=true", + }, + { + Name: "hostIPC restricted by any value", + Override: corev1.PodSpec{HostIPC: true}, + RestrictedFields: []string{"hostIPC"}, + IsErrorExpected: true, + ErrField: "hostIPC", + }, + { + Name: "hostIPC allowed when restricted value does not match", + Override: corev1.PodSpec{HostIPC: true}, + RestrictedFields: []string{"hostIPC=false"}, + IsErrorExpected: false, + }, + // ----------- HostPID ----------- + { + Name: "hostPID false value restricted by any value", + Override: corev1.PodSpec{HostPID: false}, + RestrictedFields: []string{"hostPID"}, + IsErrorExpected: true, + ErrField: "hostPID", + }, + { + Name: "hostPID restricted by specific value match", + Override: corev1.PodSpec{HostPID: true}, + RestrictedFields: []string{"hostPID=true"}, + IsErrorExpected: true, + ErrField: "hostPID=true", + }, + { + Name: "hostPID restricted by any value", + Override: corev1.PodSpec{HostPID: true}, + RestrictedFields: []string{"hostPID"}, + IsErrorExpected: true, + ErrField: "hostPID", + }, + { + Name: "hostPID allowed when restricted value does not match", + Override: corev1.PodSpec{HostPID: true}, + RestrictedFields: []string{"hostPID=false"}, + IsErrorExpected: false, + }, + // ----------- HostNetwork ----------- + { + Name: "hostNetwork false value restricted by any value", + Override: corev1.PodSpec{HostNetwork: false}, + RestrictedFields: []string{"hostNetwork"}, + IsErrorExpected: true, + ErrField: "hostNetwork", + }, + { + Name: "hostNetwork restricted by specific value match", + Override: corev1.PodSpec{HostNetwork: true}, + RestrictedFields: []string{"hostNetwork=true"}, + IsErrorExpected: true, + ErrField: "hostNetwork=true", + }, + { + Name: "hostNetwork restricted by any value", + Override: corev1.PodSpec{HostNetwork: true}, + RestrictedFields: []string{"hostNetwork"}, + IsErrorExpected: true, + ErrField: "hostNetwork", + }, + { + Name: "hostNetwork allowed when restricted value does not match", + Override: corev1.PodSpec{HostNetwork: true}, + RestrictedFields: []string{"hostNetwork=false"}, + IsErrorExpected: false, + }, + // ----------- ShareProcessNamespace ----------- + { + Name: "shareProcessNamespace nil value not restricted", + Override: corev1.PodSpec{ShareProcessNamespace: nil}, + RestrictedFields: []string{"shareProcessNamespace"}, + IsErrorExpected: false, + }, + { + Name: "shareProcessNamespace restricted by specific value match", + Override: corev1.PodSpec{ShareProcessNamespace: ptr.To(true)}, + RestrictedFields: []string{"shareProcessNamespace=true"}, + IsErrorExpected: true, + ErrField: "shareProcessNamespace=true", + }, + { + Name: "shareProcessNamespace restricted by any value", + Override: corev1.PodSpec{ShareProcessNamespace: ptr.To(true)}, + RestrictedFields: []string{"shareProcessNamespace"}, + IsErrorExpected: true, + ErrField: "shareProcessNamespace", + }, + { + Name: "shareProcessNamespace allowed when restricted value does not match", + Override: corev1.PodSpec{ShareProcessNamespace: ptr.To(true)}, + RestrictedFields: []string{"shareProcessNamespace=false"}, + IsErrorExpected: false, + }, + // ----------- Hostname ----------- + { + Name: "hostname empty value not restricted", + Override: corev1.PodSpec{Hostname: ""}, + RestrictedFields: []string{"hostname"}, + IsErrorExpected: false, + }, + { + Name: "hostname restricted by specific value match", + Override: corev1.PodSpec{Hostname: "my-host"}, + RestrictedFields: []string{"hostname=my-host"}, + IsErrorExpected: true, + ErrField: "hostname=my-host", + }, + { + Name: "hostname restricted by any value", + Override: corev1.PodSpec{Hostname: "my-host"}, + RestrictedFields: []string{"hostname"}, + IsErrorExpected: true, + ErrField: "hostname", + }, + { + Name: "hostname allowed when restricted value does not match", + Override: corev1.PodSpec{Hostname: "my-host"}, + RestrictedFields: []string{"hostname=other-host"}, + IsErrorExpected: false, + }, + // ----------- Subdomain ----------- + { + Name: "subdomain empty value not restricted", + Override: corev1.PodSpec{Subdomain: ""}, + RestrictedFields: []string{"subdomain"}, + IsErrorExpected: false, + }, + { + Name: "subdomain restricted by specific value match", + Override: corev1.PodSpec{Subdomain: "my-subdomain"}, + RestrictedFields: []string{"subdomain=my-subdomain"}, + IsErrorExpected: true, + ErrField: "subdomain=my-subdomain", + }, + { + Name: "subdomain restricted by any value", + Override: corev1.PodSpec{Subdomain: "my-subdomain"}, + RestrictedFields: []string{"subdomain"}, + IsErrorExpected: true, + ErrField: "subdomain", + }, + { + Name: "subdomain allowed when restricted value does not match", + Override: corev1.PodSpec{Subdomain: "my-subdomain"}, + RestrictedFields: []string{"subdomain=other"}, + IsErrorExpected: false, + }, + // ----------- SchedulerName ----------- + { + Name: "schedulerName empty value not restricted", + Override: corev1.PodSpec{SchedulerName: ""}, + RestrictedFields: []string{"schedulerName"}, + IsErrorExpected: false, + }, + { + Name: "schedulerName restricted by specific value match", + Override: corev1.PodSpec{SchedulerName: "custom-scheduler"}, + RestrictedFields: []string{"schedulerName=custom-scheduler"}, + IsErrorExpected: true, + ErrField: "schedulerName=custom-scheduler", + }, + { + Name: "schedulerName restricted by any value", + Override: corev1.PodSpec{SchedulerName: "custom-scheduler"}, + RestrictedFields: []string{"schedulerName"}, + IsErrorExpected: true, + ErrField: "schedulerName", + }, + { + Name: "schedulerName allowed when restricted value does not match", + Override: corev1.PodSpec{SchedulerName: "custom-scheduler"}, + RestrictedFields: []string{"schedulerName=default-scheduler"}, + IsErrorExpected: false, + }, + // ----------- PriorityClassName ----------- + { + Name: "priorityClassName empty value not restricted", + Override: corev1.PodSpec{PriorityClassName: ""}, + RestrictedFields: []string{"priorityClassName"}, + IsErrorExpected: false, + }, + { + Name: "priorityClassName restricted by specific value match", + Override: corev1.PodSpec{PriorityClassName: "high-priority"}, + RestrictedFields: []string{"priorityClassName=high-priority"}, + IsErrorExpected: true, + ErrField: "priorityClassName=high-priority", + }, + { + Name: "priorityClassName restricted by any value", + Override: corev1.PodSpec{PriorityClassName: "high-priority"}, + RestrictedFields: []string{"priorityClassName"}, + IsErrorExpected: true, + ErrField: "priorityClassName", + }, + { + Name: "priorityClassName allowed when restricted value does not match", + Override: corev1.PodSpec{PriorityClassName: "high-priority"}, + RestrictedFields: []string{"priorityClassName=low-priority"}, + IsErrorExpected: false, + }, + // ----------- Priority ----------- + { + Name: "priority nil value not restricted", + Override: corev1.PodSpec{Priority: nil}, + RestrictedFields: []string{"priority"}, + IsErrorExpected: false, + }, + { + Name: "priority restricted by specific value match", + Override: corev1.PodSpec{Priority: ptr.To(int32(1000))}, + RestrictedFields: []string{"priority=1000"}, + IsErrorExpected: true, + ErrField: "priority=1000", + }, + { + Name: "priority restricted by any value", + Override: corev1.PodSpec{Priority: ptr.To(int32(1000))}, + RestrictedFields: []string{"priority"}, + IsErrorExpected: true, + ErrField: "priority", + }, + { + Name: "priority allowed when restricted value does not match", + Override: corev1.PodSpec{Priority: ptr.To(int32(1000))}, + RestrictedFields: []string{"priority=2000"}, + IsErrorExpected: false, + }, + // ----------- RuntimeClassName ----------- + { + Name: "runtimeClassName nil value not restricted", + Override: corev1.PodSpec{RuntimeClassName: nil}, + RestrictedFields: []string{"runtimeClassName"}, + IsErrorExpected: false, + }, + { + Name: "runtimeClassName restricted by specific value match", + Override: corev1.PodSpec{RuntimeClassName: ptr.To("kata")}, + RestrictedFields: []string{"runtimeClassName=kata"}, + IsErrorExpected: true, + ErrField: "runtimeClassName=kata", + }, + { + Name: "runtimeClassName restricted by any value", + Override: corev1.PodSpec{RuntimeClassName: ptr.To("kata")}, + RestrictedFields: []string{"runtimeClassName"}, + IsErrorExpected: true, + ErrField: "runtimeClassName", + }, + { + Name: "runtimeClassName allowed when restricted value does not match", + Override: corev1.PodSpec{RuntimeClassName: ptr.To("kata")}, + RestrictedFields: []string{"runtimeClassName=gvisor"}, + IsErrorExpected: false, + }, + // ----------- EnableServiceLinks ----------- + { + Name: "enableServiceLinks nil value not restricted", + Override: corev1.PodSpec{EnableServiceLinks: nil}, + RestrictedFields: []string{"enableServiceLinks"}, + IsErrorExpected: false, + }, + { + Name: "enableServiceLinks restricted by specific value match", + Override: corev1.PodSpec{EnableServiceLinks: ptr.To(true)}, + RestrictedFields: []string{"enableServiceLinks=true"}, + IsErrorExpected: true, + ErrField: "enableServiceLinks=true", + }, + { + Name: "enableServiceLinks restricted by any value", + Override: corev1.PodSpec{EnableServiceLinks: ptr.To(true)}, + RestrictedFields: []string{"enableServiceLinks"}, + IsErrorExpected: true, + ErrField: "enableServiceLinks", + }, + { + Name: "enableServiceLinks allowed when restricted value does not match", + Override: corev1.PodSpec{EnableServiceLinks: ptr.To(true)}, + RestrictedFields: []string{"enableServiceLinks=false"}, + IsErrorExpected: false, + }, + // ----------- PreemptionPolicy ----------- + { + Name: "preemptionPolicy nil value not restricted", + Override: corev1.PodSpec{PreemptionPolicy: nil}, + RestrictedFields: []string{"preemptionPolicy"}, + IsErrorExpected: false, + }, + { + Name: "preemptionPolicy restricted by specific value match", + Override: corev1.PodSpec{PreemptionPolicy: preemptionPolicyPtr(corev1.PreemptNever)}, + RestrictedFields: []string{"preemptionPolicy=Never"}, + IsErrorExpected: true, + ErrField: "preemptionPolicy=Never", + }, + { + Name: "preemptionPolicy restricted by any value", + Override: corev1.PodSpec{PreemptionPolicy: preemptionPolicyPtr(corev1.PreemptNever)}, + RestrictedFields: []string{"preemptionPolicy"}, + IsErrorExpected: true, + ErrField: "preemptionPolicy", + }, + { + Name: "preemptionPolicy allowed when restricted value does not match", + Override: corev1.PodSpec{PreemptionPolicy: preemptionPolicyPtr(corev1.PreemptNever)}, + RestrictedFields: []string{"preemptionPolicy=PreemptLowerPriority"}, + IsErrorExpected: false, + }, + // ----------- SetHostnameAsFQDN ----------- + { + Name: "setHostnameAsFQDN nil value not restricted", + Override: corev1.PodSpec{SetHostnameAsFQDN: nil}, + RestrictedFields: []string{"setHostnameAsFQDN"}, + IsErrorExpected: false, + }, + { + Name: "setHostnameAsFQDN restricted by specific value match", + Override: corev1.PodSpec{SetHostnameAsFQDN: ptr.To(true)}, + RestrictedFields: []string{"setHostnameAsFQDN=true"}, + IsErrorExpected: true, + ErrField: "setHostnameAsFQDN=true", + }, + { + Name: "setHostnameAsFQDN restricted by any value", + Override: corev1.PodSpec{SetHostnameAsFQDN: ptr.To(true)}, + RestrictedFields: []string{"setHostnameAsFQDN"}, + IsErrorExpected: true, + ErrField: "setHostnameAsFQDN", + }, + { + Name: "setHostnameAsFQDN allowed when restricted value does not match", + Override: corev1.PodSpec{SetHostnameAsFQDN: ptr.To(true)}, + RestrictedFields: []string{"setHostnameAsFQDN=false"}, + IsErrorExpected: false, + }, + // ----------- HostUsers ----------- + { + Name: "hostUsers nil value not restricted", + Override: corev1.PodSpec{HostUsers: nil}, + RestrictedFields: []string{"hostUsers"}, + IsErrorExpected: false, + }, + { + Name: "hostUsers restricted by specific value match", + Override: corev1.PodSpec{HostUsers: ptr.To(true)}, + RestrictedFields: []string{"hostUsers=true"}, + IsErrorExpected: true, + ErrField: "hostUsers=true", + }, + { + Name: "hostUsers restricted by any value", + Override: corev1.PodSpec{HostUsers: ptr.To(true)}, + RestrictedFields: []string{"hostUsers"}, + IsErrorExpected: true, + ErrField: "hostUsers", + }, + { + Name: "hostUsers allowed when restricted value does not match", + Override: corev1.PodSpec{HostUsers: ptr.To(true)}, + RestrictedFields: []string{"hostUsers=false"}, + IsErrorExpected: false, + }, + // ----------- HostnameOverride ----------- + { + Name: "hostnameOverride nil value not restricted", + Override: corev1.PodSpec{HostnameOverride: nil}, + RestrictedFields: []string{"hostnameOverride"}, + IsErrorExpected: false, + }, + { + Name: "hostnameOverride restricted by specific value match", + Override: corev1.PodSpec{HostnameOverride: ptr.To("custom-hostname")}, + RestrictedFields: []string{"hostnameOverride=custom-hostname"}, + IsErrorExpected: true, + ErrField: "hostnameOverride=custom-hostname", + }, + { + Name: "hostnameOverride restricted by any value", + Override: corev1.PodSpec{HostnameOverride: ptr.To("custom-hostname")}, + RestrictedFields: []string{"hostnameOverride"}, + IsErrorExpected: true, + ErrField: "hostnameOverride", + }, + { + Name: "hostnameOverride allowed when restricted value does not match", + Override: corev1.PodSpec{HostnameOverride: ptr.To("custom-hostname")}, + RestrictedFields: []string{"hostnameOverride=other-hostname"}, + IsErrorExpected: false, + }, + // ----------- EphemeralContainers ----------- + { + Name: "ephemeralContainers empty slice not restricted", + Override: corev1.PodSpec{EphemeralContainers: nil}, + RestrictedFields: []string{"ephemeralContainers"}, + IsErrorExpected: false, + }, + { + Name: "ephemeralContainers restricted by any value", + Override: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{{}}}, + RestrictedFields: []string{"ephemeralContainers"}, + IsErrorExpected: true, + ErrField: "ephemeralContainers", + }, + // ----------- Affinity ----------- + { + Name: "affinity nil not restricted", + Override: corev1.PodSpec{Affinity: nil}, + RestrictedFields: []string{"affinity"}, + IsErrorExpected: false, + }, + { + Name: "affinity restricted by any value", + Override: corev1.PodSpec{Affinity: &corev1.Affinity{}}, + RestrictedFields: []string{"affinity"}, + IsErrorExpected: true, + ErrField: "affinity", + }, + // ----------- Tolerations ----------- + { + Name: "tolerations empty slice not restricted", + Override: corev1.PodSpec{Tolerations: nil}, + RestrictedFields: []string{"tolerations"}, + IsErrorExpected: false, + }, + { + Name: "tolerations restricted by any value", + Override: corev1.PodSpec{Tolerations: []corev1.Toleration{{}}}, + RestrictedFields: []string{"tolerations"}, + IsErrorExpected: true, + ErrField: "tolerations", + }, + // ----------- HostAliases ----------- + { + Name: "hostAliases empty slice not restricted", + Override: corev1.PodSpec{HostAliases: nil}, + RestrictedFields: []string{"hostAliases"}, + IsErrorExpected: false, + }, + { + Name: "hostAliases restricted by any value", + Override: corev1.PodSpec{HostAliases: []corev1.HostAlias{{}}}, + RestrictedFields: []string{"hostAliases"}, + IsErrorExpected: true, + ErrField: "hostAliases", + }, + // ----------- DNSConfig ----------- + { + Name: "dnsConfig nil not restricted", + Override: corev1.PodSpec{DNSConfig: nil}, + RestrictedFields: []string{"dnsConfig"}, + IsErrorExpected: false, + }, + { + Name: "dnsConfig restricted by any value", + Override: corev1.PodSpec{DNSConfig: &corev1.PodDNSConfig{}}, + RestrictedFields: []string{"dnsConfig"}, + IsErrorExpected: true, + ErrField: "dnsConfig", + }, + // ----------- ReadinessGates ----------- + { + Name: "readinessGates empty slice not restricted", + Override: corev1.PodSpec{ReadinessGates: nil}, + RestrictedFields: []string{"readinessGates"}, + IsErrorExpected: false, + }, + { + Name: "readinessGates restricted by any value", + Override: corev1.PodSpec{ReadinessGates: []corev1.PodReadinessGate{{}}}, + RestrictedFields: []string{"readinessGates"}, + IsErrorExpected: true, + ErrField: "readinessGates", + }, + // ----------- TopologySpreadConstraints ----------- + { + Name: "topologySpreadConstraints empty slice not restricted", + Override: corev1.PodSpec{TopologySpreadConstraints: nil}, + RestrictedFields: []string{"topologySpreadConstraints"}, + IsErrorExpected: false, + }, + { + Name: "topologySpreadConstraints restricted by any value", + Override: corev1.PodSpec{TopologySpreadConstraints: []corev1.TopologySpreadConstraint{{}}}, + RestrictedFields: []string{"topologySpreadConstraints"}, + IsErrorExpected: true, + ErrField: "topologySpreadConstraints", + }, + // ----------- OS ----------- + { + Name: "os nil not restricted", + Override: corev1.PodSpec{OS: nil}, + RestrictedFields: []string{"os"}, + IsErrorExpected: false, + }, + { + Name: "os restricted by any value", + Override: corev1.PodSpec{OS: &corev1.PodOS{}}, + RestrictedFields: []string{"os"}, + IsErrorExpected: true, + ErrField: "os", + }, + // ----------- WorkloadRef ----------- + { + Name: "schedulingGates empty slice not restricted", + Override: corev1.PodSpec{WorkloadRef: nil}, + RestrictedFields: []string{"workloadRef"}, + IsErrorExpected: false, + }, + { + Name: "schedulingGates restricted by any value", + Override: corev1.PodSpec{WorkloadRef: &corev1.WorkloadReference{}}, + RestrictedFields: []string{"workloadRef"}, + IsErrorExpected: true, + ErrField: "workloadRef", + }, + // ----------- SchedulingGates ----------- + { + Name: "schedulingGates empty slice not restricted", + Override: corev1.PodSpec{SchedulingGates: nil}, + RestrictedFields: []string{"schedulingGates"}, + IsErrorExpected: false, + }, + { + Name: "schedulingGates restricted by any value", + Override: corev1.PodSpec{SchedulingGates: []corev1.PodSchedulingGate{{}}}, + RestrictedFields: []string{"schedulingGates"}, + IsErrorExpected: true, + ErrField: "schedulingGates", + }, + // ----------- Volumes ----------- + { + Name: "volumes empty slice not restricted", + Override: corev1.PodSpec{Volumes: nil}, + RestrictedFields: []string{"volumes"}, + IsErrorExpected: false, + }, + { + Name: "volumes restricted by any value", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{}}}, + RestrictedFields: []string{"volumes"}, + IsErrorExpected: true, + ErrField: "volumes", + }, + // ----------- Volumes.Name ----------- + { + Name: "volumes.name empty value not restricted", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: ""}}}, + RestrictedFields: []string{"volumes.name"}, + IsErrorExpected: false, + }, + { + Name: "volumes.name restricted by specific value match", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumes.name=my-volume"}, + IsErrorExpected: true, + ErrField: "volumes.name=my-volume", + }, + { + Name: "volumes.name restricted by any value", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumes.name"}, + IsErrorExpected: true, + ErrField: "volumes.name", + }, + { + Name: "volumes.name allowed when restricted value does not match", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: "my-volume"}}}, + RestrictedFields: []string{"volumes.name=other-volume"}, + IsErrorExpected: false, + }, + // ----------- Volumes source type ----------- + { + Name: "volumes.hostPath restricted by any value", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: "vol", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/host"}}}}}, + RestrictedFields: []string{"volumes.hostPath"}, + IsErrorExpected: true, + ErrField: "volumes.hostPath", + }, + { + Name: "volumes.emptyDir not restricted when restriction is for different type", + Override: corev1.PodSpec{Volumes: []corev1.Volume{{Name: "vol", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}}, + RestrictedFields: []string{"volumes.hostPath"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext ----------- + { + Name: "securityContext nil not restricted", + Override: corev1.PodSpec{SecurityContext: nil}, + RestrictedFields: []string{"securityContext"}, + IsErrorExpected: false, + }, + { + Name: "securityContext restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{}}, + RestrictedFields: []string{"securityContext"}, + IsErrorExpected: true, + ErrField: "securityContext", + }, + // ----------- SecurityContext.SELinuxOptions ----------- + { + Name: "securityContext.seLinuxOptions nil not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxOptions: nil}}, + RestrictedFields: []string{"securityContext.seLinuxOptions"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.seLinuxOptions restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxOptions: &corev1.SELinuxOptions{}}}, + RestrictedFields: []string{"securityContext.seLinuxOptions"}, + IsErrorExpected: true, + ErrField: "securityContext.seLinuxOptions", + }, + // ----------- SecurityContext.RunAsUser ----------- + { + Name: "securityContext.runAsUser nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsUser: nil}}, + RestrictedFields: []string{"securityContext.runAsUser"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsUser restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser=1000"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsUser=1000", + }, + { + Name: "securityContext.runAsUser restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsUser", + }, + { + Name: "securityContext.runAsUser allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsUser: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.runAsUser=2000"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.RunAsGroup ----------- + { + Name: "securityContext.runAsGroup nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsGroup: nil}}, + RestrictedFields: []string{"securityContext.runAsGroup"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsGroup restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup=500"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsGroup=500", + }, + { + Name: "securityContext.runAsGroup restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsGroup", + }, + { + Name: "securityContext.runAsGroup allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsGroup: ptr.To(int64(500))}}, + RestrictedFields: []string{"securityContext.runAsGroup=1000"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.RunAsNonRoot ----------- + { + Name: "securityContext.runAsNonRoot nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsNonRoot: nil}}, + RestrictedFields: []string{"securityContext.runAsNonRoot"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.runAsNonRoot restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot=true"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsNonRoot=true", + }, + { + Name: "securityContext.runAsNonRoot restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot"}, + IsErrorExpected: true, + ErrField: "securityContext.runAsNonRoot", + }, + { + Name: "securityContext.runAsNonRoot allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{RunAsNonRoot: ptr.To(true)}}, + RestrictedFields: []string{"securityContext.runAsNonRoot=false"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.SupplementalGroups ----------- + { + Name: "securityContext.supplementalGroups empty slice not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroups: nil}}, + RestrictedFields: []string{"securityContext.supplementalGroups"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.supplementalGroups restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroups: []int64{1000}}}, + RestrictedFields: []string{"securityContext.supplementalGroups"}, + IsErrorExpected: true, + ErrField: "securityContext.supplementalGroups", + }, + { + Name: "securityContext.supplementalGroups restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroups: []int64{1000}}}, + RestrictedFields: []string{"securityContext.supplementalGroups=1000"}, + IsErrorExpected: true, + ErrField: "securityContext.supplementalGroups=1000", + }, + { + Name: "securityContext.supplementalGroups allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroups: []int64{1000}}}, + RestrictedFields: []string{"securityContext.supplementalGroups=500"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.SupplementalGroupsPolicy ----------- + { + Name: "securityContext.supplementalGroupsPolicy nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroupsPolicy: nil}}, + RestrictedFields: []string{"securityContext.supplementalGroupsPolicy"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.supplementalGroupsPolicy restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroupsPolicy: supplementalGroupsPolicyPtr(corev1.SupplementalGroupsPolicyMerge)}}, + RestrictedFields: []string{"securityContext.supplementalGroupsPolicy=Merge"}, + IsErrorExpected: true, + ErrField: "securityContext.supplementalGroupsPolicy=Merge", + }, + { + Name: "securityContext.supplementalGroupsPolicy restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroupsPolicy: supplementalGroupsPolicyPtr(corev1.SupplementalGroupsPolicyMerge)}}, + RestrictedFields: []string{"securityContext.supplementalGroupsPolicy"}, + IsErrorExpected: true, + ErrField: "securityContext.supplementalGroupsPolicy", + }, + { + Name: "securityContext.supplementalGroupsPolicy allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SupplementalGroupsPolicy: supplementalGroupsPolicyPtr(corev1.SupplementalGroupsPolicyMerge)}}, + RestrictedFields: []string{"securityContext.supplementalGroupsPolicy=Strict"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.FSGroup ----------- + { + Name: "securityContext.fsGroup nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroup: nil}}, + RestrictedFields: []string{"securityContext.fsGroup"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.fsGroup restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroup: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.fsGroup=1000"}, + IsErrorExpected: true, + ErrField: "securityContext.fsGroup=1000", + }, + { + Name: "securityContext.fsGroup restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroup: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.fsGroup"}, + IsErrorExpected: true, + ErrField: "securityContext.fsGroup", + }, + { + Name: "securityContext.fsGroup allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroup: ptr.To(int64(1000))}}, + RestrictedFields: []string{"securityContext.fsGroup=2000"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.Sysctls ----------- + { + Name: "securityContext.sysctls empty slice not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{Sysctls: nil}}, + RestrictedFields: []string{"securityContext.sysctls"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.sysctls restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{Sysctls: []corev1.Sysctl{{Name: "net.core.somaxconn", Value: "1024"}}}}, + RestrictedFields: []string{"securityContext.sysctls"}, + IsErrorExpected: true, + ErrField: "securityContext.sysctls", + }, + { + Name: "securityContext.sysctls restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{Sysctls: []corev1.Sysctl{{Name: "net.core.somaxconn", Value: "1024"}}}}, + RestrictedFields: []string{"securityContext.sysctls.name=net.core.somaxconn"}, + IsErrorExpected: true, + ErrField: "securityContext.sysctls.name=net.core.somaxconn", + }, + { + Name: "securityContext.sysctls allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{Sysctls: []corev1.Sysctl{{Name: "net.core.somaxconn", Value: "1024"}}}}, + RestrictedFields: []string{"securityContext.sysctls.name=kernel.shm_rmid_forced"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.FSGroupChangePolicy ----------- + { + Name: "securityContext.fsGroupChangePolicy nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroupChangePolicy: nil}}, + RestrictedFields: []string{"securityContext.fsGroupChangePolicy"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.fsGroupChangePolicy restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroupChangePolicy: fsGroupChangePolicyPtr(corev1.FSGroupChangeOnRootMismatch)}}, + RestrictedFields: []string{"securityContext.fsGroupChangePolicy=OnRootMismatch"}, + IsErrorExpected: true, + ErrField: "securityContext.fsGroupChangePolicy=OnRootMismatch", + }, + { + Name: "securityContext.fsGroupChangePolicy restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroupChangePolicy: fsGroupChangePolicyPtr(corev1.FSGroupChangeOnRootMismatch)}}, + RestrictedFields: []string{"securityContext.fsGroupChangePolicy"}, + IsErrorExpected: true, + ErrField: "securityContext.fsGroupChangePolicy", + }, + { + Name: "securityContext.fsGroupChangePolicy allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{FSGroupChangePolicy: fsGroupChangePolicyPtr(corev1.FSGroupChangeOnRootMismatch)}}, + RestrictedFields: []string{"securityContext.fsGroupChangePolicy=Always"}, + IsErrorExpected: false, + }, + // ----------- SecurityContext.SeccompProfile ----------- + { + Name: "securityContext.seccompProfile nil not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SeccompProfile: nil}}, + RestrictedFields: []string{"securityContext.seccompProfile"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.seccompProfile restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{}}}, + RestrictedFields: []string{"securityContext.seccompProfile"}, + IsErrorExpected: true, + ErrField: "securityContext.seccompProfile", + }, + // ----------- SecurityContext.AppArmorProfile ----------- + { + Name: "securityContext.appArmorProfile nil not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{AppArmorProfile: nil}}, + RestrictedFields: []string{"securityContext.appArmorProfile"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.appArmorProfile restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{AppArmorProfile: &corev1.AppArmorProfile{}}}, + RestrictedFields: []string{"securityContext.appArmorProfile"}, + IsErrorExpected: true, + ErrField: "securityContext.appArmorProfile", + }, + // ----------- SecurityContext.SELinuxChangePolicy ----------- + { + Name: "securityContext.seLinuxChangePolicy nil value not restricted", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxChangePolicy: nil}}, + RestrictedFields: []string{"securityContext.seLinuxChangePolicy"}, + IsErrorExpected: false, + }, + { + Name: "securityContext.seLinuxChangePolicy restricted by specific value match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxChangePolicy: seLinuxChangePolicyPtr(corev1.SELinuxChangePolicyRecursive)}}, + RestrictedFields: []string{"securityContext.seLinuxChangePolicy=Recursive"}, + IsErrorExpected: true, + ErrField: "securityContext.seLinuxChangePolicy=Recursive", + }, + { + Name: "securityContext.seLinuxChangePolicy restricted by any value", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxChangePolicy: seLinuxChangePolicyPtr(corev1.SELinuxChangePolicyRecursive)}}, + RestrictedFields: []string{"securityContext.seLinuxChangePolicy"}, + IsErrorExpected: true, + ErrField: "securityContext.seLinuxChangePolicy", + }, + { + Name: "securityContext.seLinuxChangePolicy allowed when restricted value does not match", + Override: corev1.PodSpec{SecurityContext: &corev1.PodSecurityContext{SELinuxChangePolicy: seLinuxChangePolicyPtr(corev1.SELinuxChangePolicyRecursive)}}, + RestrictedFields: []string{"securityContext.seLinuxChangePolicy=MountOption"}, + IsErrorExpected: false, + }, + // ----------- ImagePullSecrets ----------- + { + Name: "imagePullSecrets empty slice not restricted", + Override: corev1.PodSpec{ImagePullSecrets: nil}, + RestrictedFields: []string{"imagePullSecrets"}, + IsErrorExpected: false, + }, + { + Name: "imagePullSecrets restricted by any value", + Override: corev1.PodSpec{ImagePullSecrets: []corev1.LocalObjectReference{{}}}, + RestrictedFields: []string{"imagePullSecrets"}, + IsErrorExpected: true, + ErrField: "imagePullSecrets", + }, + // ----------- ImagePullSecrets.Name ----------- + { + Name: "imagePullSecrets.name empty value not restricted", + Override: corev1.PodSpec{ImagePullSecrets: []corev1.LocalObjectReference{{Name: ""}}}, + RestrictedFields: []string{"imagePullSecrets.name"}, + IsErrorExpected: false, + }, + { + Name: "imagePullSecrets.name restricted by specific value match", + Override: corev1.PodSpec{ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-secret"}}}, + RestrictedFields: []string{"imagePullSecrets.name=my-secret"}, + IsErrorExpected: true, + ErrField: "imagePullSecrets.name=my-secret", + }, + { + Name: "imagePullSecrets.name restricted by any value", + Override: corev1.PodSpec{ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-secret"}}}, + RestrictedFields: []string{"imagePullSecrets.name"}, + IsErrorExpected: true, + ErrField: "imagePullSecrets.name", + }, + { + Name: "imagePullSecrets.name allowed when restricted value does not match", + Override: corev1.PodSpec{ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-secret"}}}, + RestrictedFields: []string{"imagePullSecrets.name=other-secret"}, + IsErrorExpected: false, + }, + // ----------- Overhead ----------- + { + Name: "overhead empty not restricted", + Override: corev1.PodSpec{Overhead: nil}, + RestrictedFields: []string{"overhead"}, + IsErrorExpected: false, + }, + { + Name: "overhead restricted by any value", + Override: corev1.PodSpec{Overhead: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")}}, + RestrictedFields: []string{"overhead"}, + IsErrorExpected: true, + ErrField: "overhead", + }, + // ----------- Overhead.CPU ----------- + { + Name: "overhead.cpu restricted by any value", + Override: corev1.PodSpec{Overhead: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")}}, + RestrictedFields: []string{"overhead.cpu"}, + IsErrorExpected: true, + ErrField: "overhead.cpu", + }, + // ----------- Overhead.Memory ----------- + { + Name: "overhead.memory restricted by any value", + Override: corev1.PodSpec{Overhead: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("128Mi")}}, + RestrictedFields: []string{"overhead.memory"}, + IsErrorExpected: true, + ErrField: "overhead.memory", + }, + // ----------- ResourceClaims ----------- + { + Name: "resourceClaims empty slice not restricted", + Override: corev1.PodSpec{ResourceClaims: nil}, + RestrictedFields: []string{"resourceClaims"}, + IsErrorExpected: false, + }, + { + Name: "resourceClaims restricted by any value", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{}}}, + RestrictedFields: []string{"resourceClaims"}, + IsErrorExpected: true, + ErrField: "resourceClaims", + }, + // ----------- ResourceClaims.Name ----------- + { + Name: "resourceClaims.name empty slice not restricted", + Override: corev1.PodSpec{ResourceClaims: nil}, + RestrictedFields: []string{"resourceClaims.name"}, + IsErrorExpected: false, + }, + { + Name: "resourceClaims.name restricted by specific value match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu"}}}, + RestrictedFields: []string{"resourceClaims.name=gpu"}, + IsErrorExpected: true, + ErrField: "resourceClaims.name=gpu", + }, + { + Name: "resourceClaims.name restricted by any value", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu"}}}, + RestrictedFields: []string{"resourceClaims.name"}, + IsErrorExpected: true, + ErrField: "resourceClaims.name", + }, + { + Name: "resourceClaims.name allowed when restricted value does not match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu"}}}, + RestrictedFields: []string{"resourceClaims.name=tpu"}, + IsErrorExpected: false, + }, + // ----------- ResourceClaims.ResourceClaimName ----------- + { + Name: "resourceClaims.resourceClaimName nil value not restricted", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimName: nil}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimName"}, + IsErrorExpected: false, + }, + { + Name: "resourceClaims.resourceClaimName restricted by specific value match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimName: ptr.To("my-claim")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimName=my-claim"}, + IsErrorExpected: true, + ErrField: "resourceClaims.resourceClaimName=my-claim", + }, + { + Name: "resourceClaims.resourceClaimName restricted by any value", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimName: ptr.To("my-claim")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimName"}, + IsErrorExpected: true, + ErrField: "resourceClaims.resourceClaimName", + }, + { + Name: "resourceClaims.resourceClaimName allowed when restricted value does not match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimName: ptr.To("my-claim")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimName=other-claim"}, + IsErrorExpected: false, + }, + // ----------- ResourceClaims.ResourceClaimTemplateName ----------- + { + Name: "resourceClaims.resourceClaimTemplateName nil value not restricted", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimTemplateName: nil}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimTemplateName"}, + IsErrorExpected: false, + }, + { + Name: "resourceClaims.resourceClaimTemplateName restricted by specific value match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimTemplateName: ptr.To("my-template")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimTemplateName=my-template"}, + IsErrorExpected: true, + ErrField: "resourceClaims.resourceClaimTemplateName=my-template", + }, + { + Name: "resourceClaims.resourceClaimTemplateName restricted by any value", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimTemplateName: ptr.To("my-template")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimTemplateName"}, + IsErrorExpected: true, + ErrField: "resourceClaims.resourceClaimTemplateName", + }, + { + Name: "resourceClaims.resourceClaimTemplateName allowed when restricted value does not match", + Override: corev1.PodSpec{ResourceClaims: []corev1.PodResourceClaim{{Name: "gpu", ResourceClaimTemplateName: ptr.To("my-template")}}}, + RestrictedFields: []string{"resourceClaims.resourceClaimTemplateName=other-template"}, + IsErrorExpected: false, + }, + // ----------- Resources ----------- + { + Name: "resources nil not restricted", + Override: corev1.PodSpec{Resources: nil}, + RestrictedFields: []string{"resources"}, + IsErrorExpected: false, + }, + { + Name: "resources restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{}}, + RestrictedFields: []string{"resources"}, + IsErrorExpected: true, + ErrField: "resources", + }, + // ----------- Resources.Limits ----------- + { + Name: "resources.limits nil not restricted", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Limits: nil}}, + RestrictedFields: []string{"resources.limits"}, + IsErrorExpected: false, + }, + { + Name: "resources.limits restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Limits: corev1.ResourceList{}}}, + RestrictedFields: []string{"resources.limits"}, + IsErrorExpected: true, + ErrField: "resources.limits", + }, + // ----------- Resources.Requests ----------- + { + Name: "resources.requests nil not restricted", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Requests: nil}}, + RestrictedFields: []string{"resources.requests"}, + IsErrorExpected: false, + }, + { + Name: "resources.requests restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Requests: corev1.ResourceList{}}}, + RestrictedFields: []string{"resources.requests"}, + IsErrorExpected: true, + ErrField: "resources.requests", + }, + // ----------- Resources.Claims ----------- + { + Name: "resources.claims empty not restricted", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Claims: nil}}, + RestrictedFields: []string{"resources.claims"}, + IsErrorExpected: false, + }, + { + Name: "resources.claims restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Claims: []corev1.ResourceClaim{{Name: "gpu"}}}}, + RestrictedFields: []string{"resources.claims"}, + IsErrorExpected: true, + ErrField: "resources.claims", + }, + // ----------- Resources.Limits.CPU ----------- + { + Name: "resources.limits.cpu restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")}}}, + RestrictedFields: []string{"resources.limits.cpu"}, + IsErrorExpected: true, + ErrField: "resources.limits.cpu", + }, + // ----------- Resources.Limits.Memory ----------- + { + Name: "resources.limits.memory restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("512Mi")}}}, + RestrictedFields: []string{"resources.limits.memory"}, + IsErrorExpected: true, + ErrField: "resources.limits.memory", + }, + // ----------- Resources.Requests.CPU ----------- + { + Name: "resources.requests.cpu restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("50m")}}}, + RestrictedFields: []string{"resources.requests.cpu"}, + IsErrorExpected: true, + ErrField: "resources.requests.cpu", + }, + // ----------- Resources.Requests.Memory ----------- + { + Name: "resources.requests.memory restricted by any value", + Override: corev1.PodSpec{Resources: &corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("128Mi")}}}, + RestrictedFields: []string{"resources.requests.memory"}, + IsErrorExpected: true, + ErrField: "resources.requests.memory", + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + err := restrictPodOverride(&tt.Override, tt.RestrictedFields) + + if tt.IsErrorExpected { + assert.Error(t, err) + assert.Equal(t, getPodRestrictionErr(tt.ErrField).Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestApplyPodOverridesStripsUnknownFields(t *testing.T) { + overrideJSON := `{"spec":{"schedulerName":"custom","futureSecurityField":"malicious-value","unknownNested":{"key":"val"}}}` + + workspace := &common.DevWorkspaceWithConfig{} + workspace.DevWorkspace = &dw.DevWorkspace{} + workspace.Spec.Template = dw.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{ + constants.PodOverridesAttribute: apiext.JSON{Raw: []byte(overrideJSON)}, + }, + Components: []dw.Component{{ + Name: "test-component", + ComponentUnion: dw.ComponentUnion{ + Container: &dw.ContainerComponent{ + Container: dw.Container{Image: "test-image"}, + }, + }, + }}, + }, + } + + deployment := &appsv1.Deployment{} + deployment.Spec.Template.Spec.Containers = []corev1.Container{{ + Name: "test-component", + Image: "test-image", + }} + + patched, err := ApplyPodOverrides(workspace, deployment) + assert.NoError(t, err) + assert.Equal(t, "custom", patched.Spec.Template.Spec.SchedulerName) + + patchedBytes, err := json.Marshal(patched.Spec.Template.Spec) + assert.NoError(t, err) + assert.NotContains(t, string(patchedBytes), "futureSecurityField") + assert.NotContains(t, string(patchedBytes), "unknownNested") +} + +func preemptionPolicyPtr(p corev1.PreemptionPolicy) *corev1.PreemptionPolicy { + return &p +} + +func supplementalGroupsPolicyPtr(p corev1.SupplementalGroupsPolicy) *corev1.SupplementalGroupsPolicy { + return &p +} + +func fsGroupChangePolicyPtr(p corev1.PodFSGroupChangePolicy) *corev1.PodFSGroupChangePolicy { + return &p +} + +func seLinuxChangePolicyPtr(p corev1.PodSELinuxChangePolicy) *corev1.PodSELinuxChangePolicy { + return &p +} diff --git a/pkg/library/overrides/pods.go b/pkg/library/overrides/pods.go index c9b826a5c..a59406578 100644 --- a/pkg/library/overrides/pods.go +++ b/pkg/library/overrides/pods.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -28,8 +28,7 @@ import ( "github.com/devfile/devworkspace-operator/pkg/constants" ) -// NeedsPodOverrides returns whether the current DevWorkspace defines pod overrides via an attribute -// attribute. +// NeedsPodOverrides returns whether the current DevWorkspace defines pod overrides via an attribute. func NeedsPodOverrides(workspace *common.DevWorkspaceWithConfig) bool { if workspace.Spec.Template.Attributes.Exists(constants.PodOverridesAttribute) { return true @@ -43,7 +42,7 @@ func NeedsPodOverrides(workspace *common.DevWorkspaceWithConfig) bool { } func ApplyPodOverrides(workspace *common.DevWorkspaceWithConfig, deployment *appsv1.Deployment) (*appsv1.Deployment, error) { - overrides, err := getPodOverrides(&workspace.Spec.Template) + overrides, err := getPodOverrides(&workspace.Spec.Template, GetRestrictedPodOverrideFields(workspace)) if err != nil { return nil, err } @@ -76,10 +75,10 @@ func ApplyPodOverrides(workspace *common.DevWorkspaceWithConfig, deployment *app return patched, nil } -func GetVolumesFromOverrides(workspace *dw.DevWorkspaceTemplateSpec) (map[string]bool, error) { +func GetVolumesFromOverrides(workspace *dw.DevWorkspaceTemplateSpec, restrictedFields []string) (map[string]bool, error) { overrideVolumes := map[string]bool{} - overrides, err := getPodOverrides(workspace) + overrides, err := getPodOverrides(workspace, restrictedFields) if err != nil { return nil, err } @@ -103,7 +102,7 @@ func GetVolumesFromOverrides(workspace *dw.DevWorkspaceTemplateSpec) (map[string // present in the DevWorkspace. The order of elements is // 1. Pod overrides defined on Container components, in the order they appear in the DevWorkspace // 2. Pod overrides defined in the global attributes field (.spec.template.attributes) -func getPodOverrides(workspace *dw.DevWorkspaceTemplateSpec) ([]apiext.JSON, error) { +func getPodOverrides(workspace *dw.DevWorkspaceTemplateSpec, restrictedFields []string) ([]apiext.JSON, error) { var allOverrides []apiext.JSON for _, component := range workspace.Components { @@ -113,12 +112,8 @@ func getPodOverrides(workspace *dw.DevWorkspaceTemplateSpec) ([]apiext.JSON, err if err := component.Attributes.GetInto(constants.PodOverridesAttribute, &override); err != nil { return nil, fmt.Errorf("failed to parse %s attribute on component %s: %w", constants.PodOverridesAttribute, component.Name, err) } - // Do not allow overriding containers - if override.Spec.Containers != nil { - return nil, fmt.Errorf("cannot use pod-overrides to override pod containers (component %s)", component.Name) - } - if override.Spec.InitContainers != nil { - return nil, fmt.Errorf("cannot use pod-overrides to override pod initContainers (component %s)", component.Name) + if err := restrictPodOverride(&override.Spec, restrictedFields); err != nil { + return nil, fmt.Errorf("invalid %s attribute on component %s: %w", constants.PodOverridesAttribute, component.Name, err) } patchData := component.Attributes[constants.PodOverridesAttribute] allOverrides = append(allOverrides, patchData) @@ -130,12 +125,8 @@ func getPodOverrides(workspace *dw.DevWorkspaceTemplateSpec) ([]apiext.JSON, err if err != nil { return nil, fmt.Errorf("failed to parse %s attribute for workspace: %w", constants.PodOverridesAttribute, err) } - // Do not allow overriding containers - if override.Spec.Containers != nil { - return nil, fmt.Errorf("cannot use pod-overrides to override pod containers") - } - if override.Spec.InitContainers != nil { - return nil, fmt.Errorf("cannot use pod-overrides to override pod initContainers") + if err := restrictPodOverride(&override.Spec, restrictedFields); err != nil { + return nil, fmt.Errorf("invalid %s attribute for workspace: %w", constants.PodOverridesAttribute, err) } patchData := workspace.Attributes[constants.PodOverridesAttribute] allOverrides = append(allOverrides, patchData) diff --git a/pkg/library/overrides/restrictions.go b/pkg/library/overrides/restrictions.go new file mode 100644 index 000000000..505babca3 --- /dev/null +++ b/pkg/library/overrides/restrictions.go @@ -0,0 +1,186 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package overrides + +import ( + "strconv" + "strings" + + "github.com/devfile/devworkspace-operator/pkg/common" + corev1 "k8s.io/api/core/v1" +) + +type FieldRestriction struct { + name string + restrictedValue string + getRestrictionErr func(msg string) error +} + +func (r FieldRestriction) checkAny() error { + // check any value is restricted + if r.restrictedValue == "" { + return r.getRestrictionErr(r.name) + } + + return nil +} + +func (r FieldRestriction) checkString(fieldValue *string) error { + // value is empty, no restriction applied + if fieldValue == nil || *fieldValue == "" { + return nil + } + + // check any value is restricted + if r.restrictedValue == "" { + return r.getRestrictionErr(r.name) + } + + // check if specific value is restricted + return r.checkRestrictedValue(*fieldValue) +} + +func (r FieldRestriction) checkBool(fieldValue *bool) error { + if fieldValue == nil { + return nil + } + + // check any value is restricted + if r.restrictedValue == "" { + return r.getRestrictionErr(r.name) + } + + // check if specific value is restricted + return r.checkRestrictedValue(strconv.FormatBool(*fieldValue)) +} + +func (r FieldRestriction) checkInt32(fieldValue *int32) error { + if fieldValue == nil { + return nil + } + + // check any value is restricted + if r.restrictedValue == "" { + return r.getRestrictionErr(r.name) + } + + // check if specific value is restricted + return r.checkRestrictedValue(strconv.FormatInt(int64(*fieldValue), 10)) +} + +func (r FieldRestriction) checkInt64(fieldValue *int64) error { + if fieldValue == nil { + return nil + } + + // check any value is restricted + if r.restrictedValue == "" { + return r.getRestrictionErr(r.name) + } + + return r.checkRestrictedValue(strconv.FormatInt(*fieldValue, 10)) +} + +func (r FieldRestriction) checkRestrictedValue(value string) error { + if r.restrictedValue == value { + return r.getRestrictionErr(r.name + "=" + r.restrictedValue) + } + + return nil +} + +func GetRestrictedContainerOverrideFields(workspace *common.DevWorkspaceWithConfig) []string { + if workspace.Config != nil && workspace.Config.Workspace != nil && workspace.Config.Workspace.Overrides != nil { + return workspace.Config.Workspace.Overrides.RestrictedContainerOverrideFields + } + + return nil +} + +func GetRestrictedPodOverrideFields(workspace *common.DevWorkspaceWithConfig) []string { + if workspace.Config != nil && workspace.Config.Workspace != nil && workspace.Config.Workspace.Overrides != nil { + return workspace.Config.Workspace.Overrides.RestrictedPodOverrideFields + } + + return nil +} + +func checkResources(resources *corev1.ResourceRequirements, field string, restriction *FieldRestriction) error { + if resources == nil { + return nil + } + + root, remaining, _ := strings.Cut(field, ".") + + if remaining == "" { + switch root { + case "limits": + if resources.Limits != nil { + return restriction.checkAny() + } + case "requests": + if resources.Requests != nil { + return restriction.checkAny() + } + case "claims": + if len(resources.Claims) > 0 { + return restriction.checkAny() + } + } + return nil + } + + switch root { + case "limits": + return checkResourceList(resources.Limits, remaining, restriction) + case "requests": + return checkResourceList(resources.Requests, remaining, restriction) + case "claims": + return checkContainerResourceClaims(resources.Claims, remaining, restriction) + } + + return nil +} +func checkResourceList(resources corev1.ResourceList, field string, restriction *FieldRestriction) error { + if len(resources) == 0 { + return nil + } + + for resource, _ := range resources { + if field == string(resource) { + if err := restriction.checkAny(); err != nil { + return err + } + } + } + + return nil +} + +func checkContainerResourceClaims(claims []corev1.ResourceClaim, field string, restriction *FieldRestriction) error { + if len(claims) == 0 { + return nil + } + + for _, claim := range claims { + switch field { + case "request": + if err := restriction.checkString(&claim.Request); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/library/overrides/testdata/container-overrides/container-cannot-set-restricted-fields.yaml b/pkg/library/overrides/testdata/container-overrides/container-cannot-set-restricted-fields.yaml index a95725786..e310350d0 100644 --- a/pkg/library/overrides/testdata/container-overrides/container-cannot-set-restricted-fields.yaml +++ b/pkg/library/overrides/testdata/container-overrides/container-cannot-set-restricted-fields.yaml @@ -36,4 +36,4 @@ input: output: - errRegexp: "cannot use container-overrides to override container env" + errRegexp: "cannot use container-overrides to override container image" diff --git a/pkg/provision/storage/commonStorage.go b/pkg/provision/storage/commonStorage.go index 0c1a89f4d..98bcfc021 100644 --- a/pkg/provision/storage/commonStorage.go +++ b/pkg/provision/storage/commonStorage.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -81,7 +81,13 @@ func (p *CommonStorageProvisioner) ProvisionStorage(podAdditions *v1alpha1.PodAd pvcName = commonPVC.Name } - if err := p.rewriteContainerVolumeMounts(workspace.Status.DevWorkspaceId, pvcName, podAdditions, &workspace.Spec.Template); err != nil { + if err := p.rewriteContainerVolumeMounts( + workspace.Status.DevWorkspaceId, + pvcName, + podAdditions, + &workspace.Spec.Template, + overrides.GetRestrictedPodOverrideFields(workspace), + ); err != nil { return &dwerrors.FailError{ Err: err, Message: "Could not rewrite container volume mounts", @@ -126,7 +132,12 @@ func (p *CommonStorageProvisioner) CleanupWorkspaceStorage(workspace *common.Dev // (i.e. all volume mounts are subpaths into a common PVC used by all workspaces in the namespace). // // Also adds appropriate k8s Volumes to PodAdditions to accomodate the rewritten VolumeMounts. -func (p *CommonStorageProvisioner) rewriteContainerVolumeMounts(workspaceId, pvcName string, podAdditions *v1alpha1.PodAdditions, workspace *dw.DevWorkspaceTemplateSpec) error { +func (p *CommonStorageProvisioner) rewriteContainerVolumeMounts( + workspaceId, pvcName string, + podAdditions *v1alpha1.PodAdditions, + workspace *dw.DevWorkspaceTemplateSpec, + restrictedFields []string, +) error { devfileVolumes := map[string]dw.VolumeComponent{} // Construct map of volume name -> volume Component @@ -146,7 +157,7 @@ func (p *CommonStorageProvisioner) rewriteContainerVolumeMounts(workspaceId, pvc } // Containers in podAdditions may reference volumes defined in pod overrides, and this is not an error - overridesVolumes, err := overrides.GetVolumesFromOverrides(workspace) + overridesVolumes, err := overrides.GetVolumesFromOverrides(workspace, restrictedFields) if err != nil { return err } diff --git a/pkg/provision/storage/perWorkspaceStorage.go b/pkg/provision/storage/perWorkspaceStorage.go index c1a6ca523..d58aea8aa 100644 --- a/pkg/provision/storage/perWorkspaceStorage.go +++ b/pkg/provision/storage/perWorkspaceStorage.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -72,7 +72,13 @@ func (p *PerWorkspaceStorageProvisioner) ProvisionStorage(podAdditions *v1alpha1 } // Rewrite container volume mounts - if err := p.rewriteContainerVolumeMounts(workspace.Status.DevWorkspaceId, pvcName, podAdditions, &workspace.Spec.Template); err != nil { + if err := p.rewriteContainerVolumeMounts( + workspace.Status.DevWorkspaceId, + pvcName, + podAdditions, + &workspace.Spec.Template, + overrides.GetRestrictedPodOverrideFields(workspace), + ); err != nil { return &dwerrors.FailError{ Err: err, Message: "Could not rewrite container volume mounts", @@ -91,7 +97,12 @@ func (*PerWorkspaceStorageProvisioner) CleanupWorkspaceStorage(workspace *common // (i.e. all volume mounts are subpaths into a PVC used by a single workspace in the namespace). // // Also adds appropriate k8s Volumes to PodAdditions to accomodate the rewritten VolumeMounts. -func (p *PerWorkspaceStorageProvisioner) rewriteContainerVolumeMounts(workspaceId, pvcName string, podAdditions *v1alpha1.PodAdditions, workspace *dw.DevWorkspaceTemplateSpec) error { +func (p *PerWorkspaceStorageProvisioner) rewriteContainerVolumeMounts( + workspaceId, pvcName string, + podAdditions *v1alpha1.PodAdditions, + workspace *dw.DevWorkspaceTemplateSpec, + restrictedFields []string, +) error { devfileVolumes := map[string]dw.VolumeComponent{} // Construct map of volume name -> volume Component @@ -111,7 +122,7 @@ func (p *PerWorkspaceStorageProvisioner) rewriteContainerVolumeMounts(workspaceI } // Containers in podAdditions may reference volumes defined in pod overrides, and this is not an error - overridesVolumes, err := overrides.GetVolumesFromOverrides(workspace) + overridesVolumes, err := overrides.GetVolumesFromOverrides(workspace, restrictedFields) if err != nil { return err } From 16116d3f89389df1f3164070f50cffd0166d0673 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Sun, 21 Jun 2026 18:29:02 +0200 Subject: [PATCH 2/4] feat: add configurable field-level restrictions for container and pod overrides Signed-off-by: Anatolii Bazko --- docs/dwo-configuration.md | 12 ++++++++++++ pkg/config/sync.go | 4 ++-- pkg/library/overrides/pod_restrictions_test.go | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/dwo-configuration.md b/docs/dwo-configuration.md index 85436d389..c20588afc 100644 --- a/docs/dwo-configuration.md +++ b/docs/dwo-configuration.md @@ -311,6 +311,18 @@ config: Custom init containers are injected after the project-clone init container in the order they are defined in the configuration. The `init-persistent-home` container runs in this sequence along with other custom init containers. +## Always-restricted override fields + +Regardless of configuration, certain fields are **always** rejected in overrides and +cannot be unblocked by any configuration change. + +**Container overrides:** `name`, `image`, `command`, `args`, `ports`, `env` + +**Pod overrides:** `containers`, `initContainers` + +These implicit restrictions exist separately from the configurable restricted fields +described below. + ## Restricting override fields The DevWorkspace Operator allows cluster administrators to restrict which fields diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 7ae068007..8ff002135 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -795,10 +795,10 @@ func GetCurrentConfigString(currConfig *controller.OperatorConfiguration) string } if workspace.Overrides != nil { if workspace.Overrides.RestrictedContainerOverrideFields != nil { - config = append(config, fmt.Sprintf("workspace.overrides.deniedContainerOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedContainerOverrideFields, ", "))) + config = append(config, fmt.Sprintf("workspace.overrides.restrictedContainerOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedContainerOverrideFields, ", "))) } if workspace.Overrides.RestrictedPodOverrideFields != nil { - config = append(config, fmt.Sprintf("workspace.overrides.deniedPodOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedPodOverrideFields, ", "))) + config = append(config, fmt.Sprintf("workspace.overrides.restrictedPodOverrideFields=[%s]", strings.Join(workspace.Overrides.RestrictedPodOverrideFields, ", "))) } } if len(workspace.InitContainers) > 0 { diff --git a/pkg/library/overrides/pod_restrictions_test.go b/pkg/library/overrides/pod_restrictions_test.go index 8cb6fb0ec..a368f7cb2 100644 --- a/pkg/library/overrides/pod_restrictions_test.go +++ b/pkg/library/overrides/pod_restrictions_test.go @@ -806,13 +806,13 @@ func TestRestrictPodOverride(t *testing.T) { }, // ----------- WorkloadRef ----------- { - Name: "schedulingGates empty slice not restricted", + Name: "workloadRef empty slice not restricted", Override: corev1.PodSpec{WorkloadRef: nil}, RestrictedFields: []string{"workloadRef"}, IsErrorExpected: false, }, { - Name: "schedulingGates restricted by any value", + Name: "workloadRef restricted by any value", Override: corev1.PodSpec{WorkloadRef: &corev1.WorkloadReference{}}, RestrictedFields: []string{"workloadRef"}, IsErrorExpected: true, From 5304a6c1365feb9f631607542702afae382f7de0 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 22 Jun 2026 10:00:42 +0200 Subject: [PATCH 3/4] feat: add configurable field-level restrictions for container and pod overrides Signed-off-by: Anatolii Bazko --- pkg/library/overrides/container_restrictions.go | 3 +-- pkg/library/overrides/restrictions.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/library/overrides/container_restrictions.go b/pkg/library/overrides/container_restrictions.go index 2c338cf65..aedc647cc 100644 --- a/pkg/library/overrides/container_restrictions.go +++ b/pkg/library/overrides/container_restrictions.go @@ -15,7 +15,6 @@ package overrides import ( "fmt" - "reflect" "strings" corev1 "k8s.io/api/core/v1" @@ -115,7 +114,7 @@ func checkContainer(override *corev1.Container, root string, remaining string, r return restriction.checkAny() } case "resources": - if !reflect.DeepEqual(override.Resources, corev1.ResourceRequirements{}) { + if override.Resources.Limits != nil || override.Resources.Requests != nil || len(override.Resources.Claims) > 0 { return restriction.checkAny() } case "volumeMounts": diff --git a/pkg/library/overrides/restrictions.go b/pkg/library/overrides/restrictions.go index 505babca3..ba84ffb68 100644 --- a/pkg/library/overrides/restrictions.go +++ b/pkg/library/overrides/restrictions.go @@ -157,7 +157,7 @@ func checkResourceList(resources corev1.ResourceList, field string, restriction return nil } - for resource, _ := range resources { + for resource := range resources { if field == string(resource) { if err := restriction.checkAny(); err != nil { return err From 33d89bf37e94023c46d0ddd7d41d1450ae373f08 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 22 Jun 2026 10:56:39 +0200 Subject: [PATCH 4/4] feat: add configurable field-level restrictions for container and pod overrides Signed-off-by: Anatolii Bazko --- pkg/library/overrides/container_restrictions.go | 6 ++---- pkg/library/overrides/pod_restrictions.go | 14 ++++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/library/overrides/container_restrictions.go b/pkg/library/overrides/container_restrictions.go index aedc647cc..3fda37544 100644 --- a/pkg/library/overrides/container_restrictions.go +++ b/pkg/library/overrides/container_restrictions.go @@ -196,8 +196,7 @@ func checkConfigMapRef(cmRef *corev1.ConfigMapEnvSource, field string, restricti return nil } - switch field { - case "name": + if field == "name" { return restriction.checkString(&cmRef.Name) } @@ -209,8 +208,7 @@ func checkSecretRef(secretRef *corev1.SecretEnvSource, field string, restriction return nil } - switch field { - case "name": + if field == "name" { return restriction.checkString(&secretRef.Name) } diff --git a/pkg/library/overrides/pod_restrictions.go b/pkg/library/overrides/pod_restrictions.go index 7deb861c7..2d1f78c1c 100644 --- a/pkg/library/overrides/pod_restrictions.go +++ b/pkg/library/overrides/pod_restrictions.go @@ -274,9 +274,8 @@ func checkSysctls(sysctls []corev1.Sysctl, field string, restriction *FieldRestr return err } - for _, sysctl := range sysctls { - switch field { - case "name": + if field == "name" { + for _, sysctl := range sysctls { if err := restriction.checkString(&sysctl.Name); err != nil { return err } @@ -295,8 +294,8 @@ func checkSupplementalGroups(supplementalGroups []int64, field string, restricti return err } - for _, supplementalGroup := range supplementalGroups { - if field == "" { + if field == "" { + for _, supplementalGroup := range supplementalGroups { if err := restriction.checkInt64(&supplementalGroup); err != nil { return err } @@ -338,9 +337,8 @@ func checkImagePullSecrets(secrets []corev1.LocalObjectReference, field string, return nil } - for _, secret := range secrets { - switch field { - case "name": + if field == "name" { + for _, secret := range secrets { if err := restriction.checkString(&secret.Name); err != nil { return err }