192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
/*
|
|
* Please refer to the LICENSE file in the root directory of the project.
|
|
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
|
*/
|
|
|
|
package quota
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
|
|
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
utilwait "k8s.io/apimachinery/pkg/util/wait"
|
|
admissionapi "k8s.io/apiserver/pkg/admission"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/klog/v2"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
"kubesphere.io/kubesphere/kube/pkg/quota/v1"
|
|
"kubesphere.io/kubesphere/kube/pkg/quota/v1/generic"
|
|
"kubesphere.io/kubesphere/kube/pkg/quota/v1/install"
|
|
"kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota"
|
|
resourcequotaapi "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
|
"kubesphere.io/kubesphere/pkg/scheme"
|
|
)
|
|
|
|
const (
|
|
numEvaluatorThreads = 10
|
|
)
|
|
|
|
type ResourceQuotaAdmission struct {
|
|
client client.Client
|
|
|
|
decoder webhook.AdmissionDecoder
|
|
|
|
lockFactory LockFactory
|
|
|
|
// these are used to create the evaluator
|
|
registry quota.Registry
|
|
|
|
init sync.Once
|
|
evaluator resourcequota.Evaluator
|
|
}
|
|
|
|
const webhookName = "resource-quota-webhook"
|
|
|
|
func (w *Webhook) Name() string {
|
|
return webhookName
|
|
}
|
|
|
|
var _ kscontroller.Controller = &Webhook{}
|
|
|
|
type Webhook struct {
|
|
}
|
|
|
|
func (w *Webhook) SetupWithManager(mgr *kscontroller.Manager) error {
|
|
resourceQuotaAdmission := &ResourceQuotaAdmission{
|
|
client: mgr.GetClient(),
|
|
lockFactory: NewDefaultLockFactory(),
|
|
decoder: admission.NewDecoder(mgr.GetScheme()),
|
|
registry: generic.NewRegistry(install.NewQuotaConfigurationForAdmission().Evaluators()),
|
|
}
|
|
mgr.GetWebhookServer().Register("/validate-quota-kubesphere-io-v1alpha2", &webhook.Admission{Handler: resourceQuotaAdmission})
|
|
return nil
|
|
}
|
|
|
|
func (r *ResourceQuotaAdmission) Handle(ctx context.Context, req webhook.AdmissionRequest) webhook.AdmissionResponse {
|
|
// ignore all operations that correspond to sub-resource actions
|
|
if len(req.RequestSubResource) != 0 {
|
|
return webhook.Allowed("")
|
|
}
|
|
// ignore cluster level resources
|
|
if len(req.Namespace) == 0 {
|
|
return webhook.Allowed("")
|
|
}
|
|
|
|
r.init.Do(func() {
|
|
resourceQuotaAccessor := newQuotaAccessor(r.client)
|
|
r.evaluator = resourcequota.NewQuotaEvaluator(resourceQuotaAccessor, install.DefaultIgnoredResources(), r.registry, r.lockAquisition, &resourcequotaapi.Configuration{}, numEvaluatorThreads, utilwait.NeverStop)
|
|
})
|
|
|
|
attributesRecord, err := convertToAdmissionAttributes(req)
|
|
if err != nil {
|
|
klog.Error(err)
|
|
return webhook.Errored(http.StatusBadRequest, err)
|
|
}
|
|
|
|
if err := r.evaluator.Evaluate(attributesRecord); err != nil {
|
|
if errors.IsForbidden(err) {
|
|
klog.Info(err)
|
|
return webhook.Denied(err.Error())
|
|
}
|
|
klog.Error(err)
|
|
return webhook.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
return webhook.Allowed("")
|
|
}
|
|
|
|
type ByName []corev1.ResourceQuota
|
|
|
|
func (v ByName) Len() int { return len(v) }
|
|
func (v ByName) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
|
|
func (v ByName) Less(i, j int) bool { return v[i].Name < v[j].Name }
|
|
|
|
func (r *ResourceQuotaAdmission) lockAquisition(quotas []corev1.ResourceQuota) func() {
|
|
var locks []sync.Locker
|
|
|
|
// acquire the locks in alphabetical order because I'm too lazy to think of something clever
|
|
sort.Sort(ByName(quotas))
|
|
for _, q := range quotas {
|
|
lock := r.lockFactory.GetLock(string(q.UID))
|
|
lock.Lock()
|
|
locks = append(locks, lock)
|
|
}
|
|
|
|
return func() {
|
|
for i := len(locks) - 1; i >= 0; i-- {
|
|
locks[i].Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func convertToAdmissionAttributes(req admission.Request) (admissionapi.Attributes, error) {
|
|
var err error
|
|
var object runtime.Object
|
|
if len(req.Object.Raw) > 0 {
|
|
object, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Object.Raw, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var oldObject runtime.Object
|
|
if len(req.OldObject.Raw) > 0 {
|
|
oldObject, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.OldObject.Raw, nil, nil)
|
|
if err != nil {
|
|
klog.Error(err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var operationOptions runtime.Object
|
|
if len(req.Options.Raw) > 0 {
|
|
operationOptions, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Options.Raw, nil, nil)
|
|
if err != nil {
|
|
klog.Error(err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
extras := map[string][]string{}
|
|
for k, v := range req.UserInfo.Extra {
|
|
extras[k] = v
|
|
}
|
|
|
|
attributesRecord := admissionapi.NewAttributesRecord(object,
|
|
oldObject,
|
|
schema.GroupVersionKind{
|
|
Group: req.RequestKind.Group,
|
|
Version: req.RequestKind.Version,
|
|
Kind: req.RequestKind.Kind,
|
|
},
|
|
req.Namespace,
|
|
req.Name,
|
|
schema.GroupVersionResource{
|
|
Group: req.RequestResource.Group,
|
|
Version: req.RequestResource.Version,
|
|
Resource: req.RequestResource.Resource,
|
|
},
|
|
req.SubResource,
|
|
admissionapi.Operation(req.Operation),
|
|
operationOptions,
|
|
*req.DryRun,
|
|
&user.DefaultInfo{
|
|
Name: req.UserInfo.Username,
|
|
UID: req.UserInfo.UID,
|
|
Groups: req.UserInfo.Groups,
|
|
Extra: extras,
|
|
})
|
|
return attributesRecord, nil
|
|
}
|