mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 18:09:58 +00:00
feat: add defaults handler
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
committed by
Dario Tranchitella
parent
fbea737a51
commit
ab0fe91c58
@@ -8,8 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type IngressOptions struct {
|
||||
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
|
||||
AllowedClasses *api.SelectorAllowedListSpec `json:"allowedClasses,omitempty"`
|
||||
// Specifies the allowed IngressClasses assigned to the Tenant.
|
||||
// Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses.
|
||||
// A default value can be specified, and all the Ingress resources created will inherit the declared class.
|
||||
// Optional.
|
||||
AllowedClasses *api.DefaultAllowedListSpec `json:"allowedClasses,omitempty"`
|
||||
// Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames.
|
||||
//
|
||||
//
|
||||
|
||||
@@ -86,8 +86,10 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
|
||||
|
||||
in.Spec.ServiceOptions = src.Spec.ServiceOptions
|
||||
if src.Spec.StorageClasses != nil {
|
||||
in.Spec.StorageClasses = &api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *src.Spec.StorageClasses,
|
||||
in.Spec.StorageClasses = &api.DefaultAllowedListSpec{
|
||||
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *src.Spec.StorageClasses,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +108,10 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
|
||||
}
|
||||
|
||||
if ingressClass := src.Spec.IngressOptions.AllowedClasses; ingressClass != nil {
|
||||
in.Spec.IngressOptions.AllowedClasses = &api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *ingressClass,
|
||||
in.Spec.IngressOptions.AllowedClasses = &api.DefaultAllowedListSpec{
|
||||
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *ingressClass,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +128,10 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error {
|
||||
in.Spec.ImagePullPolicies = src.Spec.ImagePullPolicies
|
||||
|
||||
if src.Spec.PriorityClasses != nil {
|
||||
in.Spec.PriorityClasses = &api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *src.Spec.PriorityClasses,
|
||||
in.Spec.PriorityClasses = &api.DefaultAllowedListSpec{
|
||||
SelectorAllowedListSpec: api.SelectorAllowedListSpec{
|
||||
AllowedListSpec: *src.Spec.PriorityClasses,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@ type TenantSpec struct {
|
||||
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
|
||||
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
|
||||
ServiceOptions *api.ServiceOptions `json:"serviceOptions,omitempty"`
|
||||
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
|
||||
StorageClasses *api.SelectorAllowedListSpec `json:"storageClasses,omitempty"`
|
||||
// Specifies the allowed StorageClasses assigned to the Tenant.
|
||||
// Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses.
|
||||
// A default value can be specified, and all the PersistentVolumeClaim resources created will inherit the declared class.
|
||||
// Optional.
|
||||
StorageClasses *api.DefaultAllowedListSpec `json:"storageClasses,omitempty"`
|
||||
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
|
||||
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
|
||||
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
|
||||
@@ -35,10 +38,15 @@ type TenantSpec struct {
|
||||
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
|
||||
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
|
||||
// Specifies the allowed RuntimeClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses. Optional.
|
||||
// Specifies the allowed RuntimeClasses assigned to the Tenant.
|
||||
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses.
|
||||
// Optional.
|
||||
RuntimeClasses *api.SelectorAllowedListSpec `json:"runtimeClasses,omitempty"`
|
||||
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
|
||||
PriorityClasses *api.SelectorAllowedListSpec `json:"priorityClasses,omitempty"`
|
||||
// Specifies the allowed priorityClasses assigned to the Tenant.
|
||||
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses.
|
||||
// A default value can be specified, and all the Pod resources created will inherit the declared class.
|
||||
// Optional.
|
||||
PriorityClasses *api.DefaultAllowedListSpec `json:"priorityClasses,omitempty"`
|
||||
// Toggling the Tenant resources cordoning, when enable resources cannot be deleted.
|
||||
Cordoned bool `json:"cordoned,omitempty"`
|
||||
// Prevent accidental deletion of the Tenant.
|
||||
|
||||
@@ -261,7 +261,7 @@ func (in *IngressOptions) DeepCopyInto(out *IngressOptions) {
|
||||
*out = *in
|
||||
if in.AllowedClasses != nil {
|
||||
in, out := &in.AllowedClasses, &out.AllowedClasses
|
||||
*out = new(api.SelectorAllowedListSpec)
|
||||
*out = new(api.DefaultAllowedListSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.AllowedHostnames != nil {
|
||||
@@ -718,7 +718,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
|
||||
}
|
||||
if in.StorageClasses != nil {
|
||||
in, out := &in.StorageClasses, &out.StorageClasses
|
||||
*out = new(api.SelectorAllowedListSpec)
|
||||
*out = new(api.DefaultAllowedListSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.IngressOptions.DeepCopyInto(&out.IngressOptions)
|
||||
@@ -756,7 +756,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
|
||||
}
|
||||
if in.PriorityClasses != nil {
|
||||
in, out := &in.PriorityClasses, &out.PriorityClasses
|
||||
*out = new(api.SelectorAllowedListSpec)
|
||||
*out = new(api.DefaultAllowedListSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
4
main.go
4
main.go
@@ -38,6 +38,7 @@ 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/defaults"
|
||||
"github.com/clastix/capsule/pkg/webhook/ingress"
|
||||
namespacewebhook "github.com/clastix/capsule/pkg/webhook/namespace"
|
||||
"github.com/clastix/capsule/pkg/webhook/networkpolicy"
|
||||
@@ -237,7 +238,7 @@ func main() {
|
||||
make([]webhook.Webhook, 0),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.PatchHandler(), namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg), namespacewebhook.UserMetadataHandler())),
|
||||
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.PVC(pvc.Handler()),
|
||||
route.Service(service.Handler()),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
@@ -245,6 +246,7 @@ func main() {
|
||||
route.OwnerReference(utils.InCapsuleGroups(cfg, namespacewebhook.OwnerReferenceHandler(), ownerreference.Handler(cfg))),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
route.Defaults(defaults.Handler(cfg, kubeVersion)),
|
||||
)
|
||||
|
||||
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
|
||||
|
||||
@@ -15,18 +15,41 @@ import (
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
type DefaultAllowedListSpec struct {
|
||||
SelectorAllowedListSpec `json:",inline"`
|
||||
Default string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
func (in *DefaultAllowedListSpec) MatchDefault(value string) bool {
|
||||
return in.Default == value
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
type SelectorAllowedListSpec struct {
|
||||
AllowedListSpec `json:",inline"`
|
||||
metav1.LabelSelector `json:",inline"`
|
||||
}
|
||||
|
||||
func (in *SelectorAllowedListSpec) SelectorMatch(obj client.Object) bool {
|
||||
selector, err := metav1.LabelSelectorAsSelector(&in.LabelSelector)
|
||||
if err != nil {
|
||||
return false
|
||||
func (in *SelectorAllowedListSpec) MatchSelectByName(obj client.Object) bool {
|
||||
if obj != nil {
|
||||
return in.AllowedListSpec.Match(obj.GetName()) || in.SelectorMatch(obj)
|
||||
}
|
||||
|
||||
return selector.Matches(labels.Set(obj.GetLabels()))
|
||||
return false
|
||||
}
|
||||
|
||||
func (in *SelectorAllowedListSpec) SelectorMatch(obj client.Object) bool {
|
||||
if obj != nil {
|
||||
selector, err := metav1.LabelSelectorAsSelector(&in.LabelSelector)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return selector.Matches(labels.Set(obj.GetLabels()))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
@@ -36,6 +59,14 @@ type AllowedListSpec struct {
|
||||
Regex string `json:"allowedRegex,omitempty"`
|
||||
}
|
||||
|
||||
func (in *AllowedListSpec) Match(value string) (ok bool) {
|
||||
if in.ExactMatch(value) || in.RegexMatch(value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
|
||||
if len(in.Exact) > 0 {
|
||||
sort.SliceStable(in.Exact, func(i, j int) bool {
|
||||
|
||||
@@ -113,6 +113,22 @@ func (in *AllowedServices) DeepCopy() *AllowedServices {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DefaultAllowedListSpec) DeepCopyInto(out *DefaultAllowedListSpec) {
|
||||
*out = *in
|
||||
in.SelectorAllowedListSpec.DeepCopyInto(&out.SelectorAllowedListSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultAllowedListSpec.
|
||||
func (in *DefaultAllowedListSpec) DeepCopy() *DefaultAllowedListSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DefaultAllowedListSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
|
||||
*out = *in
|
||||
|
||||
56
pkg/webhook/defaults/errors.go
Normal file
56
pkg/webhook/defaults/errors.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StorageClassError struct {
|
||||
storageClass string
|
||||
msg error
|
||||
}
|
||||
|
||||
func NewStorageClassError(class string, msg error) error {
|
||||
return &StorageClassError{
|
||||
storageClass: class,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e StorageClassError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Storage Class %s: %s", e.storageClass, e.msg)
|
||||
}
|
||||
|
||||
type IngressClassError struct {
|
||||
ingressClass string
|
||||
msg error
|
||||
}
|
||||
|
||||
func NewIngressClassError(class string, msg error) error {
|
||||
return &IngressClassError{
|
||||
ingressClass: class,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e IngressClassError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Ingress Class %s: %s", e.ingressClass, e.msg)
|
||||
}
|
||||
|
||||
type PriorityClassError struct {
|
||||
priorityClass string
|
||||
msg error
|
||||
}
|
||||
|
||||
func NewPriorityClassError(class string, msg error) error {
|
||||
return &PriorityClassError{
|
||||
priorityClass: class,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e PriorityClassError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Priority Class %s: %s", e.priorityClass, e.msg)
|
||||
}
|
||||
68
pkg/webhook/defaults/handler.go
Normal file
68
pkg/webhook/defaults/handler.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"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/pkg/configuration"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
cfg configuration.Configuration
|
||||
version *version.Version
|
||||
}
|
||||
|
||||
func Handler(cfg configuration.Configuration, version *version.Version) capsulewebhook.Handler {
|
||||
return &handler{
|
||||
cfg: cfg,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.mutate(ctx, req, client, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
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 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 h.mutate(ctx, req, client, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) mutate(ctx context.Context, req admission.Request, c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
var response *admission.Response
|
||||
|
||||
switch {
|
||||
case req.Resource == (metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}):
|
||||
response = mutatePodDefaults(ctx, req, c, decoder, recorder)
|
||||
case req.Resource == (metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}):
|
||||
response = mutatePVCDefaults(ctx, req, c, decoder, recorder)
|
||||
case req.Resource == (metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}) || req.Resource == (metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1beta1", Resource: "ingresses"}):
|
||||
response = mutateIngressDefaults(ctx, req, h.version, c, decoder, recorder)
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
skip := admission.Allowed("Skipping Mutation")
|
||||
|
||||
response = &skip
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
78
pkg/webhook/defaults/ingress.go
Normal file
78
pkg/webhook/defaults/ingress.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
capsuleingress "github.com/clastix/capsule/pkg/webhook/ingress"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
func mutateIngressDefaults(ctx context.Context, req admission.Request, version *version.Version, c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
ingress, err := capsuleingress.FromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tnt *capsulev1beta2.Tenant
|
||||
|
||||
tnt, err = capsuleingress.TenantFromIngress(ctx, c, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
// Validate Default Ingress
|
||||
allowed := tnt.Spec.IngressOptions.AllowedClasses
|
||||
|
||||
if allowed == nil || allowed.Default == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mutate bool
|
||||
|
||||
var ingressClass client.Object
|
||||
|
||||
if ingressClassName := ingress.IngressClass(); ingressClassName != nil && *ingressClassName != allowed.Default {
|
||||
if ingressClass, err = utils.GetIngressClassByName(ctx, version, c, ingressClassName); err != nil && !k8serrors.IsNotFound(err) {
|
||||
response := admission.Denied(NewIngressClassError(*ingressClassName, err).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
} else {
|
||||
mutate = true
|
||||
}
|
||||
|
||||
if mutate = mutate || (utils.IsDefaultIngressClass(ingressClass) && ingressClass.GetName() != allowed.Default); !mutate {
|
||||
return nil
|
||||
}
|
||||
|
||||
ingress.SetIngressClass(allowed.Default)
|
||||
// Marshal Manifest
|
||||
marshaled, err := json.Marshal(ingress)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Ingress Class %s to %s/%s", allowed.Default, ingress.Name(), ingress.Namespace())
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
87
pkg/webhook/defaults/pods.go
Normal file
87
pkg/webhook/defaults/pods.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
schedulev1 "k8s.io/api/scheduling/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
func mutatePodDefaults(ctx context.Context, req admission.Request, c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
var err error
|
||||
|
||||
pod := &corev1.Pod{}
|
||||
if err = decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tnt *capsulev1beta2.Tenant
|
||||
|
||||
tnt, err = utils.TenantByStatusNamespace(ctx, c, pod.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.PriorityClasses
|
||||
|
||||
if allowed == nil || allowed.Default == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
priorityClassPod := pod.Spec.PriorityClassName
|
||||
|
||||
var mutate bool
|
||||
|
||||
var cpc *schedulev1.PriorityClass
|
||||
// PriorityClass name is empty, if no GlobalDefault is set and no PriorityClass was given on pod
|
||||
if len(priorityClassPod) > 0 && priorityClassPod != allowed.Default {
|
||||
cpc, err = utils.GetPriorityClassByName(ctx, c, priorityClassPod)
|
||||
// Should not happen, since API already checks if PC present
|
||||
if err != nil {
|
||||
response := admission.Denied(NewPriorityClassError(priorityClassPod, err).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
} else {
|
||||
mutate = true
|
||||
}
|
||||
|
||||
if mutate = mutate || (utils.IsDefaultPriorityClass(cpc) && cpc.GetName() != allowed.Default); !mutate {
|
||||
return nil
|
||||
}
|
||||
|
||||
pc, err := utils.GetPriorityClassByName(ctx, c, allowed.Default)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(fmt.Errorf("failed to assign tenant default Priority Class: %w", err))
|
||||
}
|
||||
|
||||
pod.Spec.PreemptionPolicy = pc.PreemptionPolicy
|
||||
pod.Spec.Priority = &pc.Value
|
||||
pod.Spec.PriorityClassName = pc.Name
|
||||
// Marshal Pod
|
||||
marshaled, err := json.Marshal(pod)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Priority Class %s to %s/%s", allowed.Default, pod.Namespace, pod.Name)
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
77
pkg/webhook/defaults/storage.go
Normal file
77
pkg/webhook/defaults/storage.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
func mutatePVCDefaults(ctx context.Context, req admission.Request, c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
var err error
|
||||
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
if err = decoder.Decode(req, pvc); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tnt *capsulev1beta2.Tenant
|
||||
|
||||
tnt, err = utils.TenantByStatusNamespace(ctx, c, pvc.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.StorageClasses
|
||||
|
||||
if allowed == nil || allowed.Default == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mutate bool
|
||||
|
||||
var csc *storagev1.StorageClass
|
||||
|
||||
if storageClassName := pvc.Spec.StorageClassName; storageClassName != nil && *storageClassName != allowed.Default {
|
||||
csc, err = utils.GetStorageClassByName(ctx, c, *storageClassName)
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
response := admission.Denied(NewStorageClassError(*storageClassName, err).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
} else {
|
||||
mutate = true
|
||||
}
|
||||
|
||||
if mutate = mutate || (utils.IsDefaultStorageClass(csc) && csc.GetName() != allowed.Default); !mutate {
|
||||
return nil
|
||||
}
|
||||
|
||||
pvc.Spec.StorageClassName = &tnt.Spec.StorageClasses.Default
|
||||
// Marshal Manifest
|
||||
marshaled, err := json.Marshal(pvc)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Storage Class %s to %s/%s", allowed.Default, pvc.Namespace, pvc.Name)
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -8,22 +8,25 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/clastix/capsule/pkg/api"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type ingressClassForbiddenError struct {
|
||||
className string
|
||||
spec api.SelectorAllowedListSpec
|
||||
ingressClassName string
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
func NewIngressClassForbidden(className string, spec api.SelectorAllowedListSpec) error {
|
||||
func NewIngressClassForbidden(class string, spec api.DefaultAllowedListSpec) error {
|
||||
return &ingressClassForbiddenError{
|
||||
className: className,
|
||||
spec: spec,
|
||||
ingressClassName: class,
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func (i ingressClassForbiddenError) Error() string {
|
||||
return fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant%s", i.className, appendClassError(i.spec))
|
||||
err := fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant: ", i.ingressClassName)
|
||||
|
||||
return utils.DefaultAllowedValuesErrorMessage(i.spec, err)
|
||||
}
|
||||
|
||||
type ingressHostnameNotValidError struct {
|
||||
@@ -53,35 +56,36 @@ func (i ingressHostnameNotValidError) Error() string {
|
||||
i.invalidHostnames, i.notMatchingHostnames, appendHostnameError(i.spec))
|
||||
}
|
||||
|
||||
type ingressClassNotValidError struct {
|
||||
spec api.SelectorAllowedListSpec
|
||||
type ingressClassUndefinedError struct {
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
func NewIngressClassNotValid(spec api.SelectorAllowedListSpec) error {
|
||||
return &ingressClassNotValidError{
|
||||
func NewIngressClassUndefined(spec api.DefaultAllowedListSpec) error {
|
||||
return &ingressClassUndefinedError{
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func (i ingressClassNotValidError) Error() string {
|
||||
return "A valid Ingress Class must be used" + appendClassError(i.spec)
|
||||
func (i ingressClassUndefinedError) Error() string {
|
||||
return utils.DefaultAllowedValuesErrorMessage(i.spec, "No Ingress Class is forbidden for the current Tenant. Specify a Ingress Class which is allowed within the Tenant: ")
|
||||
}
|
||||
|
||||
// nolint:predeclared
|
||||
func appendClassError(spec api.SelectorAllowedListSpec) (append string) {
|
||||
if len(spec.Exact) > 0 {
|
||||
append += fmt.Sprintf(", one of the following (%s)", strings.Join(spec.Exact, ", "))
|
||||
}
|
||||
type ingressClassNotValidError struct {
|
||||
ingressClassName string
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
if len(spec.Regex) > 0 {
|
||||
append += fmt.Sprintf(", or matching the regex %s", spec.Regex)
|
||||
func NewIngressClassNotValid(class string, spec api.DefaultAllowedListSpec) error {
|
||||
return &ingressClassNotValidError{
|
||||
ingressClassName: class,
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
if len(spec.MatchLabels) > 0 || len(spec.MatchExpressions) > 0 {
|
||||
append += fmt.Sprintf(", or matching the label selector defined in the Tenant")
|
||||
}
|
||||
func (i ingressClassNotValidError) Error() string {
|
||||
err := fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant: ", i.ingressClassName)
|
||||
|
||||
return
|
||||
return utils.DefaultAllowedValuesErrorMessage(i.spec, err)
|
||||
}
|
||||
|
||||
// nolint:predeclared
|
||||
|
||||
@@ -21,6 +21,7 @@ type Ingress interface {
|
||||
Namespace() string
|
||||
Name() string
|
||||
HostnamePathsPairs() map[string]sets.String
|
||||
SetIngressClass(string)
|
||||
}
|
||||
|
||||
type NetworkingV1 struct {
|
||||
@@ -44,6 +45,20 @@ func (n NetworkingV1) IngressClass() (res *string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (n NetworkingV1) SetIngressClass(ingressClassName string) {
|
||||
if n.Spec.IngressClassName == nil {
|
||||
if a := n.GetAnnotations(); a != nil {
|
||||
if _, ok := a[annotationName]; ok {
|
||||
a[annotationName] = ingressClassName
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Assign in case the IngressClassName property was not set
|
||||
n.Spec.IngressClassName = &ingressClassName
|
||||
}
|
||||
|
||||
func (n NetworkingV1) Namespace() string {
|
||||
return n.GetNamespace()
|
||||
}
|
||||
@@ -96,6 +111,20 @@ func (n NetworkingV1Beta1) IngressClass() (res *string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) SetIngressClass(ingressClassName string) {
|
||||
if n.Spec.IngressClassName == nil {
|
||||
if a := n.GetAnnotations(); a != nil {
|
||||
if _, ok := a[annotationName]; ok {
|
||||
a[annotationName] = ingressClassName
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Assign in case the IngressClassName property was not set
|
||||
n.Annotations[annotationName] = ingressClassName
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) Namespace() string {
|
||||
return n.GetNamespace()
|
||||
}
|
||||
@@ -148,6 +177,18 @@ func (e Extension) IngressClass() (res *string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e Extension) SetIngressClass(ingressClassName string) {
|
||||
if a := e.GetAnnotations(); a != nil {
|
||||
if _, ok := a[annotationName]; ok {
|
||||
a[annotationName] = ingressClassName
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
// Assign in case the IngressClassName property was not set
|
||||
e.Annotations[annotationName] = ingressClassName
|
||||
}
|
||||
|
||||
func (e Extension) Namespace() string {
|
||||
return e.GetNamespace()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
func tenantFromIngress(ctx context.Context, c client.Client, ingress Ingress) (*capsulev1beta2.Tenant, error) {
|
||||
func TenantFromIngress(ctx context.Context, c client.Client, ingress Ingress) (*capsulev1beta2.Tenant, error) {
|
||||
tenantList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()),
|
||||
@@ -33,7 +33,7 @@ func tenantFromIngress(ctx context.Context, c client.Client, ingress Ingress) (*
|
||||
}
|
||||
|
||||
// nolint:nakedret
|
||||
func ingressFromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) {
|
||||
func FromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) {
|
||||
switch req.Kind.Group {
|
||||
case "networking.k8s.io":
|
||||
if req.Kind.Version == "v1" {
|
||||
|
||||
@@ -7,12 +7,8 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -29,135 +25,22 @@ type class struct {
|
||||
version *version.Version
|
||||
}
|
||||
|
||||
func Class(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
version, _ := utils.GetK8sVersion()
|
||||
|
||||
func Class(configuration configuration.Configuration, version *version.Version) capsulewebhook.Handler {
|
||||
return &class{
|
||||
configuration: configuration,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) retrieveIngressClass(ctx context.Context, ctrlClient client.Client, ingressClassName *string) (client.Object, error) {
|
||||
if r.version == nil || ingressClassName == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var obj client.Object
|
||||
|
||||
switch {
|
||||
case r.version.Minor() < 18:
|
||||
return nil, nil
|
||||
case r.version.Minor() < 19:
|
||||
obj = &networkingv1beta1.IngressClass{}
|
||||
default:
|
||||
obj = &networkingv1.IngressClass{}
|
||||
}
|
||||
|
||||
if err := ctrlClient.Get(ctx, types.NamespacedName{Name: *ingressClassName}, obj); err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
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 *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ic, err := r.retrieveIngressClass(ctx, client, ingress.IngressClass())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if err = r.validateClass(*tenant, ingress.IngressClass(), ic); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var forbiddenErr *ingressClassForbiddenError
|
||||
|
||||
if errors.As(err, &forbiddenErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
var invalidErr *ingressClassNotValidError
|
||||
|
||||
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
|
||||
return r.validate(ctx, r.version, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
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 *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ic, err := r.retrieveIngressClass(ctx, client, ingress.IngressClass())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if err = r.validateClass(*tenant, ingress.IngressClass(), ic); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var forbiddenErr *ingressClassForbiddenError
|
||||
|
||||
if errors.As(err, &forbiddenErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name())
|
||||
}
|
||||
|
||||
var invalidErr *ingressClassNotValidError
|
||||
|
||||
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
|
||||
return r.validate(ctx, r.version, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,32 +50,66 @@ func (r *class) OnDelete(client.Client, *admission.Decoder, record.EventRecorder
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) validateClass(tenant capsulev1beta2.Tenant, ingressClass *string, ingressClassObj client.Object) error {
|
||||
if tenant.Spec.IngressOptions.AllowedClasses == nil {
|
||||
func (r *class) validate(ctx context.Context, version *version.Version, client client.Client, req admission.Request, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
ingress, err := FromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tnt *capsulev1beta2.Tenant
|
||||
|
||||
tnt, err = TenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.IngressOptions.AllowedClasses
|
||||
|
||||
if allowed == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ingressClass := ingress.IngressClass()
|
||||
|
||||
if ingressClass == nil {
|
||||
return NewIngressClassNotValid(*tenant.Spec.IngressOptions.AllowedClasses)
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "MissingIngressClass", "Ingress %s/%s is missing IngressClass", req.Namespace, req.Name)
|
||||
|
||||
response := admission.Denied(NewIngressClassUndefined(*allowed).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
var valid, regex, match bool
|
||||
selector := false
|
||||
|
||||
if len(tenant.Spec.IngressOptions.AllowedClasses.Exact) > 0 {
|
||||
valid = tenant.Spec.IngressOptions.AllowedClasses.ExactMatch(*ingressClass)
|
||||
// Verify if the IngressClass exists and matches the label selector/expression
|
||||
if len(allowed.MatchExpressions) > 0 || len(allowed.MatchLabels) > 0 {
|
||||
ingressClassObj, err := utils.GetIngressClassByName(ctx, version, client, ingressClass)
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// Ingress Class is present, check if it matches the selector
|
||||
if ingressClassObj != nil {
|
||||
selector = allowed.SelectorMatch(ingressClassObj)
|
||||
}
|
||||
}
|
||||
|
||||
regex = tenant.Spec.IngressOptions.AllowedClasses.RegexMatch(*ingressClass)
|
||||
switch {
|
||||
case allowed.MatchDefault(*ingressClass):
|
||||
return nil
|
||||
case allowed.Match(*ingressClass) || selector:
|
||||
return nil
|
||||
default:
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenIngressClass", "Ingress %s/%s IngressClass %s is forbidden for the current Tenant", req.Namespace, req.Name, &ingressClass)
|
||||
|
||||
if ingressClassObj != nil {
|
||||
match = tenant.Spec.IngressOptions.AllowedClasses.SelectorMatch(ingressClassObj)
|
||||
} else {
|
||||
match = true
|
||||
response := admission.Denied(NewIngressClassForbidden(*ingressClass, *allowed).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if !valid && !regex && !match {
|
||||
return NewIngressClassForbidden(*ingressClass, *tenant.Spec.IngressOptions.AllowedClasses)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,73 +34,15 @@ func Collision(configuration configuration.Configuration) capsulewebhook.Handler
|
||||
return &collision{configuration: configuration}
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
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 {
|
||||
ing, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ing)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil || tenant.Spec.IngressOptions.HostnameCollisionScope == api.HostnameCollisionScopeDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateCollision(ctx, client, ing, tenant.Spec.IngressOptions.HostnameCollisionScope); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var collisionErr *ingressHostnameCollisionError
|
||||
|
||||
if errors.As(err, &collisionErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ing.Namespace(), ing.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
return r.validate(ctx, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:dupl
|
||||
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 {
|
||||
ing, err := ingressFromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, client, ing)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil || tenant.Spec.IngressOptions.HostnameCollisionScope == api.HostnameCollisionScopeDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateCollision(ctx, client, ing, tenant.Spec.IngressOptions.HostnameCollisionScope); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var collisionErr *ingressHostnameCollisionError
|
||||
|
||||
if errors.As(err, &collisionErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ing.Namespace(), ing.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
return r.validate(ctx, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +52,38 @@ func (r *collision) OnDelete(client.Client, *admission.Decoder, record.EventReco
|
||||
}
|
||||
}
|
||||
|
||||
func (r *collision) validate(ctx context.Context, client client.Client, req admission.Request, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
ing, err := FromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = TenantFromIngress(ctx, client, ing)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil || tenant.Spec.IngressOptions.HostnameCollisionScope == api.HostnameCollisionScopeDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.validateCollision(ctx, client, ing, tenant.Spec.IngressOptions.HostnameCollisionScope); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var collisionErr *ingressHostnameCollisionError
|
||||
|
||||
if errors.As(err, &collisionErr) {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ing.Namespace(), ing.Name())
|
||||
}
|
||||
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// nolint:gocognit,gocyclo,cyclop
|
||||
func (r *collision) validateCollision(ctx context.Context, clt client.Client, ing Ingress, scope api.HostnameCollisionScope) error {
|
||||
for hostname, paths := range ing.HostnamePathsPairs() {
|
||||
|
||||
@@ -30,83 +30,13 @@ func Hostnames(configuration configuration.Configuration) capsulewebhook.Handler
|
||||
|
||||
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 *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, c, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil || tenant.Spec.IngressOptions.AllowedHostnames == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostnameList := sets.NewString()
|
||||
for hostname := range ingress.HostnamePathsPairs() {
|
||||
hostnameList.Insert(hostname)
|
||||
}
|
||||
|
||||
if err = r.validateHostnames(*tenant, hostnameList); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hostnameNotValidErr *ingressHostnameNotValidError
|
||||
|
||||
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)
|
||||
return r.validate(ctx, c, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
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 *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = tenantFromIngress(ctx, c, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostnameSet := sets.NewString()
|
||||
for hostname := range ingress.HostnamePathsPairs() {
|
||||
hostnameSet.Insert(hostname)
|
||||
}
|
||||
|
||||
if err = r.validateHostnames(*tenant, hostnameSet); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hostnameNotValidErr *ingressHostnameNotValidError
|
||||
|
||||
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)
|
||||
return r.validate(ctx, c, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +46,45 @@ func (r *hostnames) OnDelete(client.Client, *admission.Decoder, record.EventReco
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hostnames) validate(ctx context.Context, client client.Client, req admission.Request, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
ingress, err := FromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tenant *capsulev1beta2.Tenant
|
||||
|
||||
tenant, err = TenantFromIngress(ctx, client, ingress)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant == nil || tenant.Spec.IngressOptions.AllowedHostnames == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostnameList := sets.NewString()
|
||||
for hostname := range ingress.HostnamePathsPairs() {
|
||||
hostnameList.Insert(hostname)
|
||||
}
|
||||
|
||||
if err = r.validateHostnames(*tenant, hostnameList); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hostnameNotValidErr *ingressHostnameNotValidError
|
||||
|
||||
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) validateHostnames(tenant capsulev1beta2.Tenant, hostnames sets.String) error {
|
||||
if tenant.Spec.IngressOptions.AllowedHostnames == nil {
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,7 @@ func Wildcard() capsulewebhook.Handler {
|
||||
|
||||
func (h *wildcard) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.wildcardHandler(ctx, client, req, recorder, decoder)
|
||||
return h.validate(ctx, client, req, recorder, decoder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ func (h *wildcard) OnDelete(client client.Client, decoder *admission.Decoder, re
|
||||
|
||||
func (h *wildcard) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.wildcardHandler(ctx, client, req, recorder, decoder)
|
||||
return h.validate(ctx, client, req, recorder, decoder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wildcard) wildcardHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder, decoder *admission.Decoder) *admission.Response {
|
||||
func (h *wildcard) validate(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder, decoder *admission.Decoder) *admission.Response {
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
|
||||
if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
@@ -61,7 +61,7 @@ func (h *wildcard) wildcardHandler(ctx context.Context, clt client.Client, req a
|
||||
|
||||
if !tnt.Spec.IngressOptions.AllowWildcardHostnames {
|
||||
// Retrieve ingress resource from request.
|
||||
ingress, err := ingressFromRequest(req, decoder)
|
||||
ingress, err := FromRequest(req, decoder)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"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"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -52,44 +50,57 @@ func (h *priorityClass) OnCreate(c client.Client, decoder *admission.Decoder, re
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
tnt, err := utils.TenantByStatusNamespace(ctx, c, pod.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tntList.Items[0].Spec.PriorityClasses
|
||||
allowed := tnt.Spec.PriorityClasses
|
||||
|
||||
if allowed == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
priorityClassName := pod.Spec.PriorityClassName
|
||||
|
||||
class, err := h.class(ctx, c, priorityClassName)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
if len(priorityClassName) == 0 {
|
||||
// We don't have to force Pod to specify a Priority Class
|
||||
return nil
|
||||
}
|
||||
|
||||
return &response
|
||||
selector := false
|
||||
|
||||
// Verify if the StorageClass exists and matches the label selector/expression
|
||||
if len(allowed.MatchExpressions) > 0 || len(allowed.MatchLabels) > 0 {
|
||||
priorityClassObj, err := utils.GetPriorityClassByName(ctx, c, priorityClassName)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// Storage Class is present, check if it matches the selector
|
||||
if priorityClassObj != nil {
|
||||
selector = allowed.SelectorMatch(priorityClassObj)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case allowed == nil:
|
||||
// Enforcement is not in place, skipping it at all
|
||||
case allowed.MatchDefault(priorityClassName):
|
||||
// Allow if given Priority Class is equal tenant default (eventough it's not allowed by selector)
|
||||
return nil
|
||||
case len(priorityClassName) == 0:
|
||||
// We don't have to force Pod to specify a Priority Class
|
||||
case allowed.Match(priorityClassName) || selector:
|
||||
return nil
|
||||
case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName) && !allowed.SelectorMatch(class):
|
||||
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)
|
||||
default:
|
||||
recorder.Eventf(tnt, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
|
||||
type podPriorityClassForbiddenError struct {
|
||||
priorityClassName string
|
||||
spec api.SelectorAllowedListSpec
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
func NewPodPriorityClassForbidden(priorityClassName string, spec api.SelectorAllowedListSpec) error {
|
||||
func NewPodPriorityClassForbidden(priorityClassName string, spec api.DefaultAllowedListSpec) error {
|
||||
return &podPriorityClassForbiddenError{
|
||||
priorityClassName: priorityClassName,
|
||||
spec: spec,
|
||||
@@ -23,7 +23,7 @@ func NewPodPriorityClassForbidden(priorityClassName string, spec api.SelectorAll
|
||||
}
|
||||
|
||||
func (f podPriorityClassForbiddenError) Error() (err string) {
|
||||
err = fmt.Sprintf("Pod Priorioty Class %s is forbidden for the current Tenant: ", f.priorityClassName)
|
||||
msg := fmt.Sprintf("Pod Priority Class %s is forbidden for the current Tenant: ", f.priorityClassName)
|
||||
|
||||
return utils.AllowedValuesErrorMessage(f.spec, err)
|
||||
return utils.DefaultAllowedValuesErrorMessage(f.spec, msg)
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ import (
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
nodev1 "k8s.io/api/node/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"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"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -63,19 +61,16 @@ func (h *runtimeClass) validate(ctx context.Context, c client.Client, decoder *a
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
tnt, err := utils.TenantByStatusNamespace(ctx, c, pod.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tntList.Items[0].Spec.RuntimeClasses
|
||||
allowed := tnt.Spec.RuntimeClasses
|
||||
|
||||
runtimeClassName := ""
|
||||
if pod.Spec.RuntimeClassName != nil {
|
||||
@@ -96,8 +91,8 @@ func (h *runtimeClass) validate(ctx context.Context, c client.Client, decoder *a
|
||||
case len(runtimeClassName) == 0:
|
||||
// We don't have to force Pod to specify a RuntimeClass
|
||||
return nil
|
||||
case !allowed.ExactMatch(runtimeClassName) && !allowed.RegexMatch(runtimeClassName) && !allowed.SelectorMatch(class):
|
||||
recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenRuntimeClass", "Pod %s/%s is using Runtime Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, runtimeClassName)
|
||||
case !allowed.MatchSelectByName(class):
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenRuntimeClass", "Pod %s/%s is using Runtime Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, runtimeClassName)
|
||||
|
||||
response := admission.Denied(NewPodRuntimeClassForbidden(runtimeClassName, *allowed).Error())
|
||||
|
||||
|
||||
@@ -5,48 +5,33 @@ package pvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/clastix/capsule/pkg/api"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type storageClassNotValidError struct {
|
||||
spec api.SelectorAllowedListSpec
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
func NewStorageClassNotValid(storageClasses api.SelectorAllowedListSpec) error {
|
||||
func NewStorageClassNotValid(storageClasses api.DefaultAllowedListSpec) error {
|
||||
return &storageClassNotValidError{
|
||||
spec: storageClasses,
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:predeclared
|
||||
func appendError(spec api.SelectorAllowedListSpec) (append string) {
|
||||
if len(spec.Exact) > 0 {
|
||||
append += fmt.Sprintf(", one of the following (%s)", strings.Join(spec.Exact, ", "))
|
||||
}
|
||||
|
||||
if len(spec.Regex) > 0 {
|
||||
append += fmt.Sprintf(", or matching the regex %s", spec.Regex)
|
||||
}
|
||||
|
||||
if len(spec.MatchLabels) > 0 || len(spec.MatchExpressions) > 0 {
|
||||
append += ", or matching the label selector defined in the Tenant"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s storageClassNotValidError) Error() (err string) {
|
||||
return "A valid Storage Class must be used" + appendError(s.spec)
|
||||
msg := "A valid Storage Class must be used: "
|
||||
|
||||
return utils.DefaultAllowedValuesErrorMessage(s.spec, msg)
|
||||
}
|
||||
|
||||
type storageClassForbiddenError struct {
|
||||
className string
|
||||
spec api.SelectorAllowedListSpec
|
||||
spec api.DefaultAllowedListSpec
|
||||
}
|
||||
|
||||
func NewStorageClassForbidden(className string, storageClasses api.SelectorAllowedListSpec) error {
|
||||
func NewStorageClassForbidden(className string, storageClasses api.DefaultAllowedListSpec) error {
|
||||
return &storageClassForbiddenError{
|
||||
className: className,
|
||||
spec: storageClasses,
|
||||
@@ -54,5 +39,7 @@ func NewStorageClassForbidden(className string, storageClasses api.SelectorAllow
|
||||
}
|
||||
|
||||
func (f storageClassForbiddenError) Error() string {
|
||||
return fmt.Sprintf("Storage Class %s is forbidden for the current Tenant%s", f.className, appendError(f.spec))
|
||||
msg := fmt.Sprintf("Storage Class %s is forbidden for the current Tenant ", f.className)
|
||||
|
||||
return utils.DefaultAllowedValuesErrorMessage(f.spec, msg)
|
||||
}
|
||||
|
||||
@@ -8,15 +8,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"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"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -27,20 +23,6 @@ func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
func (h *handler) getStorageClass(ctx context.Context, c client.Client, name string) (client.Object, error) {
|
||||
obj := &v1.StorageClass{}
|
||||
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: name}, obj); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
@@ -48,59 +30,60 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pvc.Namespace),
|
||||
}); err != nil {
|
||||
tnt, err := utils.TenantByStatusNamespace(ctx, c, pvc.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
allowed := tnt.Spec.StorageClasses
|
||||
|
||||
if tnt.Spec.StorageClasses == nil {
|
||||
if allowed == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pvc.Spec.StorageClassName == nil {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingStorageClass", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name)
|
||||
storageClass := pvc.Spec.StorageClassName
|
||||
|
||||
response := admission.Denied(NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses).Error())
|
||||
if storageClass == nil {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "MissingStorageClass", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name)
|
||||
|
||||
response := admission.Denied(NewStorageClassNotValid(*tnt.Spec.StorageClasses).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
sc := *pvc.Spec.StorageClassName
|
||||
selector := false
|
||||
|
||||
scObj, err := h.getStorageClass(ctx, c, sc)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
// Verify if the StorageClass exists and matches the label selector/expression
|
||||
if len(allowed.MatchExpressions) > 0 || len(allowed.MatchLabels) > 0 {
|
||||
storageClassObj, err := utils.GetStorageClassByName(ctx, c, *storageClass)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
return &response
|
||||
}
|
||||
|
||||
// Storage Class is present, check if it matches the selector
|
||||
if storageClassObj != nil {
|
||||
selector = allowed.SelectorMatch(storageClassObj)
|
||||
}
|
||||
}
|
||||
|
||||
var valid, regex, match bool
|
||||
|
||||
valid, regex = tnt.Spec.StorageClasses.ExactMatch(sc), tnt.Spec.StorageClasses.RegexMatch(sc)
|
||||
|
||||
if scObj != nil {
|
||||
match = tnt.Spec.StorageClasses.SelectorMatch(scObj)
|
||||
} else {
|
||||
match = true
|
||||
}
|
||||
|
||||
if !valid && !regex && !match {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenStorageClass", "PersistentVolumeClaim %s/%s StorageClass %s is forbidden for the current Tenant", req.Namespace, req.Name, sc)
|
||||
switch {
|
||||
case allowed.MatchDefault(*storageClass):
|
||||
return nil
|
||||
case allowed.Match(*storageClass) || selector:
|
||||
return nil
|
||||
default:
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenStorageClass", "PersistentVolumeClaim %s/%s StorageClass %s is forbidden for the current Tenant", req.Namespace, req.Name, *storageClass)
|
||||
|
||||
response := admission.Denied(NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
pkg/webhook/route/defaults.go
Normal file
28
pkg/webhook/route/defaults.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/defaults,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=pods,verbs=create,versions=v1,name=pod.defaults.capsule.clastix.io
|
||||
// +kubebuilder:webhook:path=/defaults,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=storage.defaults.capsule.clastix.io
|
||||
// +kubebuilder:webhook:path=/defaults,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io,resources=ingresses,verbs=create;update,versions=v1beta1;v1,name=ingress.defaults.capsule.clastix.io
|
||||
|
||||
type defaults struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Defaults(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &defaults{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *defaults) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *defaults) GetPath() string {
|
||||
return "/defaults"
|
||||
}
|
||||
@@ -19,6 +19,10 @@ func ErroredResponse(err error) *admission.Response {
|
||||
return &response
|
||||
}
|
||||
|
||||
func DefaultAllowedValuesErrorMessage(allowed api.DefaultAllowedListSpec, err string) string {
|
||||
return AllowedValuesErrorMessage(allowed.SelectorAllowedListSpec, err)
|
||||
}
|
||||
|
||||
func AllowedValuesErrorMessage(allowed api.SelectorAllowedListSpec, err string) string {
|
||||
var extra []string
|
||||
if len(allowed.Exact) > 0 {
|
||||
@@ -26,11 +30,11 @@ func AllowedValuesErrorMessage(allowed api.SelectorAllowedListSpec, err string)
|
||||
}
|
||||
|
||||
if len(allowed.Regex) > 0 {
|
||||
extra = append(extra, fmt.Sprintf(" use one matching the following regex (%s)", allowed.Regex))
|
||||
extra = append(extra, fmt.Sprintf("use one matching the following regex (%s)", allowed.Regex))
|
||||
}
|
||||
|
||||
if len(allowed.MatchLabels) > 0 || len(allowed.MatchExpressions) > 0 {
|
||||
extra = append(extra, ", or matching the label selector defined in the Tenant")
|
||||
extra = append(extra, "matching the label selector defined in the Tenant")
|
||||
}
|
||||
|
||||
err += strings.Join(extra, " or ")
|
||||
|
||||
100
pkg/webhook/utils/resources.go
Normal file
100
pkg/webhook/utils/resources.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
schedulev1 "k8s.io/api/scheduling/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const TRUE string = "true"
|
||||
|
||||
// Get PriorityClass by name (Does not return error if not found).
|
||||
func GetPriorityClassByName(ctx context.Context, c client.Client, name string) (*schedulev1.PriorityClass, error) {
|
||||
class := &schedulev1.PriorityClass{}
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: name}, class); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return class, nil
|
||||
}
|
||||
|
||||
// Get StorageClass by name (Does not return error if not found).
|
||||
func GetStorageClassByName(ctx context.Context, c client.Client, name string) (*storagev1.StorageClass, error) {
|
||||
class := &storagev1.StorageClass{}
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: name}, class); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return class, nil
|
||||
}
|
||||
|
||||
// Get IngressClass by name (Does not return error if not found).
|
||||
func GetIngressClassByName(ctx context.Context, version *version.Version, c client.Client, ingressClassName *string) (client.Object, error) {
|
||||
if ingressClassName == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var obj client.Object
|
||||
|
||||
switch {
|
||||
case version == nil:
|
||||
obj = &networkingv1.IngressClass{}
|
||||
case version.Minor() < 18:
|
||||
return nil, nil
|
||||
case version.Minor() < 19:
|
||||
obj = &networkingv1beta1.IngressClass{}
|
||||
default:
|
||||
obj = &networkingv1.IngressClass{}
|
||||
}
|
||||
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: *ingressClassName}, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// IsDefaultPriorityClass checks if the given PriorityClass is cluster default.
|
||||
func IsDefaultPriorityClass(class *schedulev1.PriorityClass) bool {
|
||||
if class != nil {
|
||||
return class.GlobalDefault
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsDefaultIngressClass(class client.Object) bool {
|
||||
annotation := "ingressclass.kubernetes.io/is-default-class"
|
||||
|
||||
if class != nil {
|
||||
annotations := class.GetAnnotations()
|
||||
if v, ok := annotations[annotation]; ok && v == TRUE {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsDefaultStorageClass checks if the given StorageClass is cluster default.
|
||||
func IsDefaultStorageClass(class client.Object) bool {
|
||||
annotation := "storageclass.kubernetes.io/is-default-class"
|
||||
|
||||
if class != nil {
|
||||
annotations := class.GetAnnotations()
|
||||
if v, ok := annotations[annotation]; ok && v == TRUE {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
32
pkg/webhook/utils/tenant_by_field.go
Normal file
32
pkg/webhook/utils/tenant_by_field.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/clastix/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
func TenantByStatusNamespace(ctx context.Context, c client.Client, namespace string) (*capsulev1beta2.Tenant, error) {
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
tnt := &capsulev1beta2.Tenant{}
|
||||
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return tnt, nil
|
||||
}
|
||||
|
||||
*tnt = tntList.Items[0]
|
||||
|
||||
return tnt, nil
|
||||
}
|
||||
Reference in New Issue
Block a user