mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 08:17:26 +00:00
2387 lines
64 KiB
Go
2387 lines
64 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
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 validation
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
"k8s.io/kubernetes/pkg/apis/storage"
|
|
"k8s.io/kubernetes/pkg/features"
|
|
utilpointer "k8s.io/utils/pointer"
|
|
)
|
|
|
|
var (
|
|
deleteReclaimPolicy = api.PersistentVolumeReclaimDelete
|
|
immediateMode1 = storage.VolumeBindingImmediate
|
|
immediateMode2 = storage.VolumeBindingImmediate
|
|
waitingMode = storage.VolumeBindingWaitForFirstConsumer
|
|
invalidMode = storage.VolumeBindingMode("foo")
|
|
inlineSpec = api.PersistentVolumeSpec{
|
|
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
|
PersistentVolumeSource: api.PersistentVolumeSource{
|
|
CSI: &api.CSIPersistentVolumeSource{
|
|
Driver: "com.test.foo",
|
|
VolumeHandle: "foobar",
|
|
},
|
|
},
|
|
}
|
|
longerIDValidateOption = CSINodeValidationOptions{
|
|
AllowLongNodeID: true,
|
|
}
|
|
shorterIDValidationOption = CSINodeValidationOptions{
|
|
AllowLongNodeID: false,
|
|
}
|
|
)
|
|
|
|
func TestValidateStorageClass(t *testing.T) {
|
|
deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete")
|
|
retainReclaimPolicy := api.PersistentVolumeReclaimPolicy("Retain")
|
|
recycleReclaimPolicy := api.PersistentVolumeReclaimPolicy("Recycle")
|
|
successCases := []storage.StorageClass{
|
|
{
|
|
// empty parameters
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
Parameters: map[string]string{},
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
VolumeBindingMode: &immediateMode1,
|
|
},
|
|
{
|
|
// nil parameters
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
VolumeBindingMode: &immediateMode1,
|
|
},
|
|
{
|
|
// some parameters
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
Parameters: map[string]string{
|
|
"kubernetes.io/foo-parameter": "free/form/string",
|
|
"foo-parameter": "free-form-string",
|
|
"foo-parameter2": "{\"embedded\": \"json\", \"with\": {\"structures\":\"inside\"}}",
|
|
},
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
VolumeBindingMode: &immediateMode1,
|
|
},
|
|
{
|
|
// retain reclaimPolicy
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
ReclaimPolicy: &retainReclaimPolicy,
|
|
VolumeBindingMode: &immediateMode1,
|
|
},
|
|
}
|
|
|
|
// Success cases are expected to pass validation.
|
|
for k, v := range successCases {
|
|
if errs := ValidateStorageClass(&v); len(errs) != 0 {
|
|
t.Errorf("Expected success for %d, got %v", k, errs)
|
|
}
|
|
}
|
|
|
|
// generate a map longer than maxProvisionerParameterSize
|
|
longParameters := make(map[string]string)
|
|
totalSize := 0
|
|
for totalSize < maxProvisionerParameterSize {
|
|
k := fmt.Sprintf("param/%d", totalSize)
|
|
v := fmt.Sprintf("value-%d", totalSize)
|
|
longParameters[k] = v
|
|
totalSize = totalSize + len(k) + len(v)
|
|
}
|
|
|
|
errorCases := map[string]storage.StorageClass{
|
|
"namespace is present": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
},
|
|
"invalid provisioner": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/invalid/provisioner",
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
},
|
|
"invalid empty parameter name": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo",
|
|
Parameters: map[string]string{
|
|
"": "value",
|
|
},
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
},
|
|
"provisioner: Required value": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "",
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
},
|
|
"too long parameters": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo",
|
|
Parameters: longParameters,
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
},
|
|
"invalid reclaimpolicy": {
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Provisioner: "kubernetes.io/foo",
|
|
ReclaimPolicy: &recycleReclaimPolicy,
|
|
},
|
|
}
|
|
|
|
// Error cases are not expected to pass validation.
|
|
for testName, storageClass := range errorCases {
|
|
if errs := ValidateStorageClass(&storageClass); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %s", testName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVolumeAttachmentValidation(t *testing.T) {
|
|
volumeName := "pv-name"
|
|
empty := ""
|
|
migrationEnabledSuccessCases := []storage.VolumeAttachment{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-with-inlinespec"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
InlineVolumeSpec: &inlineSpec,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-with-status"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-with-inlinespec-and-status"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
InlineVolumeSpec: &inlineSpec,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range migrationEnabledSuccessCases {
|
|
if errs := ValidateVolumeAttachment(&volumeAttachment); len(errs) != 0 {
|
|
t.Errorf("expected success: %v %v", volumeAttachment, errs)
|
|
}
|
|
}
|
|
migrationEnabledErrorCases := []storage.VolumeAttachment{
|
|
{
|
|
// Empty attacher name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "",
|
|
NodeName: "mynode",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Empty node name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// No volume name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: nil,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Empty volume name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &empty,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Too long error message
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: strings.Repeat("a", maxVolumeErrorMessageSize+1),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Too long metadata
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": strings.Repeat("a", maxAttachedVolumeMetadataSize),
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// VolumeAttachmentSource with no PersistentVolumeName nor InlineSpec
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{},
|
|
},
|
|
},
|
|
{
|
|
// VolumeAttachmentSource with PersistentVolumeName and InlineSpec
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
InlineVolumeSpec: &inlineSpec,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// VolumeAttachmentSource with InlineSpec without CSI PV Source
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "node",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
InlineVolumeSpec: &api.PersistentVolumeSpec{
|
|
Capacity: api.ResourceList{
|
|
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
|
|
},
|
|
AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
|
|
PersistentVolumeSource: api.PersistentVolumeSource{
|
|
FlexVolume: &api.FlexPersistentVolumeSource{
|
|
Driver: "kubernetes.io/blue",
|
|
FSType: "ext4",
|
|
},
|
|
},
|
|
StorageClassName: "test-storage-class",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range migrationEnabledErrorCases {
|
|
if errs := ValidateVolumeAttachment(&volumeAttachment); len(errs) == 0 {
|
|
t.Errorf("expected failure for test: %v", volumeAttachment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVolumeAttachmentUpdateValidation(t *testing.T) {
|
|
volumeName := "foo"
|
|
newVolumeName := "bar"
|
|
|
|
old := storage.VolumeAttachment{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{},
|
|
NodeName: "mynode",
|
|
},
|
|
}
|
|
|
|
successCases := []storage.VolumeAttachment{
|
|
{
|
|
// no change
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
// modify status
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{},
|
|
NodeName: "mynode",
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range successCases {
|
|
volumeAttachment.Spec.Source = storage.VolumeAttachmentSource{}
|
|
old.Spec.Source = storage.VolumeAttachmentSource{}
|
|
// test scenarios with PersistentVolumeName set
|
|
volumeAttachment.Spec.Source.PersistentVolumeName = &volumeName
|
|
old.Spec.Source.PersistentVolumeName = &volumeName
|
|
if errs := ValidateVolumeAttachmentUpdate(&volumeAttachment, &old); len(errs) != 0 {
|
|
t.Errorf("expected success: %+v", errs)
|
|
}
|
|
|
|
volumeAttachment.Spec.Source = storage.VolumeAttachmentSource{}
|
|
old.Spec.Source = storage.VolumeAttachmentSource{}
|
|
// test scenarios with InlineVolumeSpec set
|
|
volumeAttachment.Spec.Source.InlineVolumeSpec = &inlineSpec
|
|
old.Spec.Source.InlineVolumeSpec = &inlineSpec
|
|
if errs := ValidateVolumeAttachmentUpdate(&volumeAttachment, &old); len(errs) != 0 {
|
|
t.Errorf("expected success: %+v", errs)
|
|
}
|
|
}
|
|
|
|
// reset old's source with volumeName in case it was left with something else by earlier tests
|
|
old.Spec.Source = storage.VolumeAttachmentSource{}
|
|
old.Spec.Source.PersistentVolumeName = &volumeName
|
|
|
|
errorCases := []storage.VolumeAttachment{
|
|
{
|
|
// change attacher
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "another-attacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
// change source volume name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &newVolumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
// change node
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "anothernode",
|
|
},
|
|
},
|
|
{
|
|
// change source
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
InlineVolumeSpec: &inlineSpec,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
{
|
|
// add invalid status
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
Status: storage.VolumeAttachmentStatus{
|
|
Attached: true,
|
|
AttachmentMetadata: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
AttachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: strings.Repeat("a", maxAttachedVolumeMetadataSize),
|
|
},
|
|
DetachError: &storage.VolumeError{
|
|
Time: metav1.Time{},
|
|
Message: "hello world",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range errorCases {
|
|
if errs := ValidateVolumeAttachmentUpdate(&volumeAttachment, &old); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %+v", volumeAttachment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVolumeAttachmentValidationV1(t *testing.T) {
|
|
volumeName := "pv-name"
|
|
invalidVolumeName := "-invalid-@#$%^&*()-"
|
|
successCases := []storage.VolumeAttachment{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
NodeName: "mynode",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range successCases {
|
|
if errs := ValidateVolumeAttachmentV1(&volumeAttachment); len(errs) != 0 {
|
|
t.Errorf("expected success: %+v", errs)
|
|
}
|
|
}
|
|
|
|
errorCases := []storage.VolumeAttachment{
|
|
{
|
|
// Invalid attacher name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "invalid-@#$%^&*()",
|
|
NodeName: "mynode",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &volumeName,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Invalid PV name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.VolumeAttachmentSpec{
|
|
Attacher: "myattacher",
|
|
NodeName: "mynode",
|
|
Source: storage.VolumeAttachmentSource{
|
|
PersistentVolumeName: &invalidVolumeName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, volumeAttachment := range errorCases {
|
|
if errs := ValidateVolumeAttachmentV1(&volumeAttachment); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %+v", volumeAttachment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeClass(mode *storage.VolumeBindingMode, topologies []api.TopologySelectorTerm) *storage.StorageClass {
|
|
return &storage.StorageClass{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "foo"},
|
|
Provisioner: "kubernetes.io/foo-provisioner",
|
|
ReclaimPolicy: &deleteReclaimPolicy,
|
|
VolumeBindingMode: mode,
|
|
AllowedTopologies: topologies,
|
|
}
|
|
}
|
|
|
|
type bindingTest struct {
|
|
class *storage.StorageClass
|
|
shouldSucceed bool
|
|
}
|
|
|
|
func TestValidateVolumeBindingMode(t *testing.T) {
|
|
cases := map[string]bindingTest{
|
|
"no mode": {
|
|
class: makeClass(nil, nil),
|
|
shouldSucceed: false,
|
|
},
|
|
"immediate mode": {
|
|
class: makeClass(&immediateMode1, nil),
|
|
shouldSucceed: true,
|
|
},
|
|
"waiting mode": {
|
|
class: makeClass(&waitingMode, nil),
|
|
shouldSucceed: true,
|
|
},
|
|
"invalid mode": {
|
|
class: makeClass(&invalidMode, nil),
|
|
shouldSucceed: false,
|
|
},
|
|
}
|
|
|
|
for testName, testCase := range cases {
|
|
errs := ValidateStorageClass(testCase.class)
|
|
if testCase.shouldSucceed && len(errs) != 0 {
|
|
t.Errorf("Expected success for test %q, got %v", testName, errs)
|
|
}
|
|
if !testCase.shouldSucceed && len(errs) == 0 {
|
|
t.Errorf("Expected failure for test %q, got success", testName)
|
|
}
|
|
}
|
|
}
|
|
|
|
type updateTest struct {
|
|
oldClass *storage.StorageClass
|
|
newClass *storage.StorageClass
|
|
shouldSucceed bool
|
|
}
|
|
|
|
func TestValidateUpdateVolumeBindingMode(t *testing.T) {
|
|
noBinding := makeClass(nil, nil)
|
|
immediateBinding1 := makeClass(&immediateMode1, nil)
|
|
immediateBinding2 := makeClass(&immediateMode2, nil)
|
|
waitBinding := makeClass(&waitingMode, nil)
|
|
|
|
cases := map[string]updateTest{
|
|
"old and new no mode": {
|
|
oldClass: noBinding,
|
|
newClass: noBinding,
|
|
shouldSucceed: true,
|
|
},
|
|
"old and new same mode ptr": {
|
|
oldClass: immediateBinding1,
|
|
newClass: immediateBinding1,
|
|
shouldSucceed: true,
|
|
},
|
|
"old and new same mode value": {
|
|
oldClass: immediateBinding1,
|
|
newClass: immediateBinding2,
|
|
shouldSucceed: true,
|
|
},
|
|
"old no mode, new mode": {
|
|
oldClass: noBinding,
|
|
newClass: waitBinding,
|
|
shouldSucceed: false,
|
|
},
|
|
"old mode, new no mode": {
|
|
oldClass: waitBinding,
|
|
newClass: noBinding,
|
|
shouldSucceed: false,
|
|
},
|
|
"old and new different modes": {
|
|
oldClass: waitBinding,
|
|
newClass: immediateBinding1,
|
|
shouldSucceed: false,
|
|
},
|
|
}
|
|
|
|
for testName, testCase := range cases {
|
|
errs := ValidateStorageClassUpdate(testCase.newClass, testCase.oldClass)
|
|
if testCase.shouldSucceed && len(errs) != 0 {
|
|
t.Errorf("Expected success for %v, got %v", testName, errs)
|
|
}
|
|
if !testCase.shouldSucceed && len(errs) == 0 {
|
|
t.Errorf("Expected failure for %v, got success", testName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateAllowedTopologies(t *testing.T) {
|
|
|
|
validTopology := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone2"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyInvalidKey := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "/invalidkey",
|
|
Values: []string{"zone1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyLackOfValues := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyDupValues := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1", "node1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyMultiValues := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1", "node2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyEmptyMatchLabelExpressions := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: nil,
|
|
},
|
|
}
|
|
|
|
topologyDupKeys := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyMultiTerm := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyDupTermsIdentical := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyExprsOneSameOneDiff := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyValuesOneSameOneDiff := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1", "node2"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1", "node3"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyDupTermsDiffExprOrder := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1"},
|
|
},
|
|
{
|
|
Key: "kubernetes.io/hostname",
|
|
Values: []string{"node1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
topologyDupTermsDiffValueOrder := []api.TopologySelectorTerm{
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone1", "zone2"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
|
|
{
|
|
Key: "failure-domain.beta.kubernetes.io/zone",
|
|
Values: []string{"zone2", "zone1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
cases := map[string]bindingTest{
|
|
"no topology": {
|
|
class: makeClass(&waitingMode, nil),
|
|
shouldSucceed: true,
|
|
},
|
|
"valid topology": {
|
|
class: makeClass(&waitingMode, validTopology),
|
|
shouldSucceed: true,
|
|
},
|
|
"topology invalid key": {
|
|
class: makeClass(&waitingMode, topologyInvalidKey),
|
|
shouldSucceed: false,
|
|
},
|
|
"topology lack of values": {
|
|
class: makeClass(&waitingMode, topologyLackOfValues),
|
|
shouldSucceed: false,
|
|
},
|
|
"duplicate TopologySelectorRequirement values": {
|
|
class: makeClass(&waitingMode, topologyDupValues),
|
|
shouldSucceed: false,
|
|
},
|
|
"multiple TopologySelectorRequirement values": {
|
|
class: makeClass(&waitingMode, topologyMultiValues),
|
|
shouldSucceed: true,
|
|
},
|
|
"empty MatchLabelExpressions": {
|
|
class: makeClass(&waitingMode, topologyEmptyMatchLabelExpressions),
|
|
shouldSucceed: false,
|
|
},
|
|
"duplicate MatchLabelExpression keys": {
|
|
class: makeClass(&waitingMode, topologyDupKeys),
|
|
shouldSucceed: false,
|
|
},
|
|
"duplicate MatchLabelExpression keys but across separate terms": {
|
|
class: makeClass(&waitingMode, topologyMultiTerm),
|
|
shouldSucceed: true,
|
|
},
|
|
"duplicate AllowedTopologies terms - identical": {
|
|
class: makeClass(&waitingMode, topologyDupTermsIdentical),
|
|
shouldSucceed: false,
|
|
},
|
|
"two AllowedTopologies terms, with a pair of the same MatchLabelExpressions and a pair of different ones": {
|
|
class: makeClass(&waitingMode, topologyExprsOneSameOneDiff),
|
|
shouldSucceed: true,
|
|
},
|
|
"two AllowedTopologies terms, with a pair of the same Values and a pair of different ones": {
|
|
class: makeClass(&waitingMode, topologyValuesOneSameOneDiff),
|
|
shouldSucceed: true,
|
|
},
|
|
"duplicate AllowedTopologies terms - different MatchLabelExpressions order": {
|
|
class: makeClass(&waitingMode, topologyDupTermsDiffExprOrder),
|
|
shouldSucceed: false,
|
|
},
|
|
"duplicate AllowedTopologies terms - different TopologySelectorRequirement values order": {
|
|
class: makeClass(&waitingMode, topologyDupTermsDiffValueOrder),
|
|
shouldSucceed: false,
|
|
},
|
|
}
|
|
|
|
for testName, testCase := range cases {
|
|
errs := ValidateStorageClass(testCase.class)
|
|
if testCase.shouldSucceed && len(errs) != 0 {
|
|
t.Errorf("Expected success for test %q, got %v", testName, errs)
|
|
}
|
|
if !testCase.shouldSucceed && len(errs) == 0 {
|
|
t.Errorf("Expected failure for test %q, got success", testName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSINodeValidation(t *testing.T) {
|
|
driverName := "driver-name"
|
|
driverName2 := "1io.kubernetes-storage-2-csi-driver3"
|
|
longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver" // 88 chars
|
|
nodeID := "nodeA"
|
|
longID := longName + longName + "abcdefghijklmnopqrstuvwxyz" // 202 chars
|
|
successCases := []storage.CSINode{
|
|
{
|
|
// driver name: dot only
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// driver name: dash only
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo2"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io-kubernetes-storage-csi-driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// driver name: numbers
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo3"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "1io-kubernetes-storage-2-csi-driver3",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// driver name: dot, dash
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo4"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage-csi-driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// driver name: dot, dash, and numbers
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo5"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName2,
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Driver name length 1
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo2"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "a",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// multiple drivers with different node IDs, topology keys
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo6"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1", "key2"},
|
|
},
|
|
{
|
|
Name: "driverB",
|
|
NodeID: "nodeA",
|
|
TopologyKeys: []string{"keyA", "keyB"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// multiple drivers with same node IDs, topology keys
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo7"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1"},
|
|
},
|
|
{
|
|
Name: "driver2",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Volume limits being zero
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo11"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(0)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Volume limits with positive number
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo11"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(1)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// topology key names with -, _, and dot .
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo8"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"zone_1", "zone.2"},
|
|
},
|
|
{
|
|
Name: "driver2",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"zone-3", "zone.4"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// topology prefix with - and dot.
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo9"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"company-com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// No topology keys
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo10"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName,
|
|
NodeID: nodeID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, csiNode := range successCases {
|
|
if errs := ValidateCSINode(&csiNode, shorterIDValidationOption); len(errs) != 0 {
|
|
t.Errorf("expected success: %v", errs)
|
|
}
|
|
}
|
|
|
|
nodeIDCase := storage.CSINode{
|
|
// node ID length > 128 but < 192
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo7"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName,
|
|
NodeID: longID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if errs := ValidateCSINode(&nodeIDCase, longerIDValidateOption); len(errs) != 0 {
|
|
t.Errorf("expected success: %v", errs)
|
|
}
|
|
|
|
errorCases := []storage.CSINode{
|
|
{
|
|
// Empty driver name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Invalid start char in driver name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo3"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "_io.kubernetes.storage.csi.driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Invalid end char in driver name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo4"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver/",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Invalid separators in driver name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo5"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io/kubernetes/storage/csi~driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// driver name: underscore only
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo6"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io_kubernetes_storage_csi_driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Driver name length > 63
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo7"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: longName,
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// No driver name
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo8"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Empty individual topology key
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo9"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName,
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", ""},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// duplicate drivers in driver specs
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo10"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1", "key2"},
|
|
},
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "nodeX",
|
|
TopologyKeys: []string{"keyA", "keyB"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// single driver with duplicate topology keys in driver specs
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo11"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1", "key1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// multiple drivers with one set of duplicate topology keys in driver specs
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo12"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "driver1",
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"key1"},
|
|
},
|
|
{
|
|
Name: "driver2",
|
|
NodeID: "nodeX",
|
|
TopologyKeys: []string{"keyA", "keyA"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Empty NodeID
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo13"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName,
|
|
NodeID: "",
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Volume limits with negative number
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo11"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(-1)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// topology prefix should be lower case
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo14"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: driverName,
|
|
NodeID: "node1",
|
|
TopologyKeys: []string{"Company.Com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
nodeIDCase,
|
|
}
|
|
|
|
for _, csiNode := range errorCases {
|
|
if errs := ValidateCSINode(&csiNode, shorterIDValidationOption); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %v", csiNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSINodeUpdateValidation(t *testing.T) {
|
|
//driverName := "driver-name"
|
|
//driverName2 := "1io.kubernetes-storage-2-csi-driver3"
|
|
//longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver"
|
|
nodeID := "nodeA"
|
|
|
|
old := storage.CSINode{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
successCases := []storage.CSINode{
|
|
{
|
|
// no change
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// remove a driver
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// add a driver
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-3",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(30)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// remove a driver and add a driver
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.new-driver",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(30)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, csiNode := range successCases {
|
|
if errs := ValidateCSINodeUpdate(&csiNode, &old, shorterIDValidationOption); len(errs) != 0 {
|
|
t.Errorf("expected success: %+v", errs)
|
|
}
|
|
}
|
|
|
|
errorCases := []storage.CSINode{
|
|
{
|
|
// invalid change node id
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: "nodeB",
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// invalid change topology keys
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// invalid change trying to set a previously unset allocatable
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(10)},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(20)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// invalid change trying to update allocatable with a different volume limit
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32(21)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// invalid change trying to update allocatable with an empty volume limit
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
Allocatable: &storage.VolumeNodeResources{Count: nil},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// invalid change trying to remove allocatable
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo1"},
|
|
Spec: storage.CSINodeSpec{
|
|
Drivers: []storage.CSINodeDriver{
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-1",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
{
|
|
Name: "io.kubernetes.storage.csi.driver-2",
|
|
NodeID: nodeID,
|
|
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, csiNode := range errorCases {
|
|
if errs := ValidateCSINodeUpdate(&csiNode, &old, shorterIDValidationOption); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %+v", csiNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSIDriverValidation(t *testing.T) {
|
|
driverName := "test-driver"
|
|
longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver"
|
|
invalidName := "-invalid-@#$%^&*()-"
|
|
attachRequired := true
|
|
attachNotRequired := false
|
|
podInfoOnMount := true
|
|
notPodInfoOnMount := false
|
|
notRequiresRepublish := false
|
|
storageCapacity := true
|
|
notStorageCapacity := false
|
|
seLinuxMount := true
|
|
notSELinuxMount := false
|
|
supportedFSGroupPolicy := storage.FileFSGroupPolicy
|
|
invalidFSGroupPolicy := storage.FSGroupPolicy("invalid-mode")
|
|
successCases := []storage.CSIDriver{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// driver name: dot only
|
|
ObjectMeta: metav1.ObjectMeta{Name: "io.kubernetes.storage.csi.driver"},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: ¬StorageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// driver name: dash only
|
|
ObjectMeta: metav1.ObjectMeta{Name: "io-kubernetes-storage-csi-driver"},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// driver name: numbers
|
|
ObjectMeta: metav1.ObjectMeta{Name: "1csi2driver3"},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// driver name: dot and dash
|
|
ObjectMeta: metav1.ObjectMeta{Name: "io.kubernetes.storage.csi-driver"},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecyclePersistent,
|
|
},
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecycleEphemeral,
|
|
},
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecycleEphemeral,
|
|
storage.VolumeLifecyclePersistent,
|
|
},
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecycleEphemeral,
|
|
storage.VolumeLifecyclePersistent,
|
|
storage.VolumeLifecycleEphemeral,
|
|
},
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
FSGroupPolicy: &supportedFSGroupPolicy,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// SELinuxMount: false
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: ¬SELinuxMount,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, csiDriver := range successCases {
|
|
if errs := ValidateCSIDriver(&csiDriver); len(errs) != 0 {
|
|
t.Errorf("expected success: %v", errs)
|
|
}
|
|
}
|
|
errorCases := []storage.CSIDriver{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: invalidName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: longName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// AttachRequired not set
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: nil,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// PodInfoOnMount not set
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: nil,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// StorageCapacity not set
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
StorageCapacity: nil,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// invalid mode
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
StorageCapacity: &storageCapacity,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
"no-such-mode",
|
|
},
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// invalid fsGroupPolicy
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
FSGroupPolicy: &invalidFSGroupPolicy,
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
},
|
|
{
|
|
// no SELinuxMount
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
StorageCapacity: &storageCapacity,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, csiDriver := range errorCases {
|
|
if errs := ValidateCSIDriver(&csiDriver); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %v", csiDriver)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSIDriverValidationUpdate(t *testing.T) {
|
|
driverName := "test-driver"
|
|
longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver"
|
|
invalidName := "-invalid-@#$%^&*()-"
|
|
attachRequired := true
|
|
attachNotRequired := false
|
|
podInfoOnMount := true
|
|
storageCapacity := true
|
|
notPodInfoOnMount := false
|
|
gcp := "gcp"
|
|
requiresRepublish := true
|
|
notRequiresRepublish := false
|
|
notStorageCapacity := false
|
|
seLinuxMount := true
|
|
notSELinuxMount := false
|
|
resourceVersion := "1"
|
|
old := storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName, ResourceVersion: resourceVersion},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachNotRequired,
|
|
PodInfoOnMount: ¬PodInfoOnMount,
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
VolumeLifecycleModes: []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecycleEphemeral,
|
|
storage.VolumeLifecyclePersistent,
|
|
},
|
|
StorageCapacity: &storageCapacity,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
}
|
|
|
|
successCases := []struct {
|
|
name string
|
|
modify func(new *storage.CSIDriver)
|
|
}{
|
|
{
|
|
name: "no change",
|
|
modify: func(new *storage.CSIDriver) {},
|
|
},
|
|
{
|
|
name: "change TokenRequests",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.TokenRequests = []storage.TokenRequest{{Audience: gcp}}
|
|
},
|
|
},
|
|
{
|
|
name: "change RequiresRepublish",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.RequiresRepublish = &requiresRepublish
|
|
},
|
|
},
|
|
{
|
|
name: "StorageCapacity changed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.StorageCapacity = ¬StorageCapacity
|
|
},
|
|
},
|
|
{
|
|
name: "SELinuxMount changed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.SELinuxMount = ¬SELinuxMount
|
|
},
|
|
},
|
|
}
|
|
for _, test := range successCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
new := old.DeepCopy()
|
|
test.modify(new)
|
|
if errs := ValidateCSIDriverUpdate(new, &old); len(errs) != 0 {
|
|
t.Errorf("Expected success for %+v: %v", new, errs)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Each test case changes exactly one field. None of that is valid.
|
|
errorCases := []struct {
|
|
name string
|
|
modify func(new *storage.CSIDriver)
|
|
}{
|
|
{
|
|
name: "invalid name",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Name = invalidName
|
|
},
|
|
},
|
|
{
|
|
name: "long name",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Name = longName
|
|
},
|
|
},
|
|
{
|
|
name: "AttachRequired not set",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.AttachRequired = nil
|
|
},
|
|
},
|
|
{
|
|
name: "AttachRequired changed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.AttachRequired = &attachRequired
|
|
},
|
|
},
|
|
{
|
|
name: "PodInfoOnMount not set",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.PodInfoOnMount = nil
|
|
},
|
|
},
|
|
{
|
|
name: "PodInfoOnMount changed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.PodInfoOnMount = &podInfoOnMount
|
|
},
|
|
},
|
|
{
|
|
name: "invalid volume lifecycle mode",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
|
|
"no-such-mode",
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "volume lifecycle modes not set",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.VolumeLifecycleModes = nil
|
|
},
|
|
},
|
|
{
|
|
name: "VolumeLifecyclePersistent removed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecycleEphemeral,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "VolumeLifecycleEphemeral removed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.VolumeLifecycleModes = []storage.VolumeLifecycleMode{
|
|
storage.VolumeLifecyclePersistent,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "FSGroupPolicy invalidated",
|
|
modify: func(new *storage.CSIDriver) {
|
|
invalidFSGroupPolicy := storage.FSGroupPolicy("invalid")
|
|
new.Spec.FSGroupPolicy = &invalidFSGroupPolicy
|
|
},
|
|
},
|
|
{
|
|
name: "FSGroupPolicy changed",
|
|
modify: func(new *storage.CSIDriver) {
|
|
fileFSGroupPolicy := storage.FileFSGroupPolicy
|
|
new.Spec.FSGroupPolicy = &fileFSGroupPolicy
|
|
},
|
|
},
|
|
{
|
|
name: "TokenRequests invalidated",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.TokenRequests = []storage.TokenRequest{{Audience: gcp}, {Audience: gcp}}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid nil StorageCapacity",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.StorageCapacity = nil
|
|
},
|
|
},
|
|
{
|
|
name: "SELinuxMount not set",
|
|
modify: func(new *storage.CSIDriver) {
|
|
new.Spec.SELinuxMount = nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range errorCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
new := old.DeepCopy()
|
|
test.modify(new)
|
|
if errs := ValidateCSIDriverUpdate(new, &old); len(errs) == 0 {
|
|
t.Errorf("Expected failure for test: %+v", new)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCSIDriverStorageCapacityEnablement(t *testing.T) {
|
|
run := func(t *testing.T, withField bool) {
|
|
driverName := "test-driver"
|
|
attachRequired := true
|
|
podInfoOnMount := true
|
|
requiresRepublish := true
|
|
storageCapacity := true
|
|
seLinuxMount := false
|
|
csiDriver := storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: &attachRequired,
|
|
PodInfoOnMount: &podInfoOnMount,
|
|
RequiresRepublish: &requiresRepublish,
|
|
SELinuxMount: &seLinuxMount,
|
|
},
|
|
}
|
|
if withField {
|
|
csiDriver.Spec.StorageCapacity = &storageCapacity
|
|
}
|
|
errs := ValidateCSIDriver(&csiDriver)
|
|
success := withField
|
|
if success && len(errs) != 0 {
|
|
t.Errorf("expected success, got: %v", errs)
|
|
}
|
|
if !success && len(errs) == 0 {
|
|
t.Errorf("expected error, got success")
|
|
}
|
|
}
|
|
|
|
yesNo := []bool{true, false}
|
|
for _, withField := range yesNo {
|
|
t.Run(fmt.Sprintf("with-field=%v", withField), func(t *testing.T) {
|
|
run(t, withField)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateCSIStorageCapacity(t *testing.T) {
|
|
storageClassName := "test-sc"
|
|
invalidName := "-invalid-@#$%^&*()-"
|
|
|
|
goodCapacity := storage.CSIStorageCapacity{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "csc-329803da-fdd2-42e4-af6f-7b07e7ccc305",
|
|
Namespace: metav1.NamespaceDefault,
|
|
},
|
|
StorageClassName: storageClassName,
|
|
}
|
|
goodTopology := metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"foo": "bar"},
|
|
}
|
|
|
|
scenarios := map[string]struct {
|
|
isExpectedFailure bool
|
|
capacity *storage.CSIStorageCapacity
|
|
}{
|
|
"good-capacity": {
|
|
capacity: &goodCapacity,
|
|
},
|
|
"missing-storage-class-name": {
|
|
isExpectedFailure: true,
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.StorageClassName = ""
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"bad-storage-class-name": {
|
|
isExpectedFailure: true,
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.StorageClassName = invalidName
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"good-capacity-value": {
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.Capacity = resource.NewQuantity(1, resource.BinarySI)
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"bad-capacity-value": {
|
|
isExpectedFailure: true,
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.Capacity = resource.NewQuantity(-11, resource.BinarySI)
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"good-topology": {
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.NodeTopology = &goodTopology
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"empty-topology": {
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.NodeTopology = &metav1.LabelSelector{}
|
|
return &capacity
|
|
}(),
|
|
},
|
|
"bad-topology-fields": {
|
|
isExpectedFailure: true,
|
|
capacity: func() *storage.CSIStorageCapacity {
|
|
capacity := goodCapacity
|
|
capacity.NodeTopology = &metav1.LabelSelector{
|
|
MatchExpressions: []metav1.LabelSelectorRequirement{
|
|
{
|
|
Key: "foo",
|
|
Operator: metav1.LabelSelectorOperator("no-such-operator"),
|
|
Values: []string{
|
|
"bar",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return &capacity
|
|
}(),
|
|
},
|
|
}
|
|
|
|
for name, scenario := range scenarios {
|
|
t.Run(name, func(t *testing.T) {
|
|
errs := ValidateCSIStorageCapacity(scenario.capacity, CSIStorageCapacityValidateOptions{false})
|
|
if len(errs) == 0 && scenario.isExpectedFailure {
|
|
t.Errorf("Unexpected success")
|
|
}
|
|
if len(errs) > 0 && !scenario.isExpectedFailure {
|
|
t.Errorf("Unexpected failure: %+v", errs)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCSIServiceAccountToken(t *testing.T) {
|
|
driverName := "test-driver"
|
|
gcp := "gcp"
|
|
aws := "aws"
|
|
notRequiresRepublish := false
|
|
tests := []struct {
|
|
desc string
|
|
csiDriver *storage.CSIDriver
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "invalid - TokenRequests has tokens with the same audience",
|
|
csiDriver: &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
TokenRequests: []storage.TokenRequest{{Audience: gcp}, {Audience: gcp}},
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "invalid - TokenRequests has tokens with ExpirationSeconds less than 10min",
|
|
csiDriver: &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
TokenRequests: []storage.TokenRequest{{Audience: gcp, ExpirationSeconds: utilpointer.Int64(10)}},
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "invalid - TokenRequests has tokens with ExpirationSeconds longer than 1<<32 min",
|
|
csiDriver: &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
TokenRequests: []storage.TokenRequest{{Audience: gcp, ExpirationSeconds: utilpointer.Int64(1<<32 + 1)}},
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "valid - TokenRequests has at most one token with empty string audience",
|
|
csiDriver: &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
TokenRequests: []storage.TokenRequest{{Audience: ""}},
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "valid - TokenRequests has tokens with different audience",
|
|
csiDriver: &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: driverName},
|
|
Spec: storage.CSIDriverSpec{
|
|
TokenRequests: []storage.TokenRequest{{}, {Audience: gcp}, {Audience: aws}},
|
|
RequiresRepublish: ¬RequiresRepublish,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test.csiDriver.Spec.AttachRequired = new(bool)
|
|
test.csiDriver.Spec.PodInfoOnMount = new(bool)
|
|
test.csiDriver.Spec.StorageCapacity = new(bool)
|
|
test.csiDriver.Spec.SELinuxMount = new(bool)
|
|
if errs := ValidateCSIDriver(test.csiDriver); test.wantErr != (len(errs) != 0) {
|
|
t.Errorf("ValidateCSIDriver = %v, want err: %v", errs, test.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCSIDriverValidationSELinuxMountAlpha(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
featureEnabled bool
|
|
seLinuxMountValue *bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "feature enabled, nil value",
|
|
featureEnabled: true,
|
|
seLinuxMountValue: nil,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "feature enabled, non-nil value",
|
|
featureEnabled: true,
|
|
seLinuxMountValue: utilpointer.Bool(true),
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "feature disabled, nil value",
|
|
featureEnabled: false,
|
|
seLinuxMountValue: nil,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "feature disabled, non-nil value",
|
|
featureEnabled: false,
|
|
seLinuxMountValue: utilpointer.Bool(true),
|
|
expectError: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SELinuxMountReadWriteOncePod, test.featureEnabled)()
|
|
csiDriver := &storage.CSIDriver{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
|
Spec: storage.CSIDriverSpec{
|
|
AttachRequired: utilpointer.Bool(true),
|
|
PodInfoOnMount: utilpointer.Bool(true),
|
|
RequiresRepublish: utilpointer.Bool(true),
|
|
StorageCapacity: utilpointer.Bool(true),
|
|
SELinuxMount: test.seLinuxMountValue,
|
|
},
|
|
}
|
|
err := ValidateCSIDriver(csiDriver)
|
|
if test.expectError && err == nil {
|
|
t.Error("Expected validation error, got nil")
|
|
}
|
|
if !test.expectError && err != nil {
|
|
t.Errorf("Validation returned error: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|