Files
kubesphere/pkg/controller/quota/resourcequota_webhook.go
hongming cfebd96a1f update dependencies (#6267)
Signed-off-by: hongming <coder.scala@gmail.com>
2024-11-06 10:27:06 +08:00

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
}