mirror of https://github.com/k3s-io/k3s
Merge pull request #30900 from jsafrane/pvc-admission
Automatic merge from submit-queue Add admission controller for default storage class. The admission controller adds a default class to PVCs that do not require any specific class. This way, users (=PVC authors) do not need to care about storage classes, administrator can configure a default one and all these PVCs that do not care about class will get the default one. The marker of default class is annotation "volume.beta.kubernetes.io/storage-class", which must be set to "true" to work. All other values (or missing annotation) makes the class non-default. Based on @thockin's code, added tests and made it not to reject a PVC when no class is marked as default. . @kubernetes/sig-storagepull/6/head
commit
ef2718620c
|
@ -138,7 +138,7 @@ fi
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Optional: Enable/disable public IP assignment for minions.
|
||||
# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
|
||||
|
|
|
@ -121,7 +121,7 @@ fi
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Optional: Enable/disable public IP assignment for minions.
|
||||
# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
|
||||
|
|
|
@ -57,4 +57,4 @@ ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-influxdb}"
|
|||
ENABLE_CLUSTER_UI="${KUBE_ENABLE_CLUSTER_UI:-true}"
|
||||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
|
|
|
@ -42,7 +42,7 @@ export FLANNEL_NET=${FLANNEL_NET:-"172.16.0.0/16"}
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
export ADMISSION_CONTROL=NamespaceLifecycle,NamespaceExists,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota
|
||||
export ADMISSION_CONTROL=NamespaceLifecycle,NamespaceExists,LimitRanger,ServiceAccount,SecurityContextDeny,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Extra options to set on the Docker command line.
|
||||
# This is useful for setting --insecure-registry for local registries.
|
||||
|
|
|
@ -56,7 +56,7 @@ KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=${SERVICE_CLUSTER_IP_RANGE}"
|
|||
# Comma-delimited list of:
|
||||
# LimitRanger, AlwaysDeny, SecurityContextDeny, NamespaceExists,
|
||||
# NamespaceLifecycle, NamespaceAutoProvision,
|
||||
# AlwaysAdmit, ServiceAccount, ResourceQuota
|
||||
# AlwaysAdmit, ServiceAccount, ResourceQuota, DefaultStorageClass
|
||||
KUBE_ADMISSION_CONTROL="--admission-control=${ADMISSION_CONTROL}"
|
||||
|
||||
# --client-ca-file="": If set, any request presenting a client certificate signed
|
||||
|
|
|
@ -136,7 +136,7 @@ ENABLE_RESCHEDULER="${KUBE_ENABLE_RESCHEDULER:-false}"
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Optional: if set to true kube-up will automatically check for existing resources and clean them up.
|
||||
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
|
||||
|
|
|
@ -155,7 +155,7 @@ fi
|
|||
ENABLE_RESCHEDULER="${KUBE_ENABLE_RESCHEDULER:-false}"
|
||||
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,ResourceQuota}"
|
||||
ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota}"
|
||||
|
||||
# Optional: if set to true kube-up will automatically check for existing resources and clean them up.
|
||||
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"--service-cluster-ip-range=10.0.0.1/24",
|
||||
"--insecure-bind-address=0.0.0.0",
|
||||
"--etcd-servers=http://127.0.0.1:2379",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota",
|
||||
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||
"--min-request-timeout=300",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"--service-cluster-ip-range=10.0.0.1/24",
|
||||
"--insecure-bind-address=127.0.0.1",
|
||||
"--etcd-servers=http://127.0.0.1:2379",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota",
|
||||
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||
"--min-request-timeout=300",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"--etcd-certfile={{ etcd_cert }}",
|
||||
{%- endif %}
|
||||
"--etcd-servers={{ connection_string }}",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota",
|
||||
"--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota",
|
||||
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||
"--min-request-timeout=300",
|
||||
|
|
|
@ -25,7 +25,7 @@ source "$KUBE_ROOT/cluster/common.sh"
|
|||
|
||||
export LIBVIRT_DEFAULT_URI=qemu:///system
|
||||
export SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
|
||||
export ADMISSION_CONTROL=${ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota}
|
||||
export ADMISSION_CONTROL=${ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota}
|
||||
readonly POOL=kubernetes
|
||||
readonly POOL_PATH=/var/lib/libvirt/images/kubernetes
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ apiserver:
|
|||
--external-hostname=apiserver
|
||||
--etcd-servers=http://etcd:4001
|
||||
--port=8888
|
||||
--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
--authorization-mode=AlwaysAllow
|
||||
--token-auth-file=/var/run/kubernetes/auth/token-users
|
||||
--basic-auth-file=/var/run/kubernetes/auth/basic-users
|
||||
|
|
|
@ -49,7 +49,7 @@ write_files:
|
|||
dns_domain: cluster.local
|
||||
federations_domain_map: ''
|
||||
instance_prefix: kubernetes
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
enable_cpu_cfs_quota: "true"
|
||||
network_provider: none
|
||||
opencontrail_tag: R2.20
|
||||
|
|
|
@ -124,5 +124,5 @@ federations_domain_map: ''
|
|||
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
||||
cluster_cidr: "$NODE_IP_RANGES"
|
||||
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
EOF
|
||||
|
|
|
@ -68,7 +68,7 @@ FLANNEL_OTHER_NET_CONFIG=''
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
export ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota
|
||||
export ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Path to the config file or directory of files of kubelet
|
||||
export KUBELET_CONFIG=${KUBELET_CONFIG:-""}
|
||||
|
|
|
@ -56,7 +56,7 @@ MASTER_PASSWD="${MASTER_PASSWD:-vagrant}"
|
|||
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
# If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely.
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
|
||||
# Optional: Enable node logging.
|
||||
ENABLE_NODE_LOGGING=false
|
||||
|
|
|
@ -124,7 +124,7 @@ federations_domain_map: ''
|
|||
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
||||
cluster_cidr: "$NODE_IP_RANGES"
|
||||
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||
EOF
|
||||
|
||||
mkdir -p /srv/salt-overlay/salt/nginx
|
||||
|
|
|
@ -40,4 +40,5 @@ import (
|
|||
_ "k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy"
|
||||
_ "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
_ "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
_ "k8s.io/kubernetes/plugin/pkg/admission/storageclass/default"
|
||||
)
|
||||
|
|
|
@ -264,9 +264,9 @@ function set_service_accounts {
|
|||
function start_apiserver {
|
||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||
if [[ -z "${ALLOW_SECURITY_CONTEXT}" ]]; then
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota,DefaultStorageClass
|
||||
else
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota
|
||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,DefaultStorageClass
|
||||
fi
|
||||
# This is the default dir and filename where the apiserver will generate a self-signed cert
|
||||
# which should be able to be used as the CA to verify itself
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
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 admission
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
admission "k8s.io/kubernetes/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginName = "DefaultStorageClass"
|
||||
)
|
||||
|
||||
func init() {
|
||||
admission.RegisterPlugin(PluginName, func(client clientset.Interface, config io.Reader) (admission.Interface, error) {
|
||||
plugin := newPlugin(client)
|
||||
plugin.Run()
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// claimDefaulterPlugin holds state for and implements the admission plugin.
|
||||
type claimDefaulterPlugin struct {
|
||||
*admission.Handler
|
||||
client clientset.Interface
|
||||
|
||||
reflector *cache.Reflector
|
||||
stopChan chan struct{}
|
||||
store cache.Store
|
||||
}
|
||||
|
||||
var _ admission.Interface = &claimDefaulterPlugin{}
|
||||
|
||||
// newPlugin creates a new admission plugin.
|
||||
func newPlugin(kclient clientset.Interface) *claimDefaulterPlugin {
|
||||
store := cache.NewStore(cache.MetaNamespaceKeyFunc)
|
||||
reflector := cache.NewReflector(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options api.ListOptions) (runtime.Object, error) {
|
||||
return kclient.Extensions().StorageClasses().List(options)
|
||||
},
|
||||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
||||
return kclient.Extensions().StorageClasses().Watch(options)
|
||||
},
|
||||
},
|
||||
&extensions.StorageClass{},
|
||||
store,
|
||||
0,
|
||||
)
|
||||
|
||||
return &claimDefaulterPlugin{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
client: kclient,
|
||||
store: store,
|
||||
reflector: reflector,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *claimDefaulterPlugin) Run() {
|
||||
if a.stopChan == nil {
|
||||
a.stopChan = make(chan struct{})
|
||||
}
|
||||
a.reflector.RunUntil(a.stopChan)
|
||||
}
|
||||
func (a *claimDefaulterPlugin) Stop() {
|
||||
if a.stopChan != nil {
|
||||
close(a.stopChan)
|
||||
a.stopChan = nil
|
||||
}
|
||||
}
|
||||
|
||||
// This is a stand-in until we have a real field. This string should be a const somewhere.
|
||||
const classAnnotation = "volume.beta.kubernetes.io/storage-class"
|
||||
|
||||
// This indicates that a particular StorageClass nominates itself as the system default.
|
||||
const isDefaultAnnotation = "storageclass.beta.kubernetes.io/is-default-class"
|
||||
|
||||
// Admit sets the default value of a PersistentVolumeClaim's storage class, in case the user did
|
||||
// not provide a value.
|
||||
//
|
||||
// 1. Find available StorageClasses.
|
||||
// 2. Figure which is the default
|
||||
// 3. Write to the PVClaim
|
||||
func (c *claimDefaulterPlugin) Admit(a admission.Attributes) error {
|
||||
if a.GetResource().GroupResource() != api.Resource("persistentvolumeclaims") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pvc, ok := a.GetObject().(*api.PersistentVolumeClaim)
|
||||
// if we can't convert then we don't handle this object so just return
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, found := pvc.Annotations[classAnnotation]
|
||||
if found {
|
||||
// The user asked for a class.
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.V(4).Infof("no storage class for claim %s (generate: %s)", pvc.Name, pvc.GenerateName)
|
||||
|
||||
def, err := getDefaultClass(c.store)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
if def == nil {
|
||||
// No default class selected, do nothing about the PVC.
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.V(4).Infof("defaulting storage class for claim %s (generate: %s) to %s", pvc.Name, pvc.GenerateName, def.Name)
|
||||
if pvc.ObjectMeta.Annotations == nil {
|
||||
pvc.ObjectMeta.Annotations = map[string]string{}
|
||||
}
|
||||
pvc.Annotations[classAnnotation] = def.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultClass returns the default StorageClass from the store, or nil.
|
||||
func getDefaultClass(store cache.Store) (*extensions.StorageClass, error) {
|
||||
defaultClasses := []*extensions.StorageClass{}
|
||||
for _, c := range store.List() {
|
||||
class, ok := c.(*extensions.StorageClass)
|
||||
if !ok {
|
||||
return nil, errors.NewInternalError(fmt.Errorf("error converting stored object to StorageClass: %v", c))
|
||||
}
|
||||
if class.Annotations[isDefaultAnnotation] == "true" {
|
||||
defaultClasses = append(defaultClasses, class)
|
||||
glog.V(4).Infof("getDefaultClass added: %s", class.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(defaultClasses) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(defaultClasses) > 1 {
|
||||
glog.V(4).Infof("getDefaultClass %s defaults found", len(defaultClasses))
|
||||
return nil, errors.NewInternalError(fmt.Errorf("%d default StorageClasses were found", len(defaultClasses)))
|
||||
}
|
||||
return defaultClasses[0], nil
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
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 admission
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/kubernetes/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/conversion"
|
||||
)
|
||||
|
||||
func TestAdmission(t *testing.T) {
|
||||
defaultClass1 := &extensions.StorageClass{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default1",
|
||||
Annotations: map[string]string{
|
||||
isDefaultAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Provisioner: "default1",
|
||||
}
|
||||
defaultClass2 := &extensions.StorageClass{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default2",
|
||||
Annotations: map[string]string{
|
||||
isDefaultAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Provisioner: "default2",
|
||||
}
|
||||
// Class that has explicit default = false
|
||||
classWithFalseDefault := &extensions.StorageClass{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "nondefault1",
|
||||
Annotations: map[string]string{
|
||||
isDefaultAnnotation: "false",
|
||||
},
|
||||
},
|
||||
Provisioner: "nondefault1",
|
||||
}
|
||||
// Class with missing default annotation (=non-default)
|
||||
classWithNoDefault := &extensions.StorageClass{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "nondefault2",
|
||||
},
|
||||
Provisioner: "nondefault1",
|
||||
}
|
||||
// Class with empty default annotation (=non-default)
|
||||
classWithEmptyDefault := &extensions.StorageClass{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "nondefault2",
|
||||
Annotations: map[string]string{
|
||||
isDefaultAnnotation: "",
|
||||
},
|
||||
},
|
||||
Provisioner: "nondefault1",
|
||||
}
|
||||
|
||||
claimWithClass := &api.PersistentVolumeClaim{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "PersistentVolumeClaim",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "claimWithClass",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
classAnnotation: "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
claimWithEmptyClass := &api.PersistentVolumeClaim{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "PersistentVolumeClaim",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "claimWithEmptyClass",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
classAnnotation: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
claimWithNoClass := &api.PersistentVolumeClaim{
|
||||
TypeMeta: unversioned.TypeMeta{
|
||||
Kind: "PersistentVolumeClaim",
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "claimWithNoClass",
|
||||
Namespace: "ns",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
classes []*extensions.StorageClass
|
||||
claim *api.PersistentVolumeClaim
|
||||
expectError bool
|
||||
expectedClassName string
|
||||
}{
|
||||
{
|
||||
"no default, no modification of PVCs",
|
||||
[]*extensions.StorageClass{classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithNoClass,
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"one default, modify PVC with class=nil",
|
||||
[]*extensions.StorageClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithNoClass,
|
||||
false,
|
||||
"default1",
|
||||
},
|
||||
{
|
||||
"one default, no modification of PVC with class=''",
|
||||
[]*extensions.StorageClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithEmptyClass,
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"one default, no modification of PVC with class='foo'",
|
||||
[]*extensions.StorageClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithClass,
|
||||
false,
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"two defaults, error with PVC with class=nil",
|
||||
[]*extensions.StorageClass{defaultClass1, defaultClass2, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithNoClass,
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"two defaults, no modification of PVC with class=''",
|
||||
[]*extensions.StorageClass{defaultClass1, defaultClass2, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithEmptyClass,
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"two defaults, no modification of PVC with class='foo'",
|
||||
[]*extensions.StorageClass{defaultClass1, defaultClass2, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
claimWithClass,
|
||||
false,
|
||||
"foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
glog.V(4).Infof("starting test %q", test.name)
|
||||
|
||||
// clone the claim, it's going to be modified
|
||||
clone, err := conversion.NewCloner().DeepCopy(test.claim)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot clone claim: %v", err)
|
||||
}
|
||||
claim := clone.(*api.PersistentVolumeClaim)
|
||||
|
||||
ctrl := newPlugin(nil)
|
||||
for _, c := range test.classes {
|
||||
ctrl.store.Add(c)
|
||||
}
|
||||
attrs := admission.NewAttributesRecord(
|
||||
claim, // new object
|
||||
nil, // old object
|
||||
api.Kind("PersistentVolumeClaim").WithVersion("version"),
|
||||
claim.Namespace,
|
||||
claim.Name,
|
||||
api.Resource("persistentvolumeclaims").WithVersion("version"),
|
||||
"", // subresource
|
||||
admission.Create,
|
||||
nil, // userInfo
|
||||
)
|
||||
err = ctrl.Admit(attrs)
|
||||
glog.Infof("Got %v", err)
|
||||
if err != nil && !test.expectError {
|
||||
t.Errorf("Test %q: unexpected error received: %v", test.name, err)
|
||||
}
|
||||
if err == nil && test.expectError {
|
||||
t.Errorf("Test %q: expected error and no error recevied", test.name)
|
||||
}
|
||||
|
||||
class := ""
|
||||
if claim.Annotations != nil {
|
||||
if value, ok := claim.Annotations[classAnnotation]; ok {
|
||||
class = value
|
||||
}
|
||||
}
|
||||
if test.expectedClassName != "" && test.expectedClassName != class {
|
||||
t.Errorf("Test %q: expected class name %q, got %q", test.name, test.expectedClassName, class)
|
||||
}
|
||||
if test.expectedClassName == "" && class != "" {
|
||||
t.Errorf("Test %q: expected class name %q, got %q", test.name, test.expectedClassName, class)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue