Skip to content

Commit 029fd47

Browse files
committed
storage: introduce CSIDriver.Spec.VolumeLifecycleModes
Using a "normal" CSI driver for an inline ephemeral volume may have unexpected and potentially harmful effects when the driver gets a NodePublishVolume call that it isn't expecting. To prevent that mistake, driver deployments for a driver that supports such volumes must: - deploy a CSIDriver object for the driver - list "ephemeral" as one of the supported modes The default is "persistent", so existing deployments continue to work and are automatically protected against incorrect usage. This commit contains the API change. Generated code and manual code which uses the new API follow.
1 parent f22b67d commit 029fd47

File tree

8 files changed

+343
-2
lines changed

8 files changed

+343
-2
lines changed

api/api-rules/violation_exceptions.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ API rule violation: list_type_missing,k8s.io/api/storage/v1,StorageClassList,Ite
344344
API rule violation: list_type_missing,k8s.io/api/storage/v1,VolumeAttachmentList,Items
345345
API rule violation: list_type_missing,k8s.io/api/storage/v1alpha1,VolumeAttachmentList,Items
346346
API rule violation: list_type_missing,k8s.io/api/storage/v1beta1,CSIDriverList,Items
347+
API rule violation: list_type_missing,k8s.io/api/storage/v1beta1,CSIDriverSpec,VolumeLifecycleModes
347348
API rule violation: list_type_missing,k8s.io/api/storage/v1beta1,CSINodeDriver,TopologyKeys
348349
API rule violation: list_type_missing,k8s.io/api/storage/v1beta1,CSINodeList,Items
349350
API rule violation: list_type_missing,k8s.io/api/storage/v1beta1,CSINodeSpec,Drivers

pkg/apis/storage/fuzzer/fuzzer.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package fuzzer
1818

1919
import (
20+
"fmt"
2021
fuzz "github.com/google/gofuzz"
2122

2223
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
@@ -37,6 +38,37 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
3738
func(obj *storage.CSIDriver, c fuzz.Continue) {
3839
c.FuzzNoCustom(obj) // fuzz self without calling this function again
3940

41+
// Custom fuzzing for volume modes.
42+
switch c.Rand.Intn(7) {
43+
case 0:
44+
obj.Spec.VolumeLifecycleModes = nil
45+
case 1:
46+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{}
47+
case 2:
48+
// Invalid mode.
49+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
50+
storage.VolumeLifecycleMode(fmt.Sprintf("%d", c.Rand.Int31())),
51+
}
52+
case 3:
53+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
54+
storage.VolumeLifecyclePersistent,
55+
}
56+
case 4:
57+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
58+
storage.VolumeLifecycleEphemeral,
59+
}
60+
case 5:
61+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
62+
storage.VolumeLifecyclePersistent,
63+
storage.VolumeLifecycleEphemeral,
64+
}
65+
case 6:
66+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
67+
storage.VolumeLifecycleEphemeral,
68+
storage.VolumeLifecyclePersistent,
69+
}
70+
}
71+
4072
// match defaulting
4173
if obj.Spec.AttachRequired == nil {
4274
obj.Spec.AttachRequired = new(bool)
@@ -46,6 +78,11 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
4678
obj.Spec.PodInfoOnMount = new(bool)
4779
*(obj.Spec.PodInfoOnMount) = false
4880
}
81+
if obj.Spec.VolumeLifecycleModes == nil {
82+
obj.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
83+
storage.VolumeLifecyclePersistent,
84+
}
85+
}
4986
},
5087
}
5188
}

pkg/apis/storage/types.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,47 @@ type CSIDriverSpec struct {
293293
// "csi.storage.k8s.io/pod.uid": string(pod.UID)
294294
// +optional
295295
PodInfoOnMount *bool
296+
297+
// VolumeLifecycleModes defines what kind of volumes this CSI volume driver supports.
298+
// The default if the list is empty is "Persistent", which is the usage
299+
// defined by the CSI specification and implemented in Kubernetes via the usual
300+
// PV/PVC mechanism.
301+
// The other mode is "Ephemeral". In this mode, volumes are defined inline
302+
// inside the pod spec with CSIVolumeSource and their lifecycle is tied to
303+
// the lifecycle of that pod. A driver has to be aware of this
304+
// because it is only going to get a NodePublishVolume call for such a volume.
305+
// For more information about implementing this mode, see
306+
// https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html
307+
// A driver can support one or more of these mode and
308+
// more modes may be added in the future.
309+
// +optional
310+
VolumeLifecycleModes []VolumeLifecycleMode
296311
}
297312

313+
// VolumeLifecycleMode specifies how a CSI volume is used in Kubernetes.
314+
// More modes may be added in the future.
315+
type VolumeLifecycleMode string
316+
317+
const (
318+
// VolumeLifecyclePersistent explicitly confirms that the driver implements
319+
// the full CSI spec. It is the default when CSIDriverSpec.VolumeLifecycleModes is not
320+
// set. Such volumes are managed in Kubernetes via the persistent volume
321+
// claim mechanism and have a lifecycle that is independent of the pods which
322+
// use them.
323+
VolumeLifecyclePersistent VolumeLifecycleMode = "Persistent"
324+
// VolumeLifecycleEphemeral indicates that the driver can be used for
325+
// ephemeral inline volumes. Such volumes are specified inside the pod
326+
// spec with a CSIVolumeSource and, as far as Kubernetes is concerned, have
327+
// a lifecycle that is tied to the lifecycle of the pod. For example, such
328+
// a volume might contain data that gets created specifically for that pod,
329+
// like secrets.
330+
// But how the volume actually gets created and managed is entirely up to
331+
// the driver. It might also use reference counting to share the same volume
332+
// instance among different pods if the CSIVolumeSource of those pods is
333+
// identical.
334+
VolumeLifecycleEphemeral VolumeLifecycleMode = "Ephemeral"
335+
)
336+
298337
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
299338

300339
// CSINode holds information about all CSI drivers installed on a node.

pkg/apis/storage/v1beta1/defaults.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"k8s.io/api/core/v1"
2121
storagev1beta1 "k8s.io/api/storage/v1beta1"
2222
"k8s.io/apimachinery/pkg/runtime"
23+
utilfeature "k8s.io/apiserver/pkg/util/feature"
24+
"k8s.io/kubernetes/pkg/features"
2325
)
2426

2527
func addDefaultingFuncs(scheme *runtime.Scheme) error {
@@ -47,4 +49,7 @@ func SetDefaults_CSIDriver(obj *storagev1beta1.CSIDriver) {
4749
obj.Spec.PodInfoOnMount = new(bool)
4850
*(obj.Spec.PodInfoOnMount) = false
4951
}
52+
if len(obj.Spec.VolumeLifecycleModes) == 0 && utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
53+
obj.Spec.VolumeLifecycleModes = append(obj.Spec.VolumeLifecycleModes, storagev1beta1.VolumeLifecyclePersistent)
54+
}
5055
}

pkg/apis/storage/v1beta1/defaults_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import (
2222

2323
storagev1beta1 "k8s.io/api/storage/v1beta1"
2424
"k8s.io/apimachinery/pkg/runtime"
25+
utilfeature "k8s.io/apiserver/pkg/util/feature"
26+
featuregatetesting "k8s.io/component-base/featuregate/testing"
2527
"k8s.io/kubernetes/pkg/api/legacyscheme"
2628
_ "k8s.io/kubernetes/pkg/apis/storage/install"
29+
"k8s.io/kubernetes/pkg/features"
2730
)
2831

2932
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
@@ -81,3 +84,30 @@ func TestSetDefaultAttachRequired(t *testing.T) {
8184
t.Errorf("Expected PodInfoOnMount to be defaulted to: %+v, got: %+v", defaultPodInfo, outPodInfo)
8285
}
8386
}
87+
88+
func TestSetDefaultVolumeLifecycleModesEnabled(t *testing.T) {
89+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIInlineVolume, true)()
90+
driver := &storagev1beta1.CSIDriver{}
91+
92+
// field should be defaulted
93+
defaultMode := storagev1beta1.VolumeLifecyclePersistent
94+
output := roundTrip(t, runtime.Object(driver)).(*storagev1beta1.CSIDriver)
95+
outModes := output.Spec.VolumeLifecycleModes
96+
if len(outModes) != 1 {
97+
t.Errorf("Expected VolumeLifecycleModes to be defaulted to: %+v, got: %+v", defaultMode, outModes)
98+
} else if outModes[0] != defaultMode {
99+
t.Errorf("Expected VolumeLifecycleModes to be defaulted to: %+v, got: %+v", defaultMode, outModes)
100+
}
101+
}
102+
103+
func TestSetDefaultVolumeLifecycleModesDisabled(t *testing.T) {
104+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIInlineVolume, false)()
105+
driver := &storagev1beta1.CSIDriver{}
106+
107+
// field should not be defaulted
108+
output := roundTrip(t, runtime.Object(driver)).(*storagev1beta1.CSIDriver)
109+
outModes := output.Spec.VolumeLifecycleModes
110+
if outModes != nil {
111+
t.Errorf("Expected VolumeLifecycleModes to remain nil, got: %+v", outModes)
112+
}
113+
}

pkg/registry/storage/csidriver/strategy.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import (
2222
"k8s.io/apimachinery/pkg/runtime"
2323
"k8s.io/apimachinery/pkg/util/validation/field"
2424
"k8s.io/apiserver/pkg/storage/names"
25+
utilfeature "k8s.io/apiserver/pkg/util/feature"
2526
"k8s.io/kubernetes/pkg/api/legacyscheme"
2627
"k8s.io/kubernetes/pkg/apis/storage"
2728
"k8s.io/kubernetes/pkg/apis/storage/validation"
29+
"k8s.io/kubernetes/pkg/features"
2830
)
2931

3032
// csiDriverStrategy implements behavior for CSIDriver objects
@@ -41,8 +43,12 @@ func (csiDriverStrategy) NamespaceScoped() bool {
4143
return false
4244
}
4345

44-
// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation.
46+
// PrepareForCreate clears the VolumeLifecycleModes field if the corresponding feature is disabled.
4547
func (csiDriverStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
48+
if !utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
49+
csiDriver := obj.(*storage.CSIDriver)
50+
csiDriver.Spec.VolumeLifecycleModes = nil
51+
}
4652
}
4753

4854
func (csiDriverStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@@ -62,8 +68,15 @@ func (csiDriverStrategy) AllowCreateOnUpdate() bool {
6268
return false
6369
}
6470

65-
// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a CSIDriver
71+
// PrepareForUpdate clears the VolumeLifecycleModes field if the corresponding feature is disabled and
72+
// existing object does not already have that field set. This allows the field to remain when
73+
// downgrading to a version that has the feature disabled.
6674
func (csiDriverStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
75+
if old.(*storage.CSIDriver).Spec.VolumeLifecycleModes == nil &&
76+
!utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
77+
newCSIDriver := obj.(*storage.CSIDriver)
78+
newCSIDriver.Spec.VolumeLifecycleModes = nil
79+
}
6780
}
6881

6982
func (csiDriverStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {

0 commit comments

Comments
 (0)