mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
refactor!: using multiple handers per route
This commit is contained in:
@@ -28,17 +28,15 @@ import (
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
"github.com/clastix/capsule/pkg/indexer"
|
||||
"github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/imagepullpolicy"
|
||||
"github.com/clastix/capsule/pkg/webhook/ingress"
|
||||
namespacewebhook "github.com/clastix/capsule/pkg/webhook/namespace"
|
||||
"github.com/clastix/capsule/pkg/webhook/networkpolicies"
|
||||
"github.com/clastix/capsule/pkg/webhook/networkpolicy"
|
||||
"github.com/clastix/capsule/pkg/webhook/ownerreference"
|
||||
"github.com/clastix/capsule/pkg/webhook/podpriority"
|
||||
"github.com/clastix/capsule/pkg/webhook/pod"
|
||||
"github.com/clastix/capsule/pkg/webhook/pvc"
|
||||
"github.com/clastix/capsule/pkg/webhook/registry"
|
||||
"github.com/clastix/capsule/pkg/webhook/services"
|
||||
"github.com/clastix/capsule/pkg/webhook/route"
|
||||
"github.com/clastix/capsule/pkg/webhook/service"
|
||||
"github.com/clastix/capsule/pkg/webhook/tenant"
|
||||
"github.com/clastix/capsule/pkg/webhook/tenantprefix"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
@@ -142,19 +140,15 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
webhooksList := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
ingress.Webhook(ingress.Handler(cfg)),
|
||||
pvc.Webhook(pvc.Handler()),
|
||||
registry.Webhook(registry.Handler()),
|
||||
podpriority.Webhook(podpriority.Handler()),
|
||||
services.Webhook(services.Handler()),
|
||||
ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
|
||||
namespacewebhook.QuotaWebhook(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler())),
|
||||
namespacewebhook.FreezedWebhook(utils.InCapsuleGroups(cfg, namespacewebhook.FreezeHandler(cfg))),
|
||||
networkpolicies.Webhook(utils.InCapsuleGroups(cfg, networkpolicies.Handler())),
|
||||
tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg))),
|
||||
tenant.Validating(tenant.ValidatingHandler(cfg)),
|
||||
imagepullpolicy.Webhook(imagepullpolicy.Handler()),
|
||||
tenant.Cordoning(tenant.CordoningHandler(cfg)),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg))),
|
||||
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg)),
|
||||
route.PVC(pvc.Handler()),
|
||||
route.Service(service.Handler()),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Tenant(tenant.NameHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.HostnamesCollisionHandler(cfg), tenant.FreezedEmitter()),
|
||||
route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg)),
|
||||
)
|
||||
if err = webhook.Register(manager, webhooksList...); err != nil {
|
||||
setupLog.Error(err, "unable to setup webhooks")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
type Func func(ctx context.Context, req admission.Request) admission.Response
|
||||
type Func func(ctx context.Context, req admission.Request) *admission.Response
|
||||
|
||||
type Handler interface {
|
||||
OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package imagepullpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/api/v1alpha1/domain"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-imagepullpolicy,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=pods,verbs=create,versions=v1,name=validating-image-pull-policy.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handler: handler}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "ImagePullPolicy"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-imagepullpolicy"
|
||||
}
|
||||
|
||||
type handler struct{}
|
||||
|
||||
func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
var pod = &corev1.Pod{}
|
||||
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
var tntList = &v1alpha1.TenantList{}
|
||||
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
// the Pod is not running in a Namespace managed by a Tenant
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := domain.NewImagePullPolicy(&tnt)
|
||||
// if Tenant doesn't enforce the pull policy, exit
|
||||
if policy == nil {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
usedPullPolicy := string(container.ImagePullPolicy)
|
||||
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
return admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ package ingress
|
||||
import (
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta "k8s.io/api/networking/v1beta1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,7 +54,7 @@ func (n NetworkingV1) Hostnames() []string {
|
||||
}
|
||||
|
||||
type NetworkingV1Beta1 struct {
|
||||
*networkingv1beta.Ingress
|
||||
*networkingv1beta1.Ingress
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) Name() string {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
"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"
|
||||
)
|
||||
|
||||
func tenantFromIngress(ctx context.Context, c client.Client, ingress Ingress) (*capsulev1alpha1.Tenant, error) {
|
||||
tenantList := &capsulev1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tenantList.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &tenantList.Items[0], nil
|
||||
}
|
||||
|
||||
func ingressFromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) {
|
||||
switch req.Kind.Group {
|
||||
case "networking.k8s.io":
|
||||
if req.Kind.Version == "v1" {
|
||||
ingressObj := &networkingv1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = NetworkingV1{Ingress: ingressObj}
|
||||
break
|
||||
}
|
||||
ingressObj := &networkingv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = NetworkingV1Beta1{Ingress: ingressObj}
|
||||
case "extensions":
|
||||
ingressObj := &extensionsv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = Extension{Ingress: ingressObj}
|
||||
default:
|
||||
err = fmt.Errorf("cannot recognize type %s", req.Kind.Group)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type class struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Class(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &class{configuration: configuration}
|
||||
}
|
||||
|
||||
func (r *class) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateClass(*tenant, ingress.IngressClass()); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var forbiddenErr *ingressClassForbidden
|
||||
|
||||
if errors.As(err, &forbiddenErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
var invalidErr *ingressClassNotValid
|
||||
|
||||
if errors.As(err, &invalidErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassNotValid", "Ingress %s/%s class is invalid", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = r.validateClass(*tenant, ingress.IngressClass())
|
||||
|
||||
var forbiddenErr *ingressClassForbidden
|
||||
|
||||
if errors.As(err, &forbiddenErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
var invalidErr *ingressClassNotValid
|
||||
|
||||
if errors.As(err, &invalidErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassNotValid", "Ingress %s/%s class is invalid", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) validateClass(tenant v1alpha1.Tenant, ingressClass *string) error {
|
||||
if tenant.Spec.IngressClasses == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ingressClass == nil {
|
||||
return NewIngressClassNotValid(*tenant.Spec.IngressClasses)
|
||||
}
|
||||
|
||||
var valid, matched bool
|
||||
|
||||
if len(tenant.Spec.IngressClasses.Exact) > 0 {
|
||||
valid = tenant.Spec.IngressClasses.ExactMatch(*ingressClass)
|
||||
}
|
||||
matched = tenant.Spec.IngressClasses.RegexMatch(*ingressClass)
|
||||
|
||||
if !valid && !matched {
|
||||
return NewIngressClassForbidden(*ingressClass, *tenant.Spec.IngressClasses)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type collision struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Collision(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &collision{configuration: configuration}
|
||||
}
|
||||
|
||||
func (r *collision) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateCollision(ctx, client, ingress); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var collisionErr *ingressHostnameCollision
|
||||
|
||||
if errors.As(err, &collisionErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func (r *collision) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = r.validateCollision(ctx, client, ingress)
|
||||
|
||||
var collisionErr *ingressHostnameCollision
|
||||
|
||||
if errors.As(err, &collisionErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func (r *collision) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *collision) validateCollision(ctx context.Context, clt client.Client, ingress Ingress) error {
|
||||
if r.configuration.AllowIngressHostnameCollision() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, hostname := range ingress.Hostnames() {
|
||||
switch ingress.(type) {
|
||||
case Extension:
|
||||
ingressObjList := &extensionsv1beta1.IngressList{}
|
||||
err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return NewIngressHostnameCollision(hostname)
|
||||
}
|
||||
case NetworkingV1:
|
||||
ingressObjList := &networkingv1.IngressList{}
|
||||
err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return NewIngressHostnameCollision(hostname)
|
||||
}
|
||||
case NetworkingV1Beta1:
|
||||
ingressObjList := &networkingv1beta1.IngressList{}
|
||||
err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1beta1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
|
||||
fallthrough
|
||||
default:
|
||||
return NewIngressHostnameCollision(hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type hostnames struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Hostnames(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &hostnames{configuration: configuration}
|
||||
}
|
||||
|
||||
func (r *hostnames) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, c, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateHostnames(*tenant, ingress.Hostnames()); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hostnameNotValidErr *ingressHostnameNotValid
|
||||
|
||||
if errors.As(err, &hostnameNotValidErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameNotValid", "Ingress %s/%s hostname is not valid", ingress.Namespace(), ingress.Name())
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hostnames) OnUpdate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *v1alpha1.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, c, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = r.validateHostnames(*tenant, ingress.Hostnames())
|
||||
|
||||
var hostnameNotValidErr *ingressHostnameNotValid
|
||||
|
||||
if errors.As(err, &hostnameNotValidErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameNotValid", "Ingress %s/%s hostname is not valid", ingress.Namespace(), ingress.Name())
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hostnames) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hostnames) validateHostnames(tenant v1alpha1.Tenant, hostnames []string) error {
|
||||
if tenant.Spec.IngressHostnames == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var valid, matched bool
|
||||
|
||||
var invalidHostnames []string
|
||||
if len(hostnames) > 0 {
|
||||
for _, currentHostname := range hostnames {
|
||||
isPresent := v1alpha1.IngressHostnamesList(tenant.Spec.IngressHostnames.Exact).IsStringInList(currentHostname)
|
||||
if !isPresent {
|
||||
invalidHostnames = append(invalidHostnames, currentHostname)
|
||||
}
|
||||
}
|
||||
if len(invalidHostnames) == 0 {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
var notMatchingHostnames []string
|
||||
allowedRegex := tenant.Spec.IngressHostnames.Regex
|
||||
if len(allowedRegex) > 0 {
|
||||
for _, currentHostname := range hostnames {
|
||||
matched, _ = regexp.MatchString(tenant.Spec.IngressHostnames.Regex, currentHostname)
|
||||
if !matched {
|
||||
notMatchingHostnames = append(notMatchingHostnames, currentHostname)
|
||||
}
|
||||
}
|
||||
if len(notMatchingHostnames) == 0 {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid && !matched {
|
||||
return NewIngressHostnamesNotValid(invalidHostnames, notMatchingHostnames, *tenant.Spec.IngressHostnames)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-ingress,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io;extensions,resources=ingresses,verbs=create;update,versions=v1beta1,name=ingress-v1beta1.capsule.clastix.io
|
||||
// +kubebuilder:webhook:path=/validating-ingress,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io,resources=ingresses,verbs=create;update,versions=v1,name=ingress-v1.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handler: handler}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "NetworkIngress"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-ingress"
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Handler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &handler{configuration: configuration}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
ingress, err := r.ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return r.validateIngress(ctx, client, ingress, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
ingress, err := r.ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return r.validateIngress(ctx, client, ingress, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) ingressFromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) {
|
||||
switch req.Kind.Group {
|
||||
case "networking.k8s.io":
|
||||
if req.Kind.Version == "v1" {
|
||||
ingressObj := &networkingv1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = NetworkingV1{Ingress: ingressObj}
|
||||
break
|
||||
}
|
||||
ingressObj := &networkingv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = NetworkingV1Beta1{Ingress: ingressObj}
|
||||
case "extensions":
|
||||
ingressObj := &extensionsv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
}
|
||||
ingress = Extension{Ingress: ingressObj}
|
||||
default:
|
||||
err = fmt.Errorf("cannot recognize type %s", req.Kind.Group)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *handler) validateClass(tenant v1alpha1.Tenant, ingressClass *string) error {
|
||||
if tenant.Spec.IngressClasses == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ingressClass == nil {
|
||||
return NewIngressClassNotValid(*tenant.Spec.IngressClasses)
|
||||
}
|
||||
|
||||
var valid, matched bool
|
||||
|
||||
if len(tenant.Spec.IngressClasses.Exact) > 0 {
|
||||
valid = tenant.Spec.IngressClasses.ExactMatch(*ingressClass)
|
||||
}
|
||||
matched = tenant.Spec.IngressClasses.RegexMatch(*ingressClass)
|
||||
|
||||
if !valid && !matched {
|
||||
return NewIngressClassForbidden(*ingressClass, *tenant.Spec.IngressClasses)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *handler) validateHostnames(tenant v1alpha1.Tenant, hostnames []string) error {
|
||||
if tenant.Spec.IngressHostnames == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var valid, matched bool
|
||||
|
||||
var invalidHostnames []string
|
||||
if len(hostnames) > 0 {
|
||||
for _, currentHostname := range hostnames {
|
||||
isPresent := v1alpha1.IngressHostnamesList(tenant.Spec.IngressHostnames.Exact).IsStringInList(currentHostname)
|
||||
if !isPresent {
|
||||
invalidHostnames = append(invalidHostnames, currentHostname)
|
||||
}
|
||||
}
|
||||
if len(invalidHostnames) == 0 {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
var notMatchingHostnames []string
|
||||
allowedRegex := tenant.Spec.IngressHostnames.Regex
|
||||
if len(allowedRegex) > 0 {
|
||||
for _, currentHostname := range hostnames {
|
||||
matched, _ = regexp.MatchString(tenant.Spec.IngressHostnames.Regex, currentHostname)
|
||||
if !matched {
|
||||
notMatchingHostnames = append(notMatchingHostnames, currentHostname)
|
||||
}
|
||||
}
|
||||
if len(notMatchingHostnames) == 0 {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid && !matched {
|
||||
return NewIngressHostnamesNotValid(invalidHostnames, notMatchingHostnames, *tenant.Spec.IngressHostnames)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress, recorder record.EventRecorder) admission.Response {
|
||||
tenantList := &v1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if len(tenantList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
tenant := tenantList.Items[0]
|
||||
|
||||
if err := r.validateClass(tenant, ingress.IngressClass()); err != nil {
|
||||
if ic := ingress.IngressClass(); ic != nil {
|
||||
recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbidenIngressClass", "Ingress %s/%s class %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), *ic)
|
||||
} else {
|
||||
recorder.Eventf(&tenant, corev1.EventTypeWarning, "MissingIngressClass", "Ingress %s/%s is missing IngressClass", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if err := r.validateHostnames(tenant, ingress.Hostnames()); err != nil {
|
||||
recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbiddenHostname", "Ingress %s/%s hostnames %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ","))
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if err := r.validateCollision(ctx, c, ingress); err != nil {
|
||||
recorder.Eventf(&tenant, corev1.EventTypeWarning, "HostnameCollision", "Ingress %s/%s hostnames collision for %s ", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ","))
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
func (r *handler) validateCollision(ctx context.Context, clt client.Client, ingress Ingress) error {
|
||||
if r.configuration.AllowIngressHostnameCollision() {
|
||||
return nil
|
||||
}
|
||||
for _, hostname := range ingress.Hostnames() {
|
||||
collisionErr := NewIngressHostnameCollision(hostname)
|
||||
|
||||
var err error
|
||||
switch ingress.(type) {
|
||||
case Extension:
|
||||
ingressObjList := &extensionsv1beta1.IngressList{}
|
||||
if err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
case NetworkingV1:
|
||||
ingressObjList := &networkingv1.IngressList{}
|
||||
err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
case NetworkingV1Beta1:
|
||||
ingressObjList := &networkingv1beta1.IngressList{}
|
||||
err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1beta1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
switch len(ingressObjList.Items) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
@@ -20,30 +19,6 @@ import (
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-v1-namespace-freezed,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create;update;delete,versions=v1,name=freezed.namespace.capsule.clastix.io
|
||||
|
||||
type freezedWebhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func FreezedWebhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &freezedWebhook{
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *freezedWebhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *freezedWebhook) GetName() string {
|
||||
return "NamespaceFreezed"
|
||||
}
|
||||
|
||||
func (w *freezedWebhook) GetPath() string {
|
||||
return "/validate-v1-namespace-freezed"
|
||||
}
|
||||
|
||||
type freezedHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
@@ -53,81 +28,87 @@ func FreezeHandler(configuration configuration.Configuration) capsulewebhook.Han
|
||||
}
|
||||
|
||||
func (r *freezedHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
for _, objectRef := range ns.ObjectMeta.OwnerReferences {
|
||||
// retrieving the selected Tenant
|
||||
tnt := &capsulev1alpha1.Tenant{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt.IsCordoned() {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be attached, the current Tenant is freezed", ns.GetName())
|
||||
|
||||
return admission.Denied("the selected Tenant is freezed")
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
// creating NS that is not bounded to any Tenant
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *freezedHandler) OnDelete(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
func (r *freezedHandler) OnDelete(c client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tntList := &capsulev1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", req.Name),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespaced %s cannot be deleted, the current Tenant is freezed", req.Name)
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name)
|
||||
|
||||
return admission.Denied("the selected Tenant is freezed")
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *freezedHandler) OnUpdate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", ns.Name),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespaced %s cannot be updated, the current Tenant is freezed", ns.GetName())
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName())
|
||||
|
||||
return admission.Denied("the selected Tenant is freezed")
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type prefixHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func PrefixHandler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &prefixHandler{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *prefixHandler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if exp, _ := r.configuration.ProtectedNamespaceRegexp(); exp != nil {
|
||||
if matched := exp.MatchString(ns.GetName()); matched {
|
||||
response := admission.Denied(fmt.Sprintf("Creating namespaces with name matching %s regexp is not allowed; please, reach out to the system administrators", exp.String()))
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
if r.configuration.ForceTenantPrefix() {
|
||||
tnt := &v1alpha1.Tenant{}
|
||||
|
||||
for _, or := range ns.ObjectMeta.OwnerReferences {
|
||||
// retrieving the selected Tenant
|
||||
if err := clt.Get(ctx, types.NamespacedName{Name: or.Name}, tnt); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName())
|
||||
|
||||
response := admission.Denied(fmt.Sprintf("The namespace doesn't match the tenant prefix, expected %s", e))
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *prefixHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *prefixHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -15,32 +14,9 @@ import (
|
||||
|
||||
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-v1-namespace-quota,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=quota.namespace.capsule.clastix.io
|
||||
|
||||
type quotaWebhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func QuotaWebhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return "aWebhook{
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *quotaWebhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *quotaWebhook) GetName() string {
|
||||
return "NamespaceQuota"
|
||||
}
|
||||
|
||||
func (w *quotaWebhook) GetPath() string {
|
||||
return "/validate-v1-namespace-quota"
|
||||
}
|
||||
|
||||
type quotaHandler struct {
|
||||
}
|
||||
|
||||
@@ -49,43 +25,40 @@ func QuotaHandler() capsulewebhook.Handler {
|
||||
}
|
||||
|
||||
func (r *quotaHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
for _, objectRef := range ns.ObjectMeta.OwnerReferences {
|
||||
// retrieving the selected Tenant
|
||||
tnt := &capsulev1alpha1.Tenant{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case tnt.IsFull():
|
||||
if tnt.IsFull() {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "NamespaceQuotaExceded", "Namespace %s cannot be attached, quota exceeded for the current Tenant", ns.GetName())
|
||||
|
||||
return admission.Denied(NewNamespaceQuotaExceededError().Error())
|
||||
case tnt.IsCordoned():
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be attached, the current Tenant is freezed", ns.GetName())
|
||||
response := admission.Denied(NewNamespaceQuotaExceededError().Error())
|
||||
|
||||
return admission.Denied("the selected Tenant is freezed")
|
||||
return &response
|
||||
}
|
||||
}
|
||||
// creating NS that is not bounded to any Tenant
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *quotaHandler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
func (r *quotaHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *quotaHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
func (r *quotaHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+20
-37
@@ -1,11 +1,10 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package networkpolicies
|
||||
package networkpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
@@ -16,30 +15,9 @@ import (
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-v1-network-policy,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io,resources=networkpolicies,verbs=create;update;delete,versions=v1,name=validating.network-policy.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handler: handler}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "NetworkPolicy"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-v1-network-policy"
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
}
|
||||
|
||||
@@ -47,9 +25,9 @@ func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
func (r *handler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,36 +53,41 @@ func (r *handler) generic(ctx context.Context, req admission.Request, client cli
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
//nolint:dupl
|
||||
func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := r.generic(ctx, req, client, decoder)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
if tnt != nil {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyDeletion", "NetworkPolicy %s/%s cannot be deleted", req.Namespace, req.Name)
|
||||
|
||||
return admission.Denied("Capsule Network Policies cannot be deleted: please, reach out to the system administrators")
|
||||
response := admission.Denied("Capsule Network Policies cannot be deleted: please, reach out to the system administrators")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
//nolint:dupl
|
||||
func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := r.generic(ctx, req, client, decoder)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt != nil {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyUpdate", "NetworkPolicy %s/%s cannot be updated", req.Namespace, req.Name)
|
||||
|
||||
return admission.Denied("Capsule Network Policies cannot be updated: please, reach out to the system administrators")
|
||||
response := admission.Denied("Capsule Network Policies cannot be updated: please, reach out to the system administrators")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -25,28 +25,6 @@ import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-v1-namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handler: handler}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "OwnerReference"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/mutate-v1-namespace-owner-reference"
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
cfg configuration.Configuration
|
||||
}
|
||||
@@ -58,30 +36,40 @@ func Handler(cfg configuration.Configuration) capsulewebhook.Handler {
|
||||
}
|
||||
|
||||
func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
// If we already had TenantName label on NS -> assign to it
|
||||
if label, ok := ns.ObjectMeta.Labels[ln]; ok {
|
||||
// retrieving the selected Tenant
|
||||
tnt := &capsulev1alpha1.Tenant{}
|
||||
if err := clt.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
if err = clt.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
// Tenant owner must adhere to user that asked for NS creation
|
||||
if !h.isTenantOwner(tnt.Spec.Owner, req.UserInfo) {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
|
||||
|
||||
return admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
|
||||
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
|
||||
|
||||
return &response
|
||||
}
|
||||
// Patching the response
|
||||
return h.patchResponseForOwnerRef(tnt, ns, recorder)
|
||||
response := h.patchResponseForOwnerRef(tnt, ns, recorder)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// If we forceTenantPrefix -> find Tenant from NS name
|
||||
@@ -89,10 +77,14 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record
|
||||
|
||||
// Find tenants belonging to user
|
||||
{
|
||||
tntList, err := h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
var tntList *capsulev1alpha1.TenantList
|
||||
|
||||
if tntList, err = h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt); err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
for _, tnt := range tntList.Items {
|
||||
tenants = append(tenants, tnt)
|
||||
}
|
||||
@@ -102,8 +94,11 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record
|
||||
for _, group := range req.UserInfo.Groups {
|
||||
tntList, err := h.listTenantsForOwnerKind(ctx, "Group", group, clt)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
for _, tnt := range tntList.Items {
|
||||
tenants = append(tenants, tnt)
|
||||
}
|
||||
@@ -113,34 +108,44 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record
|
||||
sort.Sort(sort.Reverse(tenants))
|
||||
|
||||
if len(tenants) == 0 {
|
||||
return admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
|
||||
response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if len(tenants) == 1 {
|
||||
return h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
|
||||
response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if h.cfg.ForceTenantPrefix() {
|
||||
for _, tnt := range tenants {
|
||||
if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
|
||||
return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
|
||||
response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
admission.Denied("The Namespace prefix used doesn't match any available Tenant")
|
||||
response := admission.Denied("The Namespace prefix used doesn't match any available Tenant")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
|
||||
response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Denied("Capsule user cannot update a Namespace")
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"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"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type containerRegistryHandler struct {
|
||||
}
|
||||
|
||||
func ContainerRegistry() capsulewebhook.Handler {
|
||||
return &containerRegistryHandler{}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.ContainerRegistries != nil {
|
||||
var valid, matched bool
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
registry := domain.NewRegistry(container.Image)
|
||||
|
||||
valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry())
|
||||
|
||||
matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry())
|
||||
|
||||
if !valid && !matched {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a forbidden registry %s is forbidden for the current Tenant", req.Namespace, req.Name, registry.Registry())
|
||||
|
||||
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package registry
|
||||
// nolint:dupl
|
||||
package pod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/api/v1alpha1/domain"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type imagePullPolicy struct{}
|
||||
|
||||
func ImagePullPolicy() capsulewebhook.Handler {
|
||||
return &imagePullPolicy{}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
var pod = &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tntList = &v1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
// the Pod is not running in a Namespace managed by a Tenant
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := domain.NewImagePullPolicy(&tnt)
|
||||
// if Tenant doesn't enforce the pull policy, exit
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
usedPullPolicy := string(container.ImagePullPolicy)
|
||||
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package imagepullpolicy
|
||||
package pod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type podPriorityClassForbidden struct {
|
||||
type imagePullPolicyForbidden struct {
|
||||
usedPullPolicy string
|
||||
allowedPullPolicies []string
|
||||
containerName string
|
||||
}
|
||||
|
||||
func NewImagePullPolicyForbidden(usedPullPolicy, containerName string, allowedPullPolicies []string) error {
|
||||
return &podPriorityClassForbidden{
|
||||
return &imagePullPolicyForbidden{
|
||||
usedPullPolicy: usedPullPolicy,
|
||||
containerName: containerName,
|
||||
allowedPullPolicies: allowedPullPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
func (f podPriorityClassForbidden) Error() (err string) {
|
||||
func (f imagePullPolicyForbidden) Error() (err string) {
|
||||
return fmt.Sprintf("the ImagePullPolicy %s for container %s is forbidden, use one of the followings: %s", f.usedPullPolicy, f.containerName, strings.Join(f.allowedPullPolicies, ", "))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"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"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type priorityClass struct {
|
||||
}
|
||||
|
||||
func PriorityClass() capsulewebhook.Handler {
|
||||
return &priorityClass{}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
var pod = &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tntList = &capsulev1alpha1.TenantList{}
|
||||
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 nil
|
||||
case len(priorityClassName) == 0:
|
||||
// We don't have to force Pod to specify a Priority Class
|
||||
return nil
|
||||
case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName):
|
||||
recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenPriorityClass", "Pod %s/%s is using Priority Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, priorityClassName)
|
||||
|
||||
response := admission.Denied(NewPodPriorityClassForbidden(priorityClassName, *allowed).Error())
|
||||
|
||||
return &response
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package podpriority
|
||||
// nolint:dupl
|
||||
package pod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package podpriority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"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, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
var pod = &corev1.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):
|
||||
recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenPriorityClass", "Pod %s/%s is using Priority Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, priorityClassName)
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewPodPriorityClassForbidden(priorityClassName, *allowed))
|
||||
default:
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package pvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
@@ -15,30 +14,9 @@ import (
|
||||
|
||||
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-v1-pvc,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=pvc.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 "Pvc"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-v1-pvc"
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
}
|
||||
|
||||
@@ -47,35 +25,37 @@ func Handler() capsulewebhook.Handler {
|
||||
}
|
||||
|
||||
func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
var valid, matched bool
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
if err := decoder.Decode(req, pvc); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1alpha1.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pvc.Namespace),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.StorageClasses == nil {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
if pvc.Spec.StorageClassName == nil {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingStorageClass", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name)
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses))
|
||||
response := admission.Denied(NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
sc := *pvc.Spec.StorageClassName
|
||||
@@ -84,20 +64,23 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder
|
||||
if !valid && !matched {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenStorageClass", "PersistentVolumeClaim %s/%s StorageClass %s is forbidden for the current Tenant", req.Namespace, req.Name, sc)
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses))
|
||||
response := admission.Denied(NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
return admission.Allowed("")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"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-registry,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=pod.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 "registry"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-v1-registry"
|
||||
}
|
||||
|
||||
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, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
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("")
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.ContainerRegistries != nil {
|
||||
var valid, matched bool
|
||||
for _, container := range pod.Spec.Containers {
|
||||
registry := domain.NewRegistry(container.Image)
|
||||
valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry())
|
||||
matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry())
|
||||
if !valid && !matched {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a forbidden registry %s is forbidden for the current Tenant", req.Namespace, req.Name, registry.Registry())
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/cordoning,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="*",resources="*",verbs=create;update;delete,versions="*",name=cordoning.tenant.capsule.clastix.io
|
||||
|
||||
type cordoning struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Cordoning(handlers ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &cordoning{handlers: handlers}
|
||||
}
|
||||
|
||||
func (w cordoning) GetPath() string {
|
||||
return "/cordoning"
|
||||
}
|
||||
|
||||
func (w cordoning) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/ingresses,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io;extensions,resources=ingresses,verbs=create;update,versions=v1beta1;v1,name=ingress.capsule.clastix.io
|
||||
|
||||
type ingress struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Ingress(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &ingress{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *ingress) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *ingress) GetPath() string {
|
||||
return "/ingresses"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/namespaces,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create;update;delete,versions=v1,name=namespaces.capsule.clastix.io
|
||||
|
||||
type namespace struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Namespace(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &namespace{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *namespace) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *namespace) GetPath() string {
|
||||
return "/namespaces"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/networkpolicies,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="networking.k8s.io",resources=networkpolicies,verbs=update;delete,versions=v1,name=networkpolicies.capsule.clastix.io
|
||||
|
||||
type networkPolicy struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func NetworkPolicy(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &networkPolicy{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *networkPolicy) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *networkPolicy) GetPath() string {
|
||||
return "/networkpolicies"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func OwnerReference(handlers ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handlers: handlers}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/namespace-owner-reference"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/pods,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=pods,verbs=create,versions=v1,name=pods.capsule.clastix.io
|
||||
|
||||
type pod struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Pod(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &pod{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *pod) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *pod) GetPath() string {
|
||||
return "/pods"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/persistentvolumeclaims,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=pvc.capsule.clastix.io
|
||||
|
||||
type pvc struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func PVC(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &pvc{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *pvc) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
func (w *pvc) GetPath() string {
|
||||
return "/persistentvolumeclaims"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/services,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=services,verbs=create;update,versions=v1,name=services.capsule.clastix.io
|
||||
|
||||
type service struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Service(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &service{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *service) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *service) GetPath() string {
|
||||
return "/services"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/tenants,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create;update;delete,versions=v1alpha1,name=tenants.capsule.clastix.io
|
||||
|
||||
type tenant struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Tenant(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &tenant{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *tenant) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *tenant) GetPath() string {
|
||||
return "/tenants"
|
||||
}
|
||||
+23
-7
@@ -25,11 +25,12 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error {
|
||||
recorder := manager.GetEventRecorderFor("tenant-webhook")
|
||||
|
||||
server := manager.GetWebhookServer()
|
||||
|
||||
for _, wh := range webhookList {
|
||||
server.Register(wh.GetPath(), &webhook.Admission{
|
||||
Handler: &handlerRouter{
|
||||
recorder: recorder,
|
||||
handler: wh.GetHandler(),
|
||||
handlers: wh.GetHandlers(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -37,31 +38,46 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error {
|
||||
}
|
||||
|
||||
type handlerRouter struct {
|
||||
handler Handler
|
||||
client client.Client
|
||||
decoder *admission.Decoder
|
||||
recorder record.EventRecorder
|
||||
|
||||
handlers []Handler
|
||||
}
|
||||
|
||||
func (r *handlerRouter) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
switch req.Operation {
|
||||
case admissionv1.Create:
|
||||
return r.handler.OnCreate(r.client, r.decoder, r.recorder)(ctx, req)
|
||||
for _, h := range r.handlers {
|
||||
if response := h.OnCreate(r.client, r.decoder, r.recorder)(ctx, req); response != nil {
|
||||
return *response
|
||||
}
|
||||
}
|
||||
case admissionv1.Update:
|
||||
return r.handler.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req)
|
||||
for _, h := range r.handlers {
|
||||
if response := h.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req); response != nil {
|
||||
return *response
|
||||
}
|
||||
}
|
||||
case admissionv1.Delete:
|
||||
return r.handler.OnDelete(r.client, r.decoder, r.recorder)(ctx, req)
|
||||
default:
|
||||
return admission.Allowed("")
|
||||
for _, h := range r.handlers {
|
||||
if response := h.OnDelete(r.client, r.decoder, r.recorder)(ctx, req); response != nil {
|
||||
return *response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
func (r *handlerRouter) InjectClient(c client.Client) error {
|
||||
r.client = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *handlerRouter) InjectDecoder(d *admission.Decoder) error {
|
||||
r.decoder = d
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package services
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package services
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -17,65 +16,48 @@ import (
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-external-service-ips,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=services,verbs=create;update,versions=v1,name=validating-external-service-ips.capsule.clastix.io
|
||||
|
||||
const (
|
||||
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
|
||||
)
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{handler: handler}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "Service"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-external-service-ips"
|
||||
}
|
||||
|
||||
type handler struct{}
|
||||
|
||||
func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request, recorder record.EventRecorder) admission.Response {
|
||||
func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request, recorder record.EventRecorder) *admission.Response {
|
||||
svc := &corev1.Service{}
|
||||
if err := decoder.Decode(req, svc); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &v1alpha1.TenantList{}
|
||||
if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", svc.GetNamespace()),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.GetAnnotations()[enableNodePortsAnnotation] == "false" {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenNodePort", "Service %s/%s cannot be type of NodePort for the current Tenant", req.Namespace, req.Name)
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewNodePortDisabledError())
|
||||
response := admission.Denied(NewNodePortDisabledError().Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if svc.Spec.ExternalIPs == nil || tnt.Spec.ExternalServiceIPs == nil {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
ipInCIDR := func(ip net.IP) bool {
|
||||
@@ -99,27 +81,29 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
|
||||
if !ipInCIDR(ip) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenExternalServiceIP", "Service %s/%s external IP %s is forbidden for the current Tenant", req.Namespace, req.Name, ip.String())
|
||||
|
||||
return admission.Errored(http.StatusBadRequest, NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed))
|
||||
response := admission.Denied(NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handleService(ctx, client, decoder, req, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handleService(ctx, client, decoder, req, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type containerRegistryRegexHandler struct {
|
||||
}
|
||||
|
||||
func ContainerRegistryRegexHandler() capsulewebhook.Handler {
|
||||
return &containerRegistryRegexHandler{}
|
||||
}
|
||||
|
||||
func (h *containerRegistryRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant.Spec.ContainerRegistries != nil && len(tenant.Spec.ContainerRegistries.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.ContainerRegistries.Regex); err != nil {
|
||||
response := admission.Denied("unable to compile containerRegistries allowedRegex")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *containerRegistryRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if err := h.validate(decoder, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryRegexHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if response := h.validate(decoder, req); response != nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ package tenant
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -21,28 +20,6 @@ import (
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/tenant-cordoning,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="*",resources="*",verbs=create;update;delete,versions="*",name=cordoning.tenant.capsule.clastix.io
|
||||
|
||||
type cordoning struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Cordoning(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &cordoning{handler: handler}
|
||||
}
|
||||
|
||||
func (w cordoning) GetName() string {
|
||||
return "TenantCordoning"
|
||||
}
|
||||
|
||||
func (w cordoning) GetPath() string {
|
||||
return "/tenant-cordoning"
|
||||
}
|
||||
|
||||
func (w cordoning) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
type cordoningHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
@@ -53,17 +30,17 @@ func CordoningHandler(configuration configuration.Configuration) capsulewebhook.
|
||||
}
|
||||
}
|
||||
|
||||
func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder) admission.Response {
|
||||
func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder) *admission.Response {
|
||||
tntList := &capsulev1alpha1.TenantList{}
|
||||
|
||||
if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", req.Namespace),
|
||||
}); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
// resource is not inside a Tenant namespace
|
||||
if len(tntList.Items) == 0 {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
@@ -72,27 +49,29 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
|
||||
if utils.RequestFromOwnerOrSA(tnt, req, h.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
|
||||
|
||||
return admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
|
||||
response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *cordoningHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
func (h *cordoningHandler) OnCreate(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.cordonHandler(ctx, client, req, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *cordoningHandler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
func (h *cordoningHandler) OnDelete(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.cordonHandler(ctx, client, req, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *cordoningHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
func (h *cordoningHandler) OnUpdate(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.cordonHandler(ctx, client, req, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type freezedEmitterHandler struct {
|
||||
}
|
||||
|
||||
func FreezedEmitter() capsulewebhook.Handler {
|
||||
return &freezedEmitterHandler{}
|
||||
}
|
||||
|
||||
func (h *freezedEmitterHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *freezedEmitterHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *freezedEmitterHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
oldTnt := &v1alpha1.Tenant{}
|
||||
if err := decoder.DecodeRaw(req.OldObject, oldTnt); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
newTnt := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, newTnt); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case !oldTnt.IsCordoned() && newTnt.IsCordoned():
|
||||
recorder.Eventf(newTnt, corev1.EventTypeNormal, "TenantCordoned", "Tenant has been cordoned")
|
||||
case oldTnt.IsCordoned() && !newTnt.IsCordoned():
|
||||
recorder.Eventf(newTnt, corev1.EventTypeNormal, "TenantUncordoned", "Tenant has been uncordoned")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type hostnameRegexHandler struct {
|
||||
}
|
||||
|
||||
func HostnameRegexHandler() capsulewebhook.Handler {
|
||||
return &hostnameRegexHandler{}
|
||||
}
|
||||
|
||||
func (h *hostnameRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.IngressHostnames.Regex); err != nil {
|
||||
response := admission.Denied("unable to compile allowedHostnames allowedRegex")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hostnameRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if response := h.validate(decoder, req); response != nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hostnameRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hostnameRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if err := h.validate(decoder, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type hostnamesCollisionHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func HostnamesCollisionHandler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &hostnamesCollisionHandler{configuration: configuration}
|
||||
}
|
||||
|
||||
func (h *hostnamesCollisionHandler) validateTenant(ctx context.Context, req admission.Request, clt client.Client, decoder *admission.Decoder) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if !h.configuration.AllowTenantIngressHostnamesCollision() && tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Exact) > 0 {
|
||||
for _, h := range tenant.Spec.IngressHostnames.Exact {
|
||||
tntList := &v1alpha1.TenantList{}
|
||||
if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.ingressHostnames", h),
|
||||
}); err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, fmt.Errorf("cannot retrieve Tenant list using .spec.ingressHostnames field selector: %w", err))
|
||||
|
||||
return &response
|
||||
}
|
||||
switch {
|
||||
case len(tntList.Items) == 1 && tntList.Items[0].GetName() == tenant.GetName():
|
||||
continue
|
||||
case len(tntList.Items) > 0:
|
||||
response := admission.Denied(fmt.Sprintf("the allowed hostname %s is already used by the Tenant %s, cannot proceed", h, tntList.Items[0].GetName()))
|
||||
|
||||
return &response
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hostnamesCollisionHandler) OnCreate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if response := h.validateTenant(ctx, req, client, decoder); response != nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hostnamesCollisionHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hostnamesCollisionHandler) OnUpdate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if response := h.validateTenant(ctx, req, client, decoder); response != nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type ingressClassRegexHandler struct {
|
||||
}
|
||||
|
||||
func IngressClassRegexHandler() capsulewebhook.Handler {
|
||||
return &ingressClassRegexHandler{}
|
||||
}
|
||||
|
||||
func (h *ingressClassRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant.Spec.IngressClasses != nil && len(tenant.Spec.IngressClasses.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.IngressClasses.Regex); err != nil {
|
||||
response := admission.Denied("unable to compile ingressClasses allowedRegex")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ingressClassRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if response := h.validate(decoder, req); response != nil {
|
||||
return response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ingressClassRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ingressClassRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if err := h.validate(decoder, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type nameHandler struct {
|
||||
}
|
||||
|
||||
func NameHandler() capsulewebhook.Handler {
|
||||
return &nameHandler{}
|
||||
}
|
||||
|
||||
func (h *nameHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
matched, _ := regexp.MatchString(`[a-z0-9]([-a-z0-9]*[a-z0-9])?`, tenant.GetName())
|
||||
if !matched {
|
||||
response := admission.Denied("tenant name has forbidden characters")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *nameHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *nameHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type storageClassRegexHandler struct {
|
||||
}
|
||||
|
||||
func StorageClassRegexHandler() capsulewebhook.Handler {
|
||||
return &storageClassRegexHandler{}
|
||||
}
|
||||
|
||||
func (h *storageClassRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant.Spec.StorageClasses != nil && len(tenant.Spec.StorageClasses.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.StorageClasses.Regex); err != nil {
|
||||
response := admission.Denied("unable to compile storageClasses allowedRegex")
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *storageClassRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if err := h.validate(decoder, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *storageClassRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *storageClassRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if err := h.validate(decoder, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-v1-tenant,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create;update,versions=v1alpha1,name=tenant.capsule.clastix.io
|
||||
|
||||
type validating struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Validating(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &validating{handler: handler}
|
||||
}
|
||||
|
||||
func (w validating) GetName() string {
|
||||
return "Tenant"
|
||||
}
|
||||
|
||||
func (w validating) GetPath() string {
|
||||
return "/validating-v1-tenant"
|
||||
}
|
||||
|
||||
func (w validating) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
type validatingHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func ValidatingHandler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &validatingHandler{configuration: configuration}
|
||||
}
|
||||
|
||||
// Validate Tenant name
|
||||
func (h *validatingHandler) validateTenantName(tenant *v1alpha1.Tenant) error {
|
||||
matched, _ := regexp.MatchString(`[a-z0-9]([-a-z0-9]*[a-z0-9])?`, tenant.GetName())
|
||||
if !matched {
|
||||
return fmt.Errorf("tenant name has forbidden characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate ingressClasses regexp
|
||||
func (h *validatingHandler) validateIngressClassesRegex(tenant *v1alpha1.Tenant) error {
|
||||
if tenant.Spec.IngressClasses != nil && len(tenant.Spec.IngressClasses.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.IngressClasses.Regex); err != nil {
|
||||
return fmt.Errorf("unable to compile ingressClasses allowedRegex")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate storageClasses regexp
|
||||
func (h *validatingHandler) validateStorageClassesRegex(tenant *v1alpha1.Tenant) error {
|
||||
if tenant.Spec.StorageClasses != nil && len(tenant.Spec.StorageClasses.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.StorageClasses.Regex); err != nil {
|
||||
return fmt.Errorf("unable to compile storageClasses allowedRegex")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate containerRegistries regexp
|
||||
func (h *validatingHandler) validateContainerRegistriesRegex(tenant *v1alpha1.Tenant) error {
|
||||
if tenant.Spec.ContainerRegistries != nil && len(tenant.Spec.ContainerRegistries.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.ContainerRegistries.Regex); err != nil {
|
||||
return fmt.Errorf("unable to compile containerRegistries allowedRegex")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate containerRegistries regexp
|
||||
func (h *validatingHandler) validateIngressHostnamesRegex(tenant *v1alpha1.Tenant) error {
|
||||
if tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Regex) > 0 {
|
||||
if _, err := regexp.Compile(tenant.Spec.IngressHostnames.Regex); err != nil {
|
||||
return fmt.Errorf("unable to compile ingressHostnames allowedRegex")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check Ingress hostnames collision across all available Tenants
|
||||
func (h *validatingHandler) validateIngressHostnamesCollision(context context.Context, clt client.Client, tenant *v1alpha1.Tenant) error {
|
||||
if !h.configuration.AllowTenantIngressHostnamesCollision() && tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Exact) > 0 {
|
||||
for _, h := range tenant.Spec.IngressHostnames.Exact {
|
||||
tntList := &v1alpha1.TenantList{}
|
||||
if err := clt.List(context, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.ingressHostnames", h),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("cannot retrieve Tenant list using .spec.ingressHostnames field selector: %w", err)
|
||||
}
|
||||
switch {
|
||||
case len(tntList.Items) == 1 && tntList.Items[0].GetName() == tenant.GetName():
|
||||
continue
|
||||
case len(tntList.Items) > 0:
|
||||
return fmt.Errorf("the allowed hostname %s is already used by the Tenant %s, cannot proceed", h, tntList.Items[0].GetName())
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validatingHandler) validateTenant(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) error {
|
||||
tenant := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateTenantByRegex(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateIngressHostnamesCollision(ctx, client, tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validatingHandler) validateTenantByRegex(tenant *v1alpha1.Tenant) error {
|
||||
if err := h.validateTenantName(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateIngressClassesRegex(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateStorageClassesRegex(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateContainerRegistriesRegex(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.validateIngressHostnamesRegex(tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validatingHandler) OnCreate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
if err := h.validateTenant(ctx, req, client, decoder); err != nil {
|
||||
return admission.Denied(err.Error())
|
||||
}
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *validatingHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *validatingHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
if err := h.validateTenant(ctx, req, client, decoder); err != nil {
|
||||
return admission.Denied(err.Error())
|
||||
}
|
||||
|
||||
oldTnt := &v1alpha1.Tenant{}
|
||||
if err := decoder.DecodeRaw(req.OldObject, oldTnt); err != nil {
|
||||
return admission.Denied(err.Error())
|
||||
}
|
||||
|
||||
newTnt := &v1alpha1.Tenant{}
|
||||
if err := decoder.Decode(req, newTnt); err != nil {
|
||||
return admission.Denied(err.Error())
|
||||
}
|
||||
|
||||
switch {
|
||||
case !oldTnt.IsCordoned() && newTnt.IsCordoned():
|
||||
recorder.Eventf(newTnt, v1.EventTypeNormal, "TenantCordoned", "Tenant has been cordoned")
|
||||
case oldTnt.IsCordoned() && !newTnt.IsCordoned():
|
||||
recorder.Eventf(newTnt, v1.EventTypeNormal, "TenantUncordoned", "Tenant has been uncordoned")
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenantprefix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
"github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/validating-v1-namespace-tenant-prefix,sideEffects=None,admissionReviewVersions=v1,mutating=false,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=prefix.namespace.capsule.clastix.io
|
||||
|
||||
type webhook struct {
|
||||
handler capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &webhook{
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webhook) GetHandler() capsulewebhook.Handler {
|
||||
return w.handler
|
||||
}
|
||||
|
||||
func (w *webhook) GetName() string {
|
||||
return "OwnerReference"
|
||||
}
|
||||
|
||||
func (w *webhook) GetPath() string {
|
||||
return "/validating-v1-namespace-tenant-prefix"
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Handler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &handler{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, ns); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
if exp, _ := r.configuration.ProtectedNamespaceRegexp(); exp != nil {
|
||||
if matched := exp.MatchString(ns.GetName()); matched {
|
||||
return admission.Denied("Creating namespaces with name matching " + exp.String() + " regexp is not allowed; please, reach out to the system administrators")
|
||||
}
|
||||
}
|
||||
|
||||
if !r.configuration.ForceTenantPrefix() {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
tnt := &v1alpha1.Tenant{}
|
||||
for _, or := range ns.ObjectMeta.OwnerReferences {
|
||||
// retrieving the selected Tenant
|
||||
if err := clt.Get(ctx, types.NamespacedName{Name: or.Name}, tnt); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName())
|
||||
|
||||
return admission.Denied("The namespace doesn't match the tenant prefix, expected " + e)
|
||||
}
|
||||
}
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
func ErroredResponse(err error) *admission.Response {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -15,16 +15,16 @@ import (
|
||||
"github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
func InCapsuleGroups(configuration configuration.Configuration, webhookHandler webhook.Handler) webhook.Handler {
|
||||
func InCapsuleGroups(configuration configuration.Configuration, handlers ...webhook.Handler) webhook.Handler {
|
||||
return &handler{
|
||||
configuration: configuration,
|
||||
handler: webhookHandler,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
configuration configuration.Configuration
|
||||
handler webhook.Handler
|
||||
handlers []webhook.Handler
|
||||
}
|
||||
|
||||
// If the user performing action is not a Capsule user, can be skipped
|
||||
@@ -45,29 +45,49 @@ func (h handler) isCapsuleUser(req admission.Request) bool {
|
||||
}
|
||||
|
||||
func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !h.isCapsuleUser(req) {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.handler.OnCreate(client, decoder, recorder)(ctx, req)
|
||||
for _, hndl := range h.handlers {
|
||||
if response := hndl.OnCreate(client, decoder, recorder)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !h.isCapsuleUser(req) {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
return h.handler.OnDelete(client, decoder, recorder)(ctx, req)
|
||||
|
||||
for _, hndl := range h.handlers {
|
||||
if response := hndl.OnDelete(client, decoder, recorder)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) admission.Response {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !h.isCapsuleUser(req) {
|
||||
return admission.Allowed("")
|
||||
return nil
|
||||
}
|
||||
return h.handler.OnUpdate(client, decoder, recorder)(ctx, req)
|
||||
|
||||
for _, hndl := range h.handlers {
|
||||
if response := hndl.OnUpdate(client, decoder, recorder)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package webhook
|
||||
|
||||
type Webhook interface {
|
||||
GetName() string
|
||||
GetPath() string
|
||||
GetHandler() Handler
|
||||
GetHandlers() []Handler
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user