refactor!: using multiple handers per route

This commit is contained in:
Dario Tranchitella
2021-06-27 22:36:55 +02:00
parent d79972691e
commit ba07f99c6e
48 changed files with 1795 additions and 1201 deletions
+13 -19
View File
@@ -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")
+1 -1
View File
@@ -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
-101
View File
@@ -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("")
}
}
+2 -2
View File
@@ -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 {
+61
View File
@@ -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
}
+134
View File
@@ -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
}
+177
View File
@@ -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
}
+145
View File
@@ -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
}
-285
View File
@@ -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
}
+25 -44
View File
@@ -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
}
}
+81
View File
@@ -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
}
}
+14 -41
View File
@@ -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 &quotaWebhook{
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
}
}
@@ -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
}
}
+47 -42
View File
@@ -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
}
}
+82
View File
@@ -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"
+79
View File
@@ -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, ", "))
}
+80
View File
@@ -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"
-101
View File
@@ -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("")
}
}
+19 -36
View File
@@ -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
}
}
-97
View File
@@ -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("")
}
}
+26
View File
@@ -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
}
+26
View File
@@ -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"
}
+26
View File
@@ -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"
}
+26
View File
@@ -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"
}
+23
View File
@@ -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"
}
+26
View File
@@ -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"
}
+25
View File
@@ -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"
}
+26
View File
@@ -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"
}
+26
View File
@@ -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
View File
@@ -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
}
}
+13 -34
View File
@@ -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)
}
}
+59
View File
@@ -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
}
}
+68
View File
@@ -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
}
}
+86
View File
@@ -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
}
}
+68
View File
@@ -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
}
}
+54
View File
@@ -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
}
}
+68
View File
@@ -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
}
}
-198
View File
@@ -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("")
}
}
-99
View File
@@ -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("")
}
}
+16
View File
@@ -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
}
+32 -12
View File
@@ -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
}
}
+1 -2
View File
@@ -4,7 +4,6 @@
package webhook
type Webhook interface {
GetName() string
GetPath() string
GetHandler() Handler
GetHandlers() []Handler
}