Make CSINodeInfo and CSIDriver Core APIs

This PR is the first step to transition CSINodeInfo and CSIDriver
CRD's to in-tree APIs. It adds them to the existing API group
“storage.k8s.io” as core storage APIs.
This commit is contained in:
Xing Yang
2019-02-08 08:06:07 -08:00
parent e1b79abfec
commit bb45b8ee34
36 changed files with 2379 additions and 26 deletions

View File

@@ -85,6 +85,8 @@ filegroup(
"//pkg/registry/scheduling/rest:all-srcs",
"//pkg/registry/settings/podpreset:all-srcs",
"//pkg/registry/settings/rest:all-srcs",
"//pkg/registry/storage/csidriver:all-srcs",
"//pkg/registry/storage/csinode:all-srcs",
"//pkg/registry/storage/rest:all-srcs",
"//pkg/registry/storage/storageclass:all-srcs",
"//pkg/registry/storage/volumeattachment:all-srcs",

View File

@@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"strategy.go",
],
importpath = "k8s.io/kubernetes/pkg/registry/storage/csidriver",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/apis/storage/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/registry/storage/csidriver/storage:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["strategy_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/storage:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
],
)

View File

@@ -0,0 +1,19 @@
/*
Copyright 2019 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 csidriver provides Registry interface and its REST
// implementation for storing csidriver api objects.
package csidriver

View File

@@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["storage.go"],
importpath = "k8s.io/kubernetes/pkg/registry/storage/csidriver/storage",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/storage:go_default_library",
"//pkg/registry/storage/csidriver:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["storage_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/api/testapi:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/registry/registrytest:go_default_library",
"//staging/src/k8s.io/api/storage/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library",
],
)

View File

@@ -0,0 +1,57 @@
/*
Copyright 2019 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 storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
storageapi "k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/registry/storage/csidriver"
)
// CSIDriverStorage includes storage for CSIDrivers and all subresources
type CSIDriverStorage struct {
CSIDriver *REST
}
// REST object that will work for CSIDrivers
type REST struct {
*genericregistry.Store
}
// NewStorage returns a RESTStorage object that will work against CSIDrivers
func NewStorage(optsGetter generic.RESTOptionsGetter) *CSIDriverStorage {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &storageapi.CSIDriver{} },
NewListFunc: func() runtime.Object { return &storageapi.CSIDriverList{} },
DefaultQualifiedResource: storageapi.Resource("csidrivers"),
CreateStrategy: csidriver.Strategy,
UpdateStrategy: csidriver.Strategy,
DeleteStrategy: csidriver.Strategy,
ReturnDeletedObject: true,
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up
}
return &CSIDriverStorage{
CSIDriver: &REST{store},
}
}

View File

@@ -0,0 +1,179 @@
/*
Copyright 2019 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 storage
import (
"testing"
storageapiv1beta1 "k8s.io/api/storage/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/kubernetes/pkg/api/testapi"
storageapi "k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorage(t, storageapi.GroupName)
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "csidrivers",
}
csiDriverStorage := NewStorage(restOptions)
return csiDriverStorage.CSIDriver, server
}
func validNewCSIDriver(name string) *storageapi.CSIDriver {
attachRequired := true
podInfoOnMount := true
return &storageapi.CSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: storageapi.CSIDriverSpec{
AttachRequired: &attachRequired,
PodInfoOnMount: &podInfoOnMount,
},
}
}
func TestCreate(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
csiDriver := validNewCSIDriver("foo")
csiDriver.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
attachNotRequired := false
notPodInfoOnMount := false
test.TestCreate(
// valid
csiDriver,
// invalid
&storageapi.CSIDriver{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
Spec: storageapi.CSIDriverSpec{
AttachRequired: &attachNotRequired,
PodInfoOnMount: &notPodInfoOnMount,
},
},
)
}
func TestUpdate(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
notPodInfoOnMount := false
test.TestUpdate(
// valid
validNewCSIDriver("foo"),
//invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*storageapi.CSIDriver)
object.Spec.PodInfoOnMount = &notPodInfoOnMount
return object
},
)
}
func TestDelete(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject()
test.TestDelete(validNewCSIDriver("foo"))
}
func TestGet(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestGet(validNewCSIDriver("foo"))
}
func TestList(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestList(validNewCSIDriver("foo"))
}
func TestWatch(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestWatch(
validNewCSIDriver("foo"),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"foo": "bar"},
},
// matching fields
[]fields.Set{
{"metadata.name": "foo"},
},
// not matching fields
[]fields.Set{
{"metadata.name": "bar"},
},
)
}

View File

@@ -0,0 +1,78 @@
/*
Copyright 2019 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 csidriver
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/apis/storage/validation"
)
// csiDriverStrategy implements behavior for CSIDriver objects
type csiDriverStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
// Strategy is the default logic that applies when creating and updating
// CSIDriver objects via the REST API.
var Strategy = csiDriverStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (csiDriverStrategy) NamespaceScoped() bool {
return false
}
// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation.
func (csiDriverStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}
func (csiDriverStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
csiDriver := obj.(*storage.CSIDriver)
errs := validation.ValidateCSIDriver(csiDriver)
errs = append(errs, validation.ValidateCSIDriver(csiDriver)...)
return errs
}
// Canonicalize normalizes the object after validation.
func (csiDriverStrategy) Canonicalize(obj runtime.Object) {
}
func (csiDriverStrategy) AllowCreateOnUpdate() bool {
return false
}
// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a CSIDriver
func (csiDriverStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}
func (csiDriverStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
newCSIDriverObj := obj.(*storage.CSIDriver)
oldCSIDriverObj := old.(*storage.CSIDriver)
errorList := validation.ValidateCSIDriver(newCSIDriverObj)
return append(errorList, validation.ValidateCSIDriverUpdate(newCSIDriverObj, oldCSIDriverObj)...)
}
func (csiDriverStrategy) AllowUnconditionalUpdate() bool {
return false
}

View File

@@ -0,0 +1,155 @@
/*
Copyright 2019 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 csidriver
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/storage"
)
func getValidCSIDriver(name string) *storage.CSIDriver {
attachRequired := true
podInfoOnMount := true
return &storage.CSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: storage.CSIDriverSpec{
AttachRequired: &attachRequired,
PodInfoOnMount: &podInfoOnMount,
},
}
}
func TestCSIDriverStrategy(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{
APIGroup: "storage.k8s.io",
APIVersion: "v1beta1",
Resource: "csidrivers",
})
if Strategy.NamespaceScoped() {
t.Errorf("CSIDriver must not be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("CSIDriver should not allow create on update")
}
csiDriver := getValidCSIDriver("valid-csidriver")
Strategy.PrepareForCreate(ctx, csiDriver)
errs := Strategy.Validate(ctx, csiDriver)
if len(errs) != 0 {
t.Errorf("unexpected error validating %v", errs)
}
// Update of spec is disallowed
newCSIDriver := csiDriver.DeepCopy()
attachNotRequired := false
newCSIDriver.Spec.AttachRequired = &attachNotRequired
Strategy.PrepareForUpdate(ctx, newCSIDriver, csiDriver)
errs = Strategy.ValidateUpdate(ctx, newCSIDriver, csiDriver)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
}
func TestCSIDriverValidation(t *testing.T) {
attachRequired := true
notAttachRequired := false
podInfoOnMount := true
notPodInfoOnMount := false
tests := []struct {
name string
csiDriver *storage.CSIDriver
expectError bool
}{
{
"valid csidriver",
getValidCSIDriver("foo"),
false,
},
{
"true PodInfoOnMount and AttachRequired",
&storage.CSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: storage.CSIDriverSpec{
AttachRequired: &attachRequired,
PodInfoOnMount: &podInfoOnMount,
},
},
false,
},
{
"false PodInfoOnMount and AttachRequired",
&storage.CSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: storage.CSIDriverSpec{
AttachRequired: &notAttachRequired,
PodInfoOnMount: &notPodInfoOnMount,
},
},
false,
},
{
"invalid driver name",
&storage.CSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: "*foo#",
},
Spec: storage.CSIDriverSpec{
AttachRequired: &attachRequired,
PodInfoOnMount: &podInfoOnMount,
},
},
true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testValidation := func(csiDriver *storage.CSIDriver, apiVersion string) field.ErrorList {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{
APIGroup: "storage.k8s.io",
APIVersion: "v1beta1",
Resource: "csidrivers",
})
return Strategy.Validate(ctx, csiDriver)
}
betaErr := testValidation(test.csiDriver, "v1beta1")
if len(betaErr) > 0 && !test.expectError {
t.Errorf("Validation of v1beta1 object failed: %+v", betaErr)
}
if len(betaErr) == 0 && test.expectError {
t.Errorf("Validation of v1beta1 object unexpectedly succeeded")
}
})
}
}

View File

@@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"strategy.go",
],
importpath = "k8s.io/kubernetes/pkg/registry/storage/csinode",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/apis/storage/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/registry/storage/csinode/storage:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["strategy_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/storage:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
],
)

View File

@@ -0,0 +1,19 @@
/*
Copyright 2019 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 csinode provides Registry interface and its REST
// implementation for storing csinode api objects.
package csinode

View File

@@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["storage.go"],
importpath = "k8s.io/kubernetes/pkg/registry/storage/csinode/storage",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/storage:go_default_library",
"//pkg/registry/storage/csinode:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["storage_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/api/testapi:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/registry/registrytest:go_default_library",
"//staging/src/k8s.io/api/storage/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library",
],
)

View File

@@ -0,0 +1,57 @@
/*
Copyright 2019 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 storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
storageapi "k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/registry/storage/csinode"
)
// CSINodeStorage includes storage for CSINodes and all subresources
type CSINodeStorage struct {
CSINode *REST
}
// REST object that will work for CSINodes
type REST struct {
*genericregistry.Store
}
// NewStorage returns a RESTStorage object that will work against CSINodes
func NewStorage(optsGetter generic.RESTOptionsGetter) *CSINodeStorage {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &storageapi.CSINode{} },
NewListFunc: func() runtime.Object { return &storageapi.CSINodeList{} },
DefaultQualifiedResource: storageapi.Resource("csinodes"),
CreateStrategy: csinode.Strategy,
UpdateStrategy: csinode.Strategy,
DeleteStrategy: csinode.Strategy,
ReturnDeletedObject: true,
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up
}
return &CSINodeStorage{
CSINode: &REST{store},
}
}

View File

@@ -0,0 +1,190 @@
/*
Copyright 2019 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 storage
import (
"testing"
storageapiv1beta1 "k8s.io/api/storage/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/kubernetes/pkg/api/testapi"
storageapi "k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorage(t, storageapi.GroupName)
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "csinodes",
}
csiNodeStorage := NewStorage(restOptions)
return csiNodeStorage.CSINode, server
}
func validNewCSINode(name string) *storageapi.CSINode {
return &storageapi.CSINode{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: storageapi.CSINodeSpec{
Drivers: []storageapi.CSINodeDriver{
{
Name: "valid-driver-name",
NodeID: "valid-node",
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
},
},
},
}
}
func TestCreate(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
csiNode := validNewCSINode("foo")
csiNode.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
test.TestCreate(
// valid
csiNode,
// invalid
&storageapi.CSINode{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
Spec: storageapi.CSINodeSpec{
Drivers: []storageapi.CSINodeDriver{
{
Name: "invalid-name-!@#$%^&*()",
NodeID: "invalid-node-!@#$%^&*()",
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
},
},
},
},
)
}
func TestUpdate(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestUpdate(
// valid
validNewCSINode("foo"),
// we allow status field to be set in v1beta1
func(obj runtime.Object) runtime.Object {
object := obj.(*storageapi.CSINode)
//object.Status = *getCSINodeStatus()
return object
},
//invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*storageapi.CSINode)
object.Spec.Drivers[0].Name = "invalid-name-!@#$%^&*()"
return object
},
)
}
func TestDelete(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject()
test.TestDelete(validNewCSINode("foo"))
}
func TestGet(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestGet(validNewCSINode("foo"))
}
func TestList(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestList(validNewCSINode("foo"))
}
func TestWatch(t *testing.T) {
if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion {
// skip the test for all versions exception v1beta1
return
}
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestWatch(
validNewCSINode("foo"),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"foo": "bar"},
},
// matching fields
[]fields.Set{
{"metadata.name": "foo"},
},
// not matching fields
[]fields.Set{
{"metadata.name": "bar"},
},
)
}

View File

@@ -0,0 +1,78 @@
/*
Copyright 2019 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 csinode
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/apis/storage/validation"
)
// csiNodeStrategy implements behavior for CSINode objects
type csiNodeStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
// Strategy is the default logic that applies when creating and updating
// CSINode objects via the REST API.
var Strategy = csiNodeStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (csiNodeStrategy) NamespaceScoped() bool {
return false
}
// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation.
func (csiNodeStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}
func (csiNodeStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
csiNode := obj.(*storage.CSINode)
errs := validation.ValidateCSINode(csiNode)
errs = append(errs, validation.ValidateCSINode(csiNode)...)
return errs
}
// Canonicalize normalizes the object after validation.
func (csiNodeStrategy) Canonicalize(obj runtime.Object) {
}
func (csiNodeStrategy) AllowCreateOnUpdate() bool {
return false
}
// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a CSINode
func (csiNodeStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}
func (csiNodeStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
newCSINodeObj := obj.(*storage.CSINode)
oldCSINodeObj := old.(*storage.CSINode)
errorList := validation.ValidateCSINode(newCSINodeObj)
return append(errorList, validation.ValidateCSINodeUpdate(newCSINodeObj, oldCSINodeObj)...)
}
func (csiNodeStrategy) AllowUnconditionalUpdate() bool {
return false
}

View File

@@ -0,0 +1,167 @@
/*
Copyright 2019 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 csinode
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/storage"
)
func getValidCSINode(name string) *storage.CSINode {
return &storage.CSINode{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: storage.CSINodeSpec{
Drivers: []storage.CSINodeDriver{
{
Name: "valid-driver-name",
NodeID: "valid-node",
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
},
},
},
}
}
func TestCSINodeStrategy(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{
APIGroup: "storage.k8s.io",
APIVersion: "v1beta1",
Resource: "csinodes",
})
if Strategy.NamespaceScoped() {
t.Errorf("CSINode must not be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("CSINode should not allow create on update")
}
csiNode := getValidCSINode("valid-csinode")
Strategy.PrepareForCreate(ctx, csiNode)
errs := Strategy.Validate(ctx, csiNode)
if len(errs) != 0 {
t.Errorf("unexpected error validating %v", errs)
}
// Update of spec is allowed
newCSINode := csiNode.DeepCopy()
newCSINode.Spec.Drivers[0].NodeID = "valid-node-2"
Strategy.PrepareForUpdate(ctx, newCSINode, csiNode)
errs = Strategy.ValidateUpdate(ctx, newCSINode, csiNode)
if len(errs) == 0 {
t.Errorf("expected validation error")
}
}
func TestCSINodeValidation(t *testing.T) {
tests := []struct {
name string
csiNode *storage.CSINode
expectError bool
}{
{
"valid csinode",
getValidCSINode("foo"),
false,
},
{
"invalid driver name",
&storage.CSINode{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: storage.CSINodeSpec{
Drivers: []storage.CSINodeDriver{
{
Name: "$csi-driver@",
NodeID: "valid-node",
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
},
},
},
},
true,
},
{
"empty node id",
&storage.CSINode{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: storage.CSINodeSpec{
Drivers: []storage.CSINodeDriver{
{
Name: "valid-driver-name",
NodeID: "",
TopologyKeys: []string{"company.com/zone1", "company.com/zone2"},
},
},
},
},
true,
},
{
"invalid topology keys",
&storage.CSINode{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: storage.CSINodeSpec{
Drivers: []storage.CSINodeDriver{
{
Name: "valid-driver-name",
NodeID: "valid-node",
TopologyKeys: []string{"company.com/zone1", ""},
},
},
},
},
true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testValidation := func(csiNode *storage.CSINode, apiVersion string) field.ErrorList {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{
APIGroup: "storage.k8s.io",
APIVersion: "v1beta1",
Resource: "csinodes",
})
return Strategy.Validate(ctx, csiNode)
}
betaErr := testValidation(test.csiNode, "v1beta1")
if len(betaErr) > 0 && !test.expectError {
t.Errorf("Validation of v1beta1 object failed: %+v", betaErr)
}
if len(betaErr) == 0 && test.expectError {
t.Errorf("Validation of v1beta1 object unexpectedly succeeded")
}
})
}
}

View File

@@ -12,6 +12,9 @@ go_library(
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/features:go_default_library",
"//pkg/registry/storage/csidriver/storage:go_default_library",
"//pkg/registry/storage/csinode/storage:go_default_library",
"//pkg/registry/storage/storageclass/storage:go_default_library",
"//pkg/registry/storage/volumeattachment/storage:go_default_library",
"//staging/src/k8s.io/api/storage/v1:go_default_library",
@@ -21,6 +24,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/server:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/server/storage:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
],
)

View File

@@ -24,8 +24,12 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
storageapi "k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/features"
csidriverstore "k8s.io/kubernetes/pkg/registry/storage/csidriver/storage"
csinodestore "k8s.io/kubernetes/pkg/registry/storage/csinode/storage"
storageclassstore "k8s.io/kubernetes/pkg/registry/storage/storageclass/storage"
volumeattachmentstore "k8s.io/kubernetes/pkg/registry/storage/volumeattachment/storage"
)
@@ -70,6 +74,18 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag
volumeAttachmentStorage := volumeattachmentstore.NewStorage(restOptionsGetter)
storage["volumeattachments"] = volumeAttachmentStorage.VolumeAttachment
// register csinodes if CSINodeInfo feature gate is enabled
if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) {
csiNodeStorage := csinodestore.NewStorage(restOptionsGetter)
storage["csinodes"] = csiNodeStorage.CSINode
}
// register csidrivers if CSIDriverRegistry feature gate is enabled
if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) {
csiDriverStorage := csidriverstore.NewStorage(restOptionsGetter)
storage["csidrivers"] = csiDriverStorage.CSIDriver
}
return storage
}