feat: add defaults handler

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2023-01-05 17:24:38 +01:00
committed by Dario Tranchitella
parent fbea737a51
commit ab0fe91c58
28 changed files with 906 additions and 429 deletions

View File

@@ -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.
//
//

View File

@@ -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,
},
}
}

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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" {

View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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
}
}

View 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"
}

View File

@@ -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 ")

View 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
}

View 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
}