diff --git a/cmd/schema-tweak/overrides.go b/cmd/schema-tweak/overrides.go index d08d86d9bc47..fc66e3a1cf61 100644 --- a/cmd/schema-tweak/overrides.go +++ b/cmd/schema-tweak/overrides.go @@ -254,6 +254,9 @@ func revSpecOverrides(prefixPath string) []entry { }, { name: "image", flag: config.FeaturePodSpecVolumesImage, + }, { + name: "ephemeral", + flag: config.FeaturePodSpecVolumesEphemeral, }}, }, { path: "volumes.secret", diff --git a/config/core/300-resources/configuration.yaml b/config/core/300-resources/configuration.yaml index 7184a28a704d..63627a6a3db6 100644 --- a/config/core/300-resources/configuration.yaml +++ b/config/core/300-resources/configuration.yaml @@ -1180,6 +1180,11 @@ spec: This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir type: object x-kubernetes-preserve-unknown-fields: true + ephemeral: + description: |- + This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral + type: object + x-kubernetes-preserve-unknown-fields: true hostPath: description: |- This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath diff --git a/config/core/300-resources/revision.yaml b/config/core/300-resources/revision.yaml index 5da180f1bbe0..00f4b1d83c88 100644 --- a/config/core/300-resources/revision.yaml +++ b/config/core/300-resources/revision.yaml @@ -1156,6 +1156,11 @@ spec: This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir type: object x-kubernetes-preserve-unknown-fields: true + ephemeral: + description: |- + This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral + type: object + x-kubernetes-preserve-unknown-fields: true hostPath: description: |- This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath diff --git a/config/core/300-resources/service.yaml b/config/core/300-resources/service.yaml index e2b14b4fbbed..a03abc596aad 100644 --- a/config/core/300-resources/service.yaml +++ b/config/core/300-resources/service.yaml @@ -1198,6 +1198,11 @@ spec: This is accessible behind a feature flag - kubernetes.podspec-volumes-emptydir type: object x-kubernetes-preserve-unknown-fields: true + ephemeral: + description: |- + This is accessible behind a feature flag - kubernetes.podspec-volumes-ephemeral + type: object + x-kubernetes-preserve-unknown-fields: true hostPath: description: |- This is accessible behind a feature flag - kubernetes.podspec-volumes-hostpath diff --git a/config/core/configmaps/features.yaml b/config/core/configmaps/features.yaml index 96fbc7844db4..638a934e3dc4 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: "bee75b26" + knative.dev/example-checksum: "424df6ce" data: _example: |- ################################ @@ -207,6 +207,12 @@ data: # 2. Disabled: disabling HostPath volume support kubernetes.podspec-volumes-hostpath: "disabled" + # Controls whether volume support for generic ephemeral volumes is enabled or not. + # See https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/#generic-ephemeral-volumes + # 1. Enabled: enabling generic ephemeral volume support + # 2. Disabled: disabling generic ephemeral volume support + kubernetes.podspec-volumes-ephemeral: "disabled" + # Controls whether volume support for CSI is enabled or not. # 1. Enabled: enabling CSI volume support # 2. Disabled: disabling CSI volume support diff --git a/pkg/apis/config/features.go b/pkg/apis/config/features.go index ed5eb6ea844d..74ce0e7723c4 100644 --- a/pkg/apis/config/features.go +++ b/pkg/apis/config/features.go @@ -76,6 +76,7 @@ const ( FeaturePodSpecShareProcessNamespace = "kubernetes.podspec-shareprocessnamespace" FeaturePodSpecTolerations = "kubernetes.podspec-tolerations" FeaturePodSpecTopologySpreadConstraints = "kubernetes.podspec-topologyspreadconstraints" + FeaturePodSpecVolumesEphemeral = "kubernetes.podspec-volumes-ephemeral" FeaturePodSpecVolumesImage = "kubernetes.podspec-volumes-image" ) @@ -102,6 +103,7 @@ func defaultFeaturesConfig() *Features { PodSpecVolumesHostPath: Disabled, PodSpecVolumesMountPropagation: Disabled, PodSpecVolumesCSI: Disabled, + PodSpecVolumesEphemeral: Disabled, PodSpecVolumesImage: Disabled, PodSpecPersistentVolumeClaim: Disabled, PodSpecPersistentVolumeWrite: Disabled, @@ -141,6 +143,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) { asFlag(FeaturePodSpecHostPID, &nc.PodSpecHostPID), asFlag(FeaturePodSpecHostPath, &nc.PodSpecVolumesHostPath), asFlag(FeaturePodSpecVolumesCSI, &nc.PodSpecVolumesCSI), + asFlag(FeaturePodSpecVolumesEphemeral, &nc.PodSpecVolumesEphemeral), asFlag(FeaturePodSpecVolumesImage, &nc.PodSpecVolumesImage), asFlag(FeaturePodSpecInitContainers, &nc.PodSpecInitContainers), asFlag(FeaturePodSpecVolumesMountPropagation, &nc.PodSpecVolumesMountPropagation), @@ -187,6 +190,7 @@ type Features struct { PodSpecVolumesHostPath Flag PodSpecVolumesMountPropagation Flag PodSpecVolumesCSI Flag + PodSpecVolumesEphemeral Flag PodSpecVolumesImage Flag PodSpecInitContainers Flag PodSpecPersistentVolumeClaim Flag diff --git a/pkg/apis/config/features_test.go b/pkg/apis/config/features_test.go index 49135294b19a..f1aef4566a8d 100644 --- a/pkg/apis/config/features_test.go +++ b/pkg/apis/config/features_test.go @@ -480,6 +480,24 @@ func TestFeaturesConfiguration(t *testing.T) { data: map[string]string{ "kubernetes.podspec-volumes-csi": "Enabled", }, + }, { + name: "kubernetes.podspec-volumes-ephemeral Disabled", + wantErr: false, + wantFeatures: defaultWith(&Features{ + PodSpecVolumesEphemeral: Disabled, + }), + data: map[string]string{ + "kubernetes.podspec-volumes-ephemeral": "Disabled", + }, + }, { + name: "kubernetes.podspec-volumes-ephemeral Enabled", + wantErr: false, + wantFeatures: defaultWith(&Features{ + PodSpecVolumesEphemeral: Enabled, + }), + data: map[string]string{ + "kubernetes.podspec-volumes-ephemeral": "Enabled", + }, }, { name: "kubernetes.podspec-persistent-volume-claim Disabled", wantErr: false, diff --git a/pkg/apis/serving/fieldmask.go b/pkg/apis/serving/fieldmask.go index 73c48b1b5f64..43affd8eaeb9 100644 --- a/pkg/apis/serving/fieldmask.go +++ b/pkg/apis/serving/fieldmask.go @@ -76,6 +76,10 @@ func VolumeSourceMask(ctx context.Context, in *corev1.VolumeSource) *corev1.Volu out.CSI = in.CSI } + if cfg.Features.PodSpecVolumesEphemeral != config.Disabled { + out.Ephemeral = in.Ephemeral + } + if cfg.Features.PodSpecVolumesImage != config.Disabled { out.Image = in.Image } diff --git a/pkg/apis/serving/k8s_validation.go b/pkg/apis/serving/k8s_validation.go index fd90e0f1583f..d02ff0211e83 100644 --- a/pkg/apis/serving/k8s_validation.go +++ b/pkg/apis/serving/k8s_validation.go @@ -138,6 +138,10 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("CSI volume support is disabled, "+ "but found CSI volume %s", volume.Name)}) } + if volume.Ephemeral != nil && features.PodSpecVolumesEphemeral != config.Enabled { + errs = errs.Also(&apis.FieldError{Message: fmt.Sprintf("Ephemeral volume support is disabled, "+ + "but found Ephemeral volume %s", volume.Name)}) + } errs = errs.Also(apis.CheckDisallowedFields(volume, *VolumeMask(ctx, &volume))) if volume.Name == "" { errs = apis.ErrMissingField("name") @@ -182,6 +186,10 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError specified = append(specified, "csi") } + if vs.Ephemeral != nil { + specified = append(specified, "ephemeral") + } + if vs.Image != nil { specified = append(specified, "image") errs = errs.Also(validateImageVolumeSource(vs.Image).ViaField("image")) @@ -202,6 +210,9 @@ func validateVolume(ctx context.Context, volume corev1.Volume) *apis.FieldError if cfg.Features.PodSpecVolumesCSI == config.Enabled { fieldPaths = append(fieldPaths, "csi") } + if cfg.Features.PodSpecVolumesEphemeral == config.Enabled { + fieldPaths = append(fieldPaths, "ephemeral") + } if cfg.Features.PodSpecVolumesImage == config.Enabled { fieldPaths = append(fieldPaths, "image") } @@ -717,7 +728,7 @@ func validateVolumeMounts(ctx context.Context, mounts []corev1.VolumeMount, volu } seenMountPath.Insert(path.Clean(vm.MountPath)) - shouldCheckReadOnlyVolume := volumes[vm.Name].EmptyDir == nil && volumes[vm.Name].PersistentVolumeClaim == nil + shouldCheckReadOnlyVolume := volumes[vm.Name].EmptyDir == nil && volumes[vm.Name].PersistentVolumeClaim == nil && volumes[vm.Name].Ephemeral == nil if shouldCheckReadOnlyVolume && !vm.ReadOnly { errs = errs.Also((&apis.FieldError{ Message: "volume mount should be readOnly for this type of volume", diff --git a/pkg/apis/serving/k8s_validation_test.go b/pkg/apis/serving/k8s_validation_test.go index 711f7a565dea..fa6a62508458 100644 --- a/pkg/apis/serving/k8s_validation_test.go +++ b/pkg/apis/serving/k8s_validation_test.go @@ -157,6 +157,13 @@ func withPodSpecVolumesCSIEnabled() configOption { } } +func withPodSpecVolumesEphemeralEnabled() configOption { + return func(cfg *config.Config) *config.Config { + cfg.Features.PodSpecVolumesEphemeral = config.Enabled + return cfg + } +} + func withPodSpecVolumesImageEnabled() configOption { return func(cfg *config.Config) *config.Config { cfg.Features.PodSpecVolumesImage = config.Enabled @@ -3492,6 +3499,45 @@ func TestVolumeValidation(t *testing.T) { }, cfgOpts: []configOption{withPodSpecVolumesImageEnabled()}, want: apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "image"), + }, { + name: "valid ephemeral volume with feature enabled", + v: corev1.Volume{ + Name: "foo", + VolumeSource: corev1.VolumeSource{ + Ephemeral: &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + }, + }, + }, + }, + }, + cfgOpts: []configOption{withPodSpecVolumesEphemeralEnabled()}, + }, { + name: "ephemeral volume with feature disabled", + v: corev1.Volume{ + Name: "foo", + VolumeSource: corev1.VolumeSource{ + Ephemeral: &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + }, + }, + }, + }, + }, + want: (&apis.FieldError{ + Message: `Ephemeral volume support is disabled, but found Ephemeral volume foo`, + }).Also(&apis.FieldError{Message: "must not set the field(s)", Paths: []string{"ephemeral"}}), + }, { + name: "missing ephemeral volume when required", + v: corev1.Volume{ + Name: "foo", + }, + cfgOpts: []configOption{withPodSpecVolumesEphemeralEnabled()}, + want: apis.ErrMissingOneOf("secret", "configMap", "projected", "emptyDir", "ephemeral"), }} for _, test := range tests { diff --git a/pkg/apis/serving/v1/revision_defaults.go b/pkg/apis/serving/v1/revision_defaults.go index be8c81238032..f2014724bf45 100644 --- a/pkg/apis/serving/v1/revision_defaults.go +++ b/pkg/apis/serving/v1/revision_defaults.go @@ -145,7 +145,7 @@ func (rs *RevisionSpec) applyDefault(ctx context.Context, container *corev1.Cont vNames := make(sets.Set[string]) for _, v := range rs.PodSpec.Volumes { - if v.EmptyDir != nil || v.PersistentVolumeClaim != nil { + if v.EmptyDir != nil || v.PersistentVolumeClaim != nil || v.Ephemeral != nil { vNames.Insert(v.Name) } }