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
|
# 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.
|
# 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.
|
# 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!
|
# 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
|
# 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.
|
# 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.
|
# 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!
|
# 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}"
|
ENABLE_CLUSTER_UI="${KUBE_ENABLE_CLUSTER_UI:-true}"
|
||||||
|
|
||||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
# 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
|
# 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.
|
# 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.
|
# Extra options to set on the Docker command line.
|
||||||
# This is useful for setting --insecure-registry for local registries.
|
# 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:
|
# Comma-delimited list of:
|
||||||
# LimitRanger, AlwaysDeny, SecurityContextDeny, NamespaceExists,
|
# LimitRanger, AlwaysDeny, SecurityContextDeny, NamespaceExists,
|
||||||
# NamespaceLifecycle, NamespaceAutoProvision,
|
# NamespaceLifecycle, NamespaceAutoProvision,
|
||||||
# AlwaysAdmit, ServiceAccount, ResourceQuota
|
# AlwaysAdmit, ServiceAccount, ResourceQuota, DefaultStorageClass
|
||||||
KUBE_ADMISSION_CONTROL="--admission-control=${ADMISSION_CONTROL}"
|
KUBE_ADMISSION_CONTROL="--admission-control=${ADMISSION_CONTROL}"
|
||||||
|
|
||||||
# --client-ca-file="": If set, any request presenting a client certificate signed
|
# --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
|
# 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.
|
# 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.
|
# 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}
|
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
|
||||||
|
|
|
@ -155,7 +155,7 @@ fi
|
||||||
ENABLE_RESCHEDULER="${KUBE_ENABLE_RESCHEDULER:-false}"
|
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.
|
# 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.
|
# 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}
|
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"--service-cluster-ip-range=10.0.0.1/24",
|
"--service-cluster-ip-range=10.0.0.1/24",
|
||||||
"--insecure-bind-address=0.0.0.0",
|
"--insecure-bind-address=0.0.0.0",
|
||||||
"--etcd-servers=http://127.0.0.1:2379",
|
"--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",
|
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||||
"--min-request-timeout=300",
|
"--min-request-timeout=300",
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"--service-cluster-ip-range=10.0.0.1/24",
|
"--service-cluster-ip-range=10.0.0.1/24",
|
||||||
"--insecure-bind-address=127.0.0.1",
|
"--insecure-bind-address=127.0.0.1",
|
||||||
"--etcd-servers=http://127.0.0.1:2379",
|
"--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",
|
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||||
"--min-request-timeout=300",
|
"--min-request-timeout=300",
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"--etcd-certfile={{ etcd_cert }}",
|
"--etcd-certfile={{ etcd_cert }}",
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
"--etcd-servers={{ connection_string }}",
|
"--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",
|
"--client-ca-file=/srv/kubernetes/ca.crt",
|
||||||
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
"--basic-auth-file=/srv/kubernetes/basic_auth.csv",
|
||||||
"--min-request-timeout=300",
|
"--min-request-timeout=300",
|
||||||
|
|
|
@ -25,7 +25,7 @@ source "$KUBE_ROOT/cluster/common.sh"
|
||||||
|
|
||||||
export LIBVIRT_DEFAULT_URI=qemu:///system
|
export LIBVIRT_DEFAULT_URI=qemu:///system
|
||||||
export SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
|
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=kubernetes
|
||||||
readonly POOL_PATH=/var/lib/libvirt/images/kubernetes
|
readonly POOL_PATH=/var/lib/libvirt/images/kubernetes
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ apiserver:
|
||||||
--external-hostname=apiserver
|
--external-hostname=apiserver
|
||||||
--etcd-servers=http://etcd:4001
|
--etcd-servers=http://etcd:4001
|
||||||
--port=8888
|
--port=8888
|
||||||
--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||||
--authorization-mode=AlwaysAllow
|
--authorization-mode=AlwaysAllow
|
||||||
--token-auth-file=/var/run/kubernetes/auth/token-users
|
--token-auth-file=/var/run/kubernetes/auth/token-users
|
||||||
--basic-auth-file=/var/run/kubernetes/auth/basic-users
|
--basic-auth-file=/var/run/kubernetes/auth/basic-users
|
||||||
|
|
|
@ -49,7 +49,7 @@ write_files:
|
||||||
dns_domain: cluster.local
|
dns_domain: cluster.local
|
||||||
federations_domain_map: ''
|
federations_domain_map: ''
|
||||||
instance_prefix: kubernetes
|
instance_prefix: kubernetes
|
||||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||||
enable_cpu_cfs_quota: "true"
|
enable_cpu_cfs_quota: "true"
|
||||||
network_provider: none
|
network_provider: none
|
||||||
opencontrail_tag: R2.20
|
opencontrail_tag: R2.20
|
||||||
|
|
|
@ -124,5 +124,5 @@ federations_domain_map: ''
|
||||||
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
||||||
cluster_cidr: "$NODE_IP_RANGES"
|
cluster_cidr: "$NODE_IP_RANGES"
|
||||||
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
||||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||||
EOF
|
EOF
|
||||||
|
|
|
@ -68,7 +68,7 @@ FLANNEL_OTHER_NET_CONFIG=''
|
||||||
|
|
||||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
# 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.
|
# 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
|
# Path to the config file or directory of files of kubelet
|
||||||
export KUBELET_CONFIG=${KUBELET_CONFIG:-""}
|
export KUBELET_CONFIG=${KUBELET_CONFIG:-""}
|
||||||
|
|
|
@ -56,7 +56,7 @@ MASTER_PASSWD="${MASTER_PASSWD:-vagrant}"
|
||||||
|
|
||||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
# 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.
|
# 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.
|
# Optional: Enable node logging.
|
||||||
ENABLE_NODE_LOGGING=false
|
ENABLE_NODE_LOGGING=false
|
||||||
|
|
|
@ -124,7 +124,7 @@ federations_domain_map: ''
|
||||||
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
e2e_storage_test_environment: "${E2E_STORAGE_TEST_ENVIRONMENT:-false}"
|
||||||
cluster_cidr: "$NODE_IP_RANGES"
|
cluster_cidr: "$NODE_IP_RANGES"
|
||||||
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
allocate_node_cidrs: "${ALLOCATE_NODE_CIDRS:-true}"
|
||||||
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
admission_control: NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DefaultStorageClass,ResourceQuota
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
mkdir -p /srv/salt-overlay/salt/nginx
|
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/security/podsecuritypolicy"
|
||||||
_ "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
_ "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||||
_ "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
_ "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 {
|
function start_apiserver {
|
||||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||||
if [[ -z "${ALLOW_SECURITY_CONTEXT}" ]]; then
|
if [[ -z "${ALLOW_SECURITY_CONTEXT}" ]]; then
|
||||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
|
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota,DefaultStorageClass
|
||||||
else
|
else
|
||||||
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota
|
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,DefaultStorageClass
|
||||||
fi
|
fi
|
||||||
# This is the default dir and filename where the apiserver will generate a self-signed cert
|
# 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
|
# 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