Skip to content

Commit 7f2b2c7

Browse files
jbuntingclaude
andcommitted
feat: add support for generic ephemeral volumes
Add a new feature flag `kubernetes.podspec-volumes-ephemeral` to allow generic ephemeral volumes (volumeClaimTemplate) in Knative services. This enables workloads that need per-pod dynamically provisioned storage, such as local NVMe scratch space on cloud VMs. Also exempt ephemeral volumes from the forced readOnly default, matching the existing behavior for emptyDir and PersistentVolumeClaim volumes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb213e0 commit 7f2b2c7

9 files changed

Lines changed: 89 additions & 3 deletions

File tree

config/core/300-resources/configuration.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,11 @@ spec:
11801180
This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir
11811181
type: object
11821182
x-kubernetes-preserve-unknown-fields: true
1183+
ephemeral:
1184+
description: |-
1185+
This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral
1186+
type: object
1187+
x-kubernetes-preserve-unknown-fields: true
11831188
hostPath:
11841189
description: |-
11851190
This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath

config/core/300-resources/revision.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,11 @@ spec:
11561156
This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir
11571157
type: object
11581158
x-kubernetes-preserve-unknown-fields: true
1159+
ephemeral:
1160+
description: |-
1161+
This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral
1162+
type: object
1163+
x-kubernetes-preserve-unknown-fields: true
11591164
hostPath:
11601165
description: |-
11611166
This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath

config/core/300-resources/service.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,11 @@ spec:
11981198
This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir
11991199
type: object
12001200
x-kubernetes-preserve-unknown-fields: true
1201+
ephemeral:
1202+
description: |-
1203+
This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral
1204+
type: object
1205+
x-kubernetes-preserve-unknown-fields: true
12011206
hostPath:
12021207
description: |-
12031208
This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath

config/core/configmaps/features.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ metadata:
2222
app.kubernetes.io/component: controller
2323
app.kubernetes.io/version: devel
2424
annotations:
25-
knative.dev/example-checksum: "bee75b26"
25+
knative.dev/example-checksum: "424df6ce"
2626
data:
2727
_example: |-
2828
################################
@@ -207,6 +207,12 @@ data:
207207
# 2. Disabled: disabling HostPath volume support
208208
kubernetes.podspec-volumes-hostpath: "disabled"
209209
210+
# Controls whether volume support for generic ephemeral volumes is enabled or not.
211+
# See https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/#generic-ephemeral-volumes
212+
# 1. Enabled: enabling generic ephemeral volume support
213+
# 2. Disabled: disabling generic ephemeral volume support
214+
kubernetes.podspec-volumes-ephemeral: "disabled"
215+
210216
# Controls whether volume support for CSI is enabled or not.
211217
# 1. Enabled: enabling CSI volume support
212218
# 2. Disabled: disabling CSI volume support

pkg/apis/config/features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const (
7676
FeaturePodSpecShareProcessNamespace = "kubernetes.podspec-shareprocessnamespace"
7777
FeaturePodSpecTolerations = "kubernetes.podspec-tolerations"
7878
FeaturePodSpecTopologySpreadConstraints = "kubernetes.podspec-topologyspreadconstraints"
79+
FeaturePodSpecVolumesEphemeral = "kubernetes.podspec-volumes-ephemeral"
7980
FeaturePodSpecVolumesImage = "kubernetes.podspec-volumes-image"
8081
)
8182

@@ -102,6 +103,7 @@ func defaultFeaturesConfig() *Features {
102103
PodSpecVolumesHostPath: Disabled,
103104
PodSpecVolumesMountPropagation: Disabled,
104105
PodSpecVolumesCSI: Disabled,
106+
PodSpecVolumesEphemeral: Disabled,
105107
PodSpecVolumesImage: Disabled,
106108
PodSpecPersistentVolumeClaim: Disabled,
107109
PodSpecPersistentVolumeWrite: Disabled,
@@ -141,6 +143,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) {
141143
asFlag(FeaturePodSpecHostPID, &nc.PodSpecHostPID),
142144
asFlag(FeaturePodSpecHostPath, &nc.PodSpecVolumesHostPath),
143145
asFlag(FeaturePodSpecVolumesCSI, &nc.PodSpecVolumesCSI),
146+
asFlag(FeaturePodSpecVolumesEphemeral, &nc.PodSpecVolumesEphemeral),
144147
asFlag(FeaturePodSpecVolumesImage, &nc.PodSpecVolumesImage),
145148
asFlag(FeaturePodSpecInitContainers, &nc.PodSpecInitContainers),
146149
asFlag(FeaturePodSpecVolumesMountPropagation, &nc.PodSpecVolumesMountPropagation),
@@ -187,6 +190,7 @@ type Features struct {
187190
PodSpecVolumesHostPath Flag
188191
PodSpecVolumesMountPropagation Flag
189192
PodSpecVolumesCSI Flag
193+
PodSpecVolumesEphemeral Flag
190194
PodSpecVolumesImage Flag
191195
PodSpecInitContainers Flag
192196
PodSpecPersistentVolumeClaim Flag

pkg/apis/serving/fieldmask.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ func VolumeSourceMask(ctx context.Context, in *corev1.VolumeSource) *corev1.Volu
7676
out.CSI = in.CSI
7777
}
7878

79+
if cfg.Features.PodSpecVolumesEphemeral != config.Disabled {
80+
out.Ephemeral = in.Ephemeral
81+
}
82+
7983
if cfg.Features.PodSpecVolumesImage != config.Disabled {
8084
out.Image = in.Image
8185
}

pkg/apis/serving/k8s_validation.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError
138138
errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("CSI volume support is disabled, "+
139139
"but found CSI volume %s", volume.Name)})
140140
}
141+
if volume.Ephemeral != nil && features.PodSpecVolumesEphemeral != config.Enabled {
142+
errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("Ephemeral volume support is disabled, "+
143+
"but found Ephemeral volume %s", volume.Name)})
144+
}
141145
errs = errs.Also(apis.CheckDisallowedFields(volume, *VolumeMask(ctx, &volume)))
142146
if volume.Name == "" {
143147
errs = apis.ErrMissingField("name")
@@ -182,6 +186,10 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError
182186
specified = append(specified, "csi")
183187
}
184188

189+
if vs.Ephemeral != nil {
190+
specified = append(specified, "ephemeral")
191+
}
192+
185193
if vs.Image != nil {
186194
specified = append(specified, "image")
187195
errs = errs.Also(validateImageVolumeSource(vs.Image).ViaField("image"))
@@ -202,6 +210,9 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError
202210
if cfg.Features.PodSpecVolumesCSI == config.Enabled {
203211
fieldPaths = append(fieldPaths, "csi")
204212
}
213+
if cfg.Features.PodSpecVolumesEphemeral == config.Enabled {
214+
fieldPaths = append(fieldPaths, "ephemeral")
215+
}
205216
if cfg.Features.PodSpecVolumesImage == config.Enabled {
206217
fieldPaths = append(fieldPaths, "image")
207218
}
@@ -717,7 +728,7 @@ func validateVolumeMounts(ctx context.Context, mounts []corev1.VolumeMount, volu
717728
}
718729
seenMountPath.Insert(path.Clean(vm.MountPath))
719730

720-
shouldCheckReadOnlyVolume := volumes[vm.Name].EmptyDir == nil && volumes[vm.Name].PersistentVolumeClaim == nil
731+
shouldCheckReadOnlyVolume := volumes[vm.Name].EmptyDir == nil && volumes[vm.Name].PersistentVolumeClaim == nil && volumes[vm.Name].Ephemeral == nil
721732
if shouldCheckReadOnlyVolume && !vm.ReadOnly {
722733
errs = errs.Also((&apis.FieldError{
723734
Message: "volume mount should be readOnly for this type of volume",

pkg/apis/serving/k8s_validation_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ func withPodSpecVolumesCSIEnabled() configOption {
157157
}
158158
}
159159

160+
func withPodSpecVolumesEphemeralEnabled() configOption {
161+
return func(cfg *config.Config) *config.Config {
162+
cfg.Features.PodSpecVolumesEphemeral = config.Enabled
163+
return cfg
164+
}
165+
}
166+
160167
func withPodSpecVolumesImageEnabled() configOption {
161168
return func(cfg *config.Config) *config.Config {
162169
cfg.Features.PodSpecVolumesImage = config.Enabled
@@ -3492,6 +3499,45 @@ func TestVolumeValidation(t *testing.T) {
34923499
},
34933500
cfgOpts: []configOption{withPodSpecVolumesImageEnabled()},
34943501
want: apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "image"),
3502+
}, {
3503+
name: "valid ephemeral volume with feature enabled",
3504+
v: corev1.Volume{
3505+
Name: "foo",
3506+
VolumeSource: corev1.VolumeSource{
3507+
Ephemeral: &corev1.EphemeralVolumeSource{
3508+
VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{
3509+
Spec: corev1.PersistentVolumeClaimSpec{
3510+
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
3511+
},
3512+
},
3513+
},
3514+
},
3515+
},
3516+
cfgOpts: []configOption{withPodSpecVolumesEphemeralEnabled()},
3517+
}, {
3518+
name: "ephemeral volume with feature disabled",
3519+
v: corev1.Volume{
3520+
Name: "foo",
3521+
VolumeSource: corev1.VolumeSource{
3522+
Ephemeral: &corev1.EphemeralVolumeSource{
3523+
VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{
3524+
Spec: corev1.PersistentVolumeClaimSpec{
3525+
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
3526+
},
3527+
},
3528+
},
3529+
},
3530+
},
3531+
want: (&apis.FieldError{
3532+
Message: `Ephemeral volume support is disabled, but found Ephemeral volume foo`,
3533+
}).Also(&apis.FieldError{Message: "must not set the field(s)", Paths: []string{"ephemeral"}}),
3534+
}, {
3535+
name: "missing ephemeral volume when required",
3536+
v: corev1.Volume{
3537+
Name: "foo",
3538+
},
3539+
cfgOpts: []configOption{withPodSpecVolumesEphemeralEnabled()},
3540+
want: apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "ephemeral"),
34953541
}}
34963542

34973543
for _, test := range tests {

pkg/apis/serving/v1/revision_defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (rs *RevisionSpec) applyDefault(ctx context.Context, container *corev1.Cont
145145

146146
vNames := make(sets.Set[string])
147147
for _, v := range rs.PodSpec.Volumes {
148-
if v.EmptyDir != nil || v.PersistentVolumeClaim != nil {
148+
if v.EmptyDir != nil || v.PersistentVolumeClaim != nil || v.Ephemeral != nil {
149149
vNames.Insert(v.Name)
150150
}
151151
}

0 commit comments

Comments
 (0)