From 04818225559120830b9f9c27a454481b62ed53bd Mon Sep 17 00:00:00 2001 From: Dario Tranchitella Date: Tue, 25 May 2021 16:46:39 +0200 Subject: [PATCH] feat: enforcing Pod Priority Class --- api/v1alpha1/domain/podpriority.go | 51 ++++++++++++ main.go | 3 +- pkg/webhook/podpriority/errors.go | 49 ++++++++++++ pkg/webhook/podpriority/validating.go | 111 ++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 api/v1alpha1/domain/podpriority.go create mode 100644 pkg/webhook/podpriority/errors.go create mode 100644 pkg/webhook/podpriority/validating.go diff --git a/api/v1alpha1/domain/podpriority.go b/api/v1alpha1/domain/podpriority.go new file mode 100644 index 00000000..a5cd62ef --- /dev/null +++ b/api/v1alpha1/domain/podpriority.go @@ -0,0 +1,51 @@ +/* +Copyright 2020 Clastix Labs. + +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 domain + +import ( + "regexp" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/clastix/capsule/api/v1alpha1" +) + +const ( + podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed" + podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex" +) + +func NewPodPriority(object metav1.Object) (allowed *v1alpha1.AllowedListSpec) { + annotations := object.GetAnnotations() + + if v, ok := annotations[podPriorityAllowedAnnotation]; ok { + allowed = &v1alpha1.AllowedListSpec{} + allowed.Exact = strings.Split(v, ",") + } + + if v, ok := annotations[podPriorityAllowedRegexAnnotation]; ok { + if _, err := regexp.Compile(v); err == nil { + if allowed == nil { + allowed = &v1alpha1.AllowedListSpec{} + } + allowed.Regex = v + } + } + + return +} diff --git a/main.go b/main.go index 3b24da32..3d77350f 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,6 @@ import ( goRuntime "runtime" flag "github.com/spf13/pflag" - "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -45,6 +44,7 @@ import ( "github.com/clastix/capsule/pkg/webhook/namespacequota" "github.com/clastix/capsule/pkg/webhook/networkpolicies" "github.com/clastix/capsule/pkg/webhook/ownerreference" + "github.com/clastix/capsule/pkg/webhook/podpriority" "github.com/clastix/capsule/pkg/webhook/pvc" "github.com/clastix/capsule/pkg/webhook/registry" "github.com/clastix/capsule/pkg/webhook/services" @@ -176,6 +176,7 @@ func main() { ingress.Webhook(ingress.Handler(allowIngressHostnamesCollision)), pvc.Webhook(pvc.Handler()), registry.Webhook(registry.Handler()), + podpriority.Webhook(podpriority.Handler()), services.Webhook(services.Handler()), ownerreference.Webhook(utils.InCapsuleGroups(capsuleGroups, ownerreference.Handler(forceTenantPrefix))), namespacequota.Webhook(utils.InCapsuleGroups(capsuleGroups, namespacequota.Handler())), diff --git a/pkg/webhook/podpriority/errors.go b/pkg/webhook/podpriority/errors.go new file mode 100644 index 00000000..9b13931c --- /dev/null +++ b/pkg/webhook/podpriority/errors.go @@ -0,0 +1,49 @@ +/* +Copyright 2020 Clastix Labs. + +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 podpriority + +import ( + "fmt" + "strings" + + "github.com/clastix/capsule/api/v1alpha1" +) + +type podPriorityClassForbidden struct { + priorityClassName string + spec v1alpha1.AllowedListSpec +} + +func NewPodPriorityClassForbidden(priorityClassName string, spec v1alpha1.AllowedListSpec) error { + return &podPriorityClassForbidden{ + priorityClassName: priorityClassName, + spec: spec, + } +} + +func (f podPriorityClassForbidden) Error() (err string) { + err = fmt.Sprintf("Pod Priorioty Class %s is forbidden for the current Tenant: ", f.priorityClassName) + var extra []string + if len(f.spec.Exact) > 0 { + extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(f.spec.Exact, ", "))) + } + if len(f.spec.Regex) > 0 { + extra = append(extra, fmt.Sprintf(" use one matching the following regex (%s)", f.spec.Regex)) + } + err += strings.Join(extra, " or ") + return +} diff --git a/pkg/webhook/podpriority/validating.go b/pkg/webhook/podpriority/validating.go new file mode 100644 index 00000000..e3febf3a --- /dev/null +++ b/pkg/webhook/podpriority/validating.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 Clastix Labs. + +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 podpriority + +import ( + "context" + "net/http" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" + "github.com/clastix/capsule/api/v1alpha1/domain" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/validating-v1-podpriority,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=podpriority.capsule.clastix.io + +type webhook struct { + handler capsulewebhook.Handler +} + +func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { + return &webhook{handler: handler} +} + +func (w *webhook) GetName() string { + return "podpriority" +} + +func (w *webhook) GetPath() string { + return "/validating-v1-podpriority" +} + +func (w *webhook) GetHandler() capsulewebhook.Handler { + return w.handler +} + +type handler struct { +} + +func Handler() capsulewebhook.Handler { + return &handler{} +} + +func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + var pod = &v1.Pod{} + + if err := decoder.Decode(req, pod); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + var tntList = &capsulev1alpha1.TenantList{} + + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), + }); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if len(tntList.Items) == 0 { + return admission.Allowed("") + } + + var allowed = domain.NewPodPriority(&tntList.Items[0]) + + var priorityClassName = pod.Spec.PriorityClassName + + switch { + case allowed == nil: + // Enforcement is not in place, skipping it at all + return admission.Allowed("") + case len(priorityClassName) == 0: + // We don't have to force Pod to specify a Priority Class + return admission.Allowed("") + case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName): + return admission.Errored(http.StatusBadRequest, NewPodPriorityClassForbidden(priorityClassName, *allowed)) + default: + return admission.Allowed("") + } + } +} + +func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + return admission.Allowed("") + } +} + +func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + return admission.Allowed("") + } +}