Repository: knative/serving
Pull request #15878: Add support for Kubernetes “image” volume type
Author: login: Fedosin
Labels: name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
Created: 2025-05-09T17:36:53Z | Merged: 2025-05-23T00:10:30Z

Fixes #15488

<!-- Please include the 'why' behind your changes if no issue exists -->
## Proposed Changes

Kubernetes v1.31 introduced image volumes that allow mounting OCI images directly into Pods.
This patch extends Knative Serving to recognize and validate this new type.

Highlights

• Feature gate
    – New config-map key kubernetes.podspec-volumes-image and flag PodSpecVolumesImage (disabled by default).

• API / field-mask updates
    – VolumeSourceMask now preserves .image when the gate is enabled. 
    – Added ImageVolumeSourceMask helper for shallow-copying allowed fields.

• Validation
    – validateVolume checks feature-gate state, treats image as a mutually-exclusive source and performs per-field checks via validateImageVolumeSource.

• Schema-tweak overrides
    – Whitelisted volumes.image behind the new feature gate so generated CRD/OpenAPI stays in sync.

• Tests
    – Added helper to enable the feature in tests.
    – Expanded TestVolumeValidation with positive/negative cases for the image volume.

**Release Note**

<!-- Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". -->

```release-note
Knative Serving now supports Kubernetes’ new "image" volume type.
```


Linked issue #15488: Support volume type "image" for volume mounting
## In what area(s)?

/area API

## Describe the feature

Kubernetes 1.31 introduces a new volume type "image" which allows the direct mount of OCI container images into a Pod. According to https://knative.dev/docs/serving/services/storage/ volume mounts are supported in Knative, but the type "image" is missing. 

The "image" type should be added to the allow-list for possible types so that this new K8s feature can be used.

KServe would need this for introducing such image mounts for mounting model data stored in OCI images --> https://github.com/kserve/kserve/issues/3908
Linked issue comments: user: login: skonto
created_at: 2024-09-09T13:55:06Z
body: As a side note we are a bit behind on tested versions (1.28, 1.29) and also we are not on 1.31 from an api pov (1.30). As it is mentioned [Image type](https://github.com/kubernetes/api/blob/master/core/v1/types.go#L200) was introduced in 1.31 and it is Alpha in 1.31 https://kubernetes.io/docs/concepts/storage/volumes/#image so I guess it is a bit early but we will track it.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-12-09T01:29:09Z
body: This issue is stale because it has been open for 90 days with no
activity. It will automatically close after 30 more days of
inactivity. Reopen the issue with `/reopen`. Mark the issue as
fresh by adding the comment `/remove-lifecycle stale`.
author_association: NONE
user: login: rhuss
created_at: 2024-12-09T11:34:09Z
body: /remove-lifecycle stale
author_association: CONTRIBUTOR
user: login: Fedosin
created_at: 2025-05-09T15:07:04Z
body: I've started working on this issue.
author_association: CONTRIBUTOR
user: login: kahirokunn
created_at: 2025-05-20T08:40:40Z
body: I wrote a blog post about this feature, but I really want to use image volumes with Knative.
https://qiita.com/kahirokunn/items/7945dbc0e0f8a5f5e25c

Being able to separate runtime and code is the dawn of a wonderful new era.
author_association: MEMBER

Diff:
diff --git a/cmd/schema-tweak/overrides.go b/cmd/schema-tweak/overrides.go
index 2034a1864fe3..d08d86d9bc47 100644
--- a/cmd/schema-tweak/overrides.go
+++ b/cmd/schema-tweak/overrides.go
@@ -251,6 +251,9 @@ func revSpecOverrides(prefixPath string) []entry {
 		}, {
 			name: "csi",
 			flag: config.FeaturePodSpecVolumesCSI,
+		}, {
+			name: "image",
+			flag: config.FeaturePodSpecVolumesImage,
 		}},
 	}, {
 		path: "volumes.secret",
diff --git a/config/core/300-resources/configuration.yaml b/config/core/300-resources/configuration.yaml
index ea6bf819c369..1e42a23eb77a 100644
--- a/config/core/300-resources/configuration.yaml
+++ b/config/core/300-resources/configuration.yaml
@@ -1181,6 +1181,11 @@ spec:
                                   This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath
                                 type: object
                                 x-kubernetes-preserve-unknown-fields: true
+                              image:
+                                description: |-
+                                  This is accessible behind a feature flag - kubernetes.podspec-volumes-image
+                                type: object
+                                x-kubernetes-preserve-unknown-fields: true
                               name:
                                 description: |-
                                   name of the volume.
diff --git a/config/core/300-resources/revision.yaml b/config/core/300-resources/revision.yaml
index 4f5c79556bf9..d12364251632 100644
--- a/config/core/300-resources/revision.yaml
+++ b/config/core/300-resources/revision.yaml
@@ -1157,6 +1157,11 @@ spec:
                           This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath
                         type: object
                         x-kubernetes-preserve-unknown-fields: true
+                      image:
+                        description: |-
+                          This is accessible behind a feature flag - kubernetes.podspec-volumes-image
+                        type: object
+                        x-kubernetes-preserve-unknown-fields: true
                       name:
                         description: |-
                           name of the volume.
diff --git a/config/core/300-resources/service.yaml b/config/core/300-resources/service.yaml
index e6fc2682d566..42543b46919b 100644
--- a/config/core/300-resources/service.yaml
+++ b/config/core/300-resources/service.yaml
@@ -1199,6 +1199,11 @@ spec:
                                   This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath
                                 type: object
                                 x-kubernetes-preserve-unknown-fields: true
+                              image:
+                                description: |-
+                                  This is accessible behind a feature flag - kubernetes.podspec-volumes-image
+                                type: object
+                                x-kubernetes-preserve-unknown-fields: true
                               name:
                                 description: |-
                                   name of the volume.
diff --git a/config/core/configmaps/features.yaml b/config/core/configmaps/features.yaml
index 234e8ab61c30..c2673c7a47f4 100644
--- a/config/core/configmaps/features.yaml
+++ b/config/core/configmaps/features.yaml
@@ -22,7 +22,7 @@ metadata:
     app.kubernetes.io/component: controller
     app.kubernetes.io/version: devel
   annotations:
-    knative.dev/example-checksum: "82cbbba9"
+    knative.dev/example-checksum: "0f9b4ade"
 data:
   _example: |-
     ################################
@@ -200,6 +200,11 @@ data:
     # 2. Disabled: disabling EmptyDir volume support
     kubernetes.podspec-volumes-emptydir: "enabled"
 
+    # Controls whether volume support for image is enabled or not.
+    # 1. Enabled: enabling image volume support
+    # 2. Disabled: disabling image volume support
+    kubernetes.podspec-volumes-image: "disabled"
+
     # Controls whether volume support for HostPath is enabled or not.
     # WARNING: Cannot safely be disabled once enabled.
     # WARNING: If you can avoid using a hostPath volume, you should.
diff --git a/pkg/apis/config/features.go b/pkg/apis/config/features.go
index 95b0af282985..7a8d6c2d070a 100644
--- a/pkg/apis/config/features.go
+++ b/pkg/apis/config/features.go
@@ -77,6 +77,7 @@ const (
 	FeaturePodSpecShareProcessNamespace     = "kubernetes.podspec-shareprocessnamespace"
 	FeaturePodSpecTolerations               = "kubernetes.podspec-tolerations"
 	FeaturePodSpecTopologySpreadConstraints = "kubernetes.podspec-topologyspreadconstraints"
+	FeaturePodSpecVolumesImage              = "kubernetes.podspec-volumes-image"
 )
 
 func defaultFeaturesConfig() *Features {
@@ -103,6 +104,7 @@ func defaultFeaturesConfig() *Features {
 		PodSpecVolumesHostPath:           Disabled,
 		PodSpecVolumesMountPropagation:   Disabled,
 		PodSpecVolumesCSI:                Disabled,
+		PodSpecVolumesImage:              Disabled,
 		PodSpecPersistentVolumeClaim:     Disabled,
 		PodSpecPersistentVolumeWrite:     Disabled,
 		QueueProxyMountPodInfo:           Disabled,
@@ -142,6 +144,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) {
 		asFlag(FeaturePodSpecHostPID, &nc.PodSpecHostPID),
 		asFlag(FeaturePodSpecHostPath, &nc.PodSpecVolumesHostPath),
 		asFlag(FeaturePodSpecVolumesCSI, &nc.PodSpecVolumesCSI),
+		asFlag(FeaturePodSpecVolumesImage, &nc.PodSpecVolumesImage),
 		asFlag(FeaturePodSpecInitContainers, &nc.PodSpecInitContainers),
 		asFlag(FeaturePodSpecVolumesMountPropagation, &nc.PodSpecVolumesMountPropagation),
 		asFlag(FeaturePodSpecNodeSelector, &nc.PodSpecNodeSelector),
@@ -188,6 +191,7 @@ type Features struct {
 	PodSpecVolumesHostPath           Flag
 	PodSpecVolumesMountPropagation   Flag
 	PodSpecVolumesCSI                Flag
+	PodSpecVolumesImage              Flag
 	PodSpecInitContainers            Flag
 	PodSpecPersistentVolumeClaim     Flag
 	PodSpecPersistentVolumeWrite     Flag
diff --git a/pkg/apis/config/features_test.go b/pkg/apis/config/features_test.go
index 0fa22e5b980a..4fe2b0b6f5c3 100644
--- a/pkg/apis/config/features_test.go
+++ b/pkg/apis/config/features_test.go
@@ -437,6 +437,24 @@ func TestFeaturesConfiguration(t *testing.T) {
 		data: map[string]string{
 			"kubernetes.podspec-volumes-emptydir": "Enabled",
 		},
+	}, {
+		name:    "kubernetes.podspec-volumes-image Disabled",
+		wantErr: false,
+		wantFeatures: defaultWith(&Features{
+			PodSpecVolumesImage: Disabled,
+		}),
+		data: map[string]string{
+			"kubernetes.podspec-volumes-image": "Disabled",
+		},
+	}, {
+		name:    "kubernetes.podspec-volumes-image Enabled",
+		wantErr: false,
+		wantFeatures: defaultWith(&Features{
+			PodSpecVolumesImage: Enabled,
+		}),
+		data: map[string]string{
+			"kubernetes.podspec-volumes-image": "Enabled",
+		},
 	}, {
 		name:    "kubernetes.podspec-volumes-hostpath Disabled",
 		wantErr: false,
diff --git a/pkg/apis/serving/fieldmask.go b/pkg/apis/serving/fieldmask.go
index 4de8adac1795..ea7181024a6c 100644
--- a/pkg/apis/serving/fieldmask.go
+++ b/pkg/apis/serving/fieldmask.go
@@ -74,6 +74,10 @@ func VolumeSourceMask(ctx context.Context, in *corev1.VolumeSource) *corev1.Volu
 		out.CSI = in.CSI
 	}
 
+	if cfg.Features.PodSpecVolumesImage != config.Disabled {
+		out.Image = in.Image
+	}
+
 	// Too many disallowed fields to list
 
 	return out
@@ -780,3 +784,19 @@ func NamespacedObjectReferenceMask(in *corev1.ObjectReference) *corev1.ObjectRef
 
 	return out
 }
+
+// ImageVolumeSourceMask performs a _shallow_ copy of Kubernetes ImageVolumeSource
+// bringing over only the fields allowed in the Knative API.
+func ImageVolumeSourceMask(in *corev1.ImageVolumeSource) *corev1.ImageVolumeSource {
+	if in == nil {
+		return nil
+	}
+
+	out := new(corev1.ImageVolumeSource)
+
+	// Allowed fields
+	out.Reference = in.Reference
+	out.PullPolicy = in.PullPolicy
+
+	return out
+}
diff --git a/pkg/apis/serving/k8s_validation.go b/pkg/apis/serving/k8s_validation.go
index 86f89c1af49c..496008f2d9c4 100644
--- a/pkg/apis/serving/k8s_validation.go
+++ b/pkg/apis/serving/k8s_validation.go
@@ -121,6 +121,10 @@ func validatePersistentVolumeClaims(volume corev1.VolumeSource, features *config
 func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError {
 	features := config.FromContextOrDefaults(ctx).Features
 	errs := validatePersistentVolumeClaims(volume.VolumeSource, features)
+	if volume.Image != nil && features.PodSpecVolumesImage != config.Enabled {
+		errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("Image volume support is disabled, "+
+			"but found Image volume %s", volume.Name)})
+	}
 	if volume.EmptyDir != nil && features.PodSpecVolumesEmptyDir != config.Enabled {
 		errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("EmptyDir volume support is disabled, "+
 			"but found EmptyDir volume %s", volume.Name)})
@@ -177,6 +181,11 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError
 		specified = append(specified, "csi")
 	}
 
+	if vs.Image != nil {
+		specified = append(specified, "image")
+		errs = errs.Also(validateImageVolumeSource(vs.Image).ViaField("image"))
+	}
+
 	if len(specified) == 0 {
 		fieldPaths := []string{"secret", "configMap", "projected"}
 		cfg := config.FromContextOrDefaults(ctx)
@@ -192,6 +201,9 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError
 		if cfg.Features.PodSpecVolumesCSI == config.Enabled {
 			fieldPaths = append(fieldPaths, "csi")
 		}
+		if cfg.Features.PodSpecVolumesImage == config.Enabled {
+			fieldPaths = append(fieldPaths, "image")
+		}
 		errs = errs.Also(apis.ErrMissingOneOf(fieldPaths...))
 	} else if len(specified) > 1 {
 		errs = errs.Also(apis.ErrMultipleOneOf(specified...))
@@ -1056,3 +1068,20 @@ func WithinInitContainer(ctx context.Context) context.Context {
 func IsInitContainer(ctx context.Context) bool {
 	return ctx.Value(initContainer{}) != nil
 }
+
+func validateImageVolumeSource(iv *corev1.ImageVolumeSource) *apis.FieldError {
+	errs := apis.CheckDisallowedFields(*iv, *ImageVolumeSourceMask(iv))
+
+	if iv.Reference == "" {
+		errs = errs.Also(apis.ErrMissingField("reference"))
+	}
+
+	switch iv.PullPolicy {
+	case corev1.PullIfNotPresent, corev1.PullAlways, corev1.PullNever, "":
+		// ok
+	default:
+		errs = errs.Also(apis.ErrInvalidValue(iv.PullPolicy, "pullPolicy"))
+	}
+
+	return errs
+}
diff --git a/pkg/apis/serving/k8s_validation_test.go b/pkg/apis/serving/k8s_validation_test.go
index a0be4e55afb6..5a2414ccacb5 100644
--- a/pkg/apis/serving/k8s_validation_test.go
+++ b/pkg/apis/serving/k8s_validation_test.go
@@ -143,6 +143,13 @@ func withPodSpecVolumesCSIEnabled() configOption {
 	}
 }
 
+func withPodSpecVolumesImageEnabled() configOption {
+	return func(cfg *config.Config) *config.Config {
+		cfg.Features.PodSpecVolumesImage = config.Enabled
+		return cfg
+	}
+}
+
 func withPodSpecPersistentVolumeWriteEnabled() configOption {
 	return func(cfg *config.Config) *config.Config {
 		cfg.Features.PodSpecPersistentVolumeWrite = config.Enabled
@@ -3324,6 +3331,44 @@ func TestVolumeValidation(t *testing.T) {
 			},
 		},
 		want: apis.ErrGeneric("Within a single item, cannot set both", "projected[0].downwardAPI.items[0].fieldRef", "projected[0].downwardAPI.items[0].resourceFieldRef"),
+	}, {
+		name: "missing CSI volume when required",
+		v: corev1.Volume{
+			Name: "foo",
+		},
+		cfgOpts: []configOption{withPodSpecVolumesCSIEnabled()},
+		want:    apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "csi"),
+	}, {
+		name: "valid image volume with feature enabled",
+		v: corev1.Volume{
+			Name: "foo",
+			VolumeSource: corev1.VolumeSource{
+				Image: &corev1.ImageVolumeSource{
+					Reference: "gcr.io/example/image:latest",
+				},
+			},
+		},
+		cfgOpts: []configOption{withPodSpecVolumesImageEnabled()},
+	}, {
+		name: "image volume with feature disabled",
+		v: corev1.Volume{
+			Name: "foo",
+			VolumeSource: corev1.VolumeSource{
+				Image: &corev1.ImageVolumeSource{
+					Reference: "gcr.io/example/image:latest",
+				},
+			},
+		},
+		want: (&apis.FieldError{
+			Message: `Image volume support is disabled, but found Image volume foo`,
+		}).Also(&apis.FieldError{Message: "must not set the field(s)", Paths: []string{"image"}}),
+	}, {
+		name: "missing image volume when required",
+		v: corev1.Volume{
+			Name: "foo",
+		},
+		cfgOpts: []configOption{withPodSpecVolumesImageEnabled()},
+		want:    apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "image"),
 	}}
 
 	for _, test := range tests {


Reviews:
user: login: dsimansk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:34:17Z
user: login: dsimansk
body: Missing entry and and short explanation in default ConfigMap: https://github.com/knative/serving/blob/main/config/core/configmaps/features.yaml

Otherwise looks good, thanks a lot @Fedosin!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:37:15Z
user: login: skonto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:15:52Z
user: login: Fedosin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:18:26Z
user: login: Fedosin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:21:06Z

Review comments:
diff_hunk: @@ -77,6 +77,7 @@ const (
 	FeaturePodSpecShareProcessNamespace     = "kubernetes.podspec-shareprocessnamespace"
 	FeaturePodSpecTolerations               = "kubernetes.podspec-tolerations"
 	FeaturePodSpecTopologySpreadConstraints = "kubernetes.podspec-topologyspreadconstraints"
+	FeaturePodSpecVolumesImage              = "kubernetes.podspec-volumes-image"
path: pkg/apis/config/features.go
user: login: dsimansk
body: @Fedosin a little nit, there's should be at least a enable/disable feature flag test added into `features_test.go`.
created_at: 2025-05-14T10:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1056,3 +1068,20 @@ func WithinInitContainer(ctx context.Context) context.Context {
 func IsInitContainer(ctx context.Context) bool {
 	return ctx.Value(initContainer{}) != nil
 }
+
+func validateImageVolumeSource(iv *corev1.ImageVolumeSource) *apis.FieldError {
+	errs := apis.CheckDisallowedFields(*iv, *ImageVolumeSourceMask(iv))
+
+	if iv.Reference == "" {
+		errs = errs.Also(apis.ErrMissingField("reference"))
+	}
+
+	switch iv.PullPolicy {
+	case corev1.PullIfNotPresent, corev1.PullAlways, corev1.PullNever, "":
+		// ok
path: pkg/apis/serving/k8s_validation.go
user: login: skonto
body: I think we should be consistent with what K8s has wrt policy [validation](https://github.com/kubernetes/kubernetes/blob/2a3ca42c917a698e9fd3e07c55f369a9accbe2c2/pkg/apis/core/validation/validation.go#L3406) e.g. `""` case, :thinking: 
created_at: 2025-05-14T13:15:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1056,3 +1068,20 @@ func WithinInitContainer(ctx context.Context) context.Context {
 func IsInitContainer(ctx context.Context) bool {
 	return ctx.Value(initContainer{}) != nil
 }
+
+func validateImageVolumeSource(iv *corev1.ImageVolumeSource) *apis.FieldError {
+	errs := apis.CheckDisallowedFields(*iv, *ImageVolumeSourceMask(iv))
+
+	if iv.Reference == "" {
+		errs = errs.Also(apis.ErrMissingField("reference"))
+	}
+
+	switch iv.PullPolicy {
+	case corev1.PullIfNotPresent, corev1.PullAlways, corev1.PullNever, "":
+		// ok
path: pkg/apis/serving/k8s_validation.go
user: login: Fedosin
body: Hey! I think the function you provided is for internal Kubernetes validation. From the user's perspective, pullPolicy is an optional parameter, and an empty string ("") is a valid value.

Here's how I tested it:
```bash
➭ cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: image-volume
spec:
  containers:
  - name: shell
    image: debian
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: volume
      mountPath: /volume
  volumes:
  - name: volume
    image:
      reference: quay.io/crio/artifact:v2
      pullPolicy: ""
EOF
pod/image-volume created
➭ kubectl get pod image-volume -o jsonpath='{.spec.volumes[?(@.name=="volume")].image.pullPolicy}'
IfNotPresent
```
I took the code from [here](https://kubernetes.io/docs/concepts/storage/volumes/#image) and changed `pullPolicy` to "".

From the field [description](https://pkg.go.dev/k8s.io/api/core/v1#ImageVolumeSource) it also appears that the
value can be "", because it "defaults to Always if :latest tag is specified, or IfNotPresent otherwise."
created_at: 2025-05-14T15:18:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +77,7 @@ const (
 	FeaturePodSpecShareProcessNamespace     = "kubernetes.podspec-shareprocessnamespace"
 	FeaturePodSpecTolerations               = "kubernetes.podspec-tolerations"
 	FeaturePodSpecTopologySpreadConstraints = "kubernetes.podspec-topologyspreadconstraints"
+	FeaturePodSpecVolumesImage              = "kubernetes.podspec-volumes-image"
path: pkg/apis/config/features.go
user: login: Fedosin
body: Thanks! I added the test there.
created_at: 2025-05-14T15:21:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-09T17:42:16Z
body: ## [Codecov](https://app.codecov.io/gh/knative/serving/pull/15878?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) Report
Attention: Patch coverage is `78.94737%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 80.93%. Comparing base [(`589b6ab`)](https://app.codecov.io/gh/knative/serving/commit/589b6abe4b3a02b8b2c1bd66b0c310371de017e4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) to head [(`885c72d`)](https://app.codecov.io/gh/knative/serving/commit/885c72d28f35fb5c8c6db32fb2a286675cbce69c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).
> Report is 9 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/knative/serving/pull/15878?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) | Patch % | Lines |
|---|---|---|
| [pkg/apis/serving/k8s\_validation.go](https://app.codecov.io/gh/knative/serving/pull/15878?src=pr&el=tree&filepath=pkg%2Fapis%2Fserving%2Fk8s_validation.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-cGtnL2FwaXMvc2VydmluZy9rOHNfdmFsaWRhdGlvbi5nbw==) | 77.27% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/knative/serving/pull/15878?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) |
| [pkg/apis/serving/fieldmask.go](https://app.codecov.io/gh/knative/serving/pull/15878?src=pr&el=tree&filepath=pkg%2Fapis%2Fserving%2Ffieldmask.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-cGtnL2FwaXMvc2VydmluZy9maWVsZG1hc2suZ28=) | 78.57% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/knative/serving/pull/15878?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #15878      +/-   ##
==========================================
+ Coverage   80.91%   80.93%   +0.01%     
==========================================
  Files         210      210              
  Lines       16731    16769      +38     
==========================================
+ Hits        13538    13572      +34     
- Misses       2842     2843       +1     
- Partials      351      354       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/knative/serving/pull/15878?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: dsimansk
created_at: 2025-05-14T17:31:55Z
body: > Missing entry and and short explanation in default ConfigMap: https://github.com/knative/serving/blob/main/config/core/configmaps/features.yaml

PTAL, the above config map should be updated. 

author_association: CONTRIBUTOR
user: login: dsimansk
created_at: 2025-05-15T08:21:40Z
body: /lgtm 
Looks good to me, thanks!

Deferring to Dave for approval, PTAL.
/assign @dprotaso 

author_association: CONTRIBUTOR
user: login: dprotaso
created_at: 2025-05-16T16:03:41Z
body: So we usually have feature flags to gate end users from using PodSpec features that may affect scalability etc. Hence why we've guarded persistent volumes before since at a certain scale it's not clear how many readers/writers say an EFS volume can support.

I think for image volumes it doesn't suffer from this problem since it's just pulling down an OCI image. Thus I'm inclined to just support this feature without needing a feature flag.
author_association: MEMBER
user: login: Fedosin
created_at: 2025-05-16T17:09:19Z
body: @dprotaso Hey! As I understand Image volume types are [disabled](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-gates-for-alpha-or-beta-features) by default in kubernetes and might be removed from API (very unlikely, but still). So, maybe we should keep the flag for now until the feature is GA in kubernetes, or at least it is enabled by default?
author_association: CONTRIBUTOR
user: login: skonto
created_at: 2025-05-16T18:43:15Z
body: Regarding scalability there might be some concerns not sure to what extent this is battle tested:
a) Large static content may cause delays, for example LLM images.
b) Ephemeral disk pressure: https://github.com/kubernetes/enhancements/issues/4639#issuecomment-2719440808
c) Registry pressure

WDYT?
author_association: CONTRIBUTOR
user: login: Fedosin
created_at: 2025-05-20T20:08:13Z
body: @skonto Yeah, bugs and other issues are the reason why the "image" volume type is not enabled by default in Kubernetes. But I believe they will fix the issues and make it stable in the near future.
As for now, I would advise against adding this volume type to the stable serving API and recommend hiding it behind a feature flag until the Kubernetes feature is GA.
author_association: CONTRIBUTOR
user: login: dprotaso
created_at: 2025-05-22T22:52:02Z
body: > Regarding scalability there might be some concerns not sure to what extent this is battle tested: a) Large static content may cause delays, for example LLM images. b) Ephemeral disk pressure: [kubernetes/enhancements#4639 (comment)](https://github.com/kubernetes/enhancements/issues/4639#issuecomment-2719440808) c) Registry pressure
> 
> WDYT?

I think this applies to containers images which users can already abuse - so I'm not concerned about volumes.
author_association: MEMBER
user: login: dprotaso
created_at: 2025-05-22T22:53:51Z
body: If it's alpha I'm ok putting this behind a flag. @Fedosin can we create a follow up issue to drop the flag when the feature GAs and we catch up to that release of K8s?
author_association: MEMBER
user: login: dprotaso
created_at: 2025-05-22T22:55:46Z
body: /lgtm
/approve
author_association: MEMBER
user: login: knative-prow[bot]
created_at: 2025-05-22T22:55:55Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/serving/pull/15878#issuecomment-2902802726" title="Approved">dprotaso</a>*, *<a href="https://github.com/knative/serving/pull/15878#" title="Author self-approved">Fedosin</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fserving).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative/serving/blob/main/OWNERS)~~ [dprotaso]
- ~~[pkg/apis/OWNERS](https://github.com/knative/serving/blob/main/pkg/apis/OWNERS)~~ [dprotaso]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE