diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml
index 75c547236..715ce1353 100644
--- a/config/crd/bases/capsule.clastix.io_tenants.yaml
+++ b/config/crd/bases/capsule.clastix.io_tenants.yaml
@@ -1873,6 +1873,28 @@ spec:
required:
- allowed
type: object
+ forbiddenAnnotations:
+ description: Define the annotations that a Tenant Owner cannot
+ set for their Service resources.
+ properties:
+ denied:
+ items:
+ type: string
+ type: array
+ deniedRegex:
+ type: string
+ type: object
+ forbiddenLabels:
+ description: Define the labels that a Tenant Owner cannot set
+ for their Service resources.
+ properties:
+ denied:
+ items:
+ type: string
+ type: array
+ deniedRegex:
+ type: string
+ type: object
type: object
storageClasses:
description: Specifies the allowed StorageClasses assigned to the
@@ -3107,6 +3129,28 @@ spec:
required:
- allowed
type: object
+ forbiddenAnnotations:
+ description: Define the annotations that a Tenant Owner cannot
+ set for their Service resources.
+ properties:
+ denied:
+ items:
+ type: string
+ type: array
+ deniedRegex:
+ type: string
+ type: object
+ forbiddenLabels:
+ description: Define the labels that a Tenant Owner cannot set
+ for their Service resources.
+ properties:
+ denied:
+ items:
+ type: string
+ type: array
+ deniedRegex:
+ type: string
+ type: object
type: object
storageClasses:
description: Specifies the allowed StorageClasses assigned to the
diff --git a/docs/content/general/crds-apis.md b/docs/content/general/crds-apis.md
index 696a41eba..f017b3c5c 100644
--- a/docs/content/general/crds-apis.md
+++ b/docs/content/general/crds-apis.md
@@ -4756,6 +4756,20 @@ Specifies options for the Service, such as additional metadata or block of certa
Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
false |
+
+ forbiddenAnnotations |
+ object |
+
+ Define the annotations that a Tenant Owner cannot set for their Service resources.
+ |
+ false |
+
+ forbiddenLabels |
+ object |
+
+ Define the labels that a Tenant Owner cannot set for their Service resources.
+ |
+ false |
@@ -4865,6 +4879,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An
+### Tenant.spec.serviceOptions.forbiddenAnnotations
+
+
+
+Define the annotations that a Tenant Owner cannot set for their Service resources.
+
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ denied |
+ []string |
+
+
+ |
+ false |
+
+ deniedRegex |
+ string |
+
+
+ |
+ false |
+
+
+
+
+### Tenant.spec.serviceOptions.forbiddenLabels
+
+
+
+Define the labels that a Tenant Owner cannot set for their Service resources.
+
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ denied |
+ []string |
+
+
+ |
+ false |
+
+ deniedRegex |
+ string |
+
+
+ |
+ false |
+
+
+
+
### Tenant.spec.storageClasses
@@ -6615,6 +6695,20 @@ Specifies options for the Service, such as additional metadata or block of certa
Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
false |
+
+ forbiddenAnnotations |
+ object |
+
+ Define the annotations that a Tenant Owner cannot set for their Service resources.
+ |
+ false |
+
+ forbiddenLabels |
+ object |
+
+ Define the labels that a Tenant Owner cannot set for their Service resources.
+ |
+ false |
@@ -6724,6 +6818,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An
+### Tenant.spec.serviceOptions.forbiddenAnnotations
+
+
+
+Define the annotations that a Tenant Owner cannot set for their Service resources.
+
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ denied |
+ []string |
+
+
+ |
+ false |
+
+ deniedRegex |
+ string |
+
+
+ |
+ false |
+
+
+
+
+### Tenant.spec.serviceOptions.forbiddenLabels
+
+
+
+Define the labels that a Tenant Owner cannot set for their Service resources.
+
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ denied |
+ []string |
+
+
+ |
+ false |
+
+ deniedRegex |
+ string |
+
+
+ |
+ false |
+
+
+
+
### Tenant.spec.storageClasses
diff --git a/e2e/service_forbidden_metadata_test.go b/e2e/service_forbidden_metadata_test.go
new file mode 100644
index 000000000..0af4ca14c
--- /dev/null
+++ b/e2e/service_forbidden_metadata_test.go
@@ -0,0 +1,161 @@
+//go:build e2e
+
+// Copyright 2020-2023 Project Capsule Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package e2e
+
+import (
+ "context"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+
+ capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
+ "github.com/projectcapsule/capsule/pkg/api"
+)
+
+var _ = Describe("creating a Service with user-specified labels and annotations", func() {
+ tnt := &capsulev1beta2.Tenant{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "tenant-user-metadata-forbidden",
+ },
+ Spec: capsulev1beta2.TenantSpec{
+ ServiceOptions: &api.ServiceOptions{
+ ForbiddenLabels: api.ForbiddenListSpec{
+ Exact: []string{"foo", "bar"},
+ Regex: "^gatsby-.*$",
+ },
+ ForbiddenAnnotations: api.ForbiddenListSpec{
+ Exact: []string{"foo", "bar"},
+ Regex: "^gatsby-.*$",
+ },
+ },
+ Owners: capsulev1beta2.OwnerListSpec{
+ {
+ Name: "gatsby",
+ Kind: "User",
+ },
+ },
+ },
+ }
+
+ JustBeforeEach(func() {
+ EventuallyCreation(func() error {
+ tnt.ResourceVersion = ""
+ return k8sClient.Create(context.TODO(), tnt)
+ }).Should(Succeed())
+ })
+ JustAfterEach(func() {
+ Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
+ })
+
+ It("should allow", func() {
+ By("specifying non-forbidden labels", func() {
+ svc := NewService("")
+ svc.SetLabels(map[string]string{"bim": "baz"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ })
+ By("specifying non-forbidden annotations", func() {
+ svc := NewService("")
+ svc.SetAnnotations(map[string]string{"bim": "baz"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ })
+ })
+
+ It("should fail when creating a Service", func() {
+ By("specifying forbidden labels using exact match", func() {
+ svc := NewService("")
+ svc.SetLabels(map[string]string{"foo": "bar"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
+ })
+ By("specifying forbidden labels using regex match", func() {
+ svc := NewService("")
+ svc.SetLabels(map[string]string{"gatsby-foo": "bar"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
+ })
+ By("specifying forbidden annotations using exact match", func() {
+ svc := NewService("")
+ svc.SetAnnotations(map[string]string{"foo": "bar"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
+ })
+ By("specifying forbidden annotations using regex match", func() {
+ svc := NewService("")
+ svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"})
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
+ })
+ })
+
+ It("should fail when updating a Service", func() {
+ cs := ownerClient(tnt.Spec.Owners[0])
+
+ By("specifying forbidden labels using exact match", func() {
+ svc := NewService("forbidden-labels-exact-match")
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ Consistently(func() error {
+ if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil {
+ return nil
+ }
+
+ svc.SetLabels(map[string]string{"foo": "bar"})
+
+ _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{})
+
+ return err
+ }, 10*time.Second, time.Second).ShouldNot(Succeed())
+ })
+ By("specifying forbidden labels using regex match", func() {
+ svc := NewService("forbidden-labels-regex-match")
+
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ Consistently(func() error {
+ if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil {
+ return nil
+ }
+
+ svc.SetLabels(map[string]string{"gatsby-foo": "bar"})
+
+ _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{})
+
+ return err
+ }, 3*time.Second, time.Second).ShouldNot(Succeed())
+ })
+ By("specifying forbidden annotations using exact match", func() {
+ svc := NewService("forbidden-annotations-exact-match")
+
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ rbacPatch(svc.GetName())
+ Consistently(func() error {
+ if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil {
+ return nil
+ }
+
+ svc.SetAnnotations(map[string]string{"foo": "bar"})
+
+ _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{})
+
+ return err
+ }, 10*time.Second, time.Second).ShouldNot(Succeed())
+ })
+ By("specifying forbidden annotations using regex match", func() {
+ svc := NewService("forbidden-annotations-regex-match")
+
+ ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+ rbacPatch(svc.GetName())
+ Consistently(func() error {
+ if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil {
+ return nil
+ }
+
+ svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"})
+
+ _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{})
+
+ return err
+ }, 10*time.Second, time.Second).ShouldNot(Succeed())
+ })
+ })
+})
diff --git a/e2e/utils_test.go b/e2e/utils_test.go
index 4bfcae681..3e5a876ee 100644
--- a/e2e/utils_test.go
+++ b/e2e/utils_test.go
@@ -31,6 +31,26 @@ const (
defaultPollInterval = time.Second
)
+func NewService(name string) *corev1.Service {
+ if len(name) == 0 {
+ name = rand.String(10)
+ }
+
+ return &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ },
+ }
+}
+
+func ServiceCreation(svc *corev1.Service, owner capsulev1beta2.OwnerSpec, timeout time.Duration) AsyncAssertion {
+ cs := ownerClient(owner)
+ return Eventually(func() (err error) {
+ _, err = cs.CoreV1().Service().Create(context.TODO(), svc, metav1.CreateOptions{})
+ return
+ }, timeout, defaultPollInterval)
+}
+
func NewNamespace(name string) *corev1.Namespace {
if len(name) == 0 {
name = rand.String(10)
diff --git a/pkg/api/forbidden_list.go b/pkg/api/forbidden_list.go
index b92a66a40..77de462fd 100644
--- a/pkg/api/forbidden_list.go
+++ b/pkg/api/forbidden_list.go
@@ -4,13 +4,21 @@
package api
import (
+ "fmt"
+ "reflect"
"regexp"
"sort"
"strings"
)
-// +kubebuilder:object:generate=true
+const (
+ // ForbiddenLabelReason used as reason string to deny forbidden labels.
+ ForbiddenLabelReason = "ForbiddenLabel"
+ // ForbiddenAnnotationReason used as reason string to deny forbidden annotations.
+ ForbiddenAnnotationReason = "ForbiddenAnnotation"
+)
+// +kubebuilder:object:generate=true
type ForbiddenListSpec struct {
Exact []string `json:"denied,omitempty"`
Regex string `json:"deniedRegex,omitempty"`
@@ -37,3 +45,57 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
return
}
+
+type ForbiddenError struct {
+ key string
+ spec ForbiddenListSpec
+}
+
+func NewForbiddenError(key string, forbiddenSpec ForbiddenListSpec) error {
+ return &ForbiddenError{
+ key: key,
+ spec: forbiddenSpec,
+ }
+}
+
+//nolint:predeclared
+func (f *ForbiddenError) appendForbiddenError() (append string) {
+ append += "Forbidden are "
+ if len(f.spec.Exact) > 0 {
+ append += fmt.Sprintf("one of the following (%s)", strings.Join(f.spec.Exact, ", "))
+ if len(f.spec.Regex) > 0 {
+ append += " or "
+ }
+ }
+
+ if len(f.spec.Regex) > 0 {
+ append += fmt.Sprintf("matching the regex %s", f.spec.Regex)
+ }
+
+ return
+}
+
+func (f ForbiddenError) Error() string {
+ return fmt.Sprintf("%s is forbidden for the current Tenant. %s", f.key, f.appendForbiddenError())
+}
+
+func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error {
+ if reflect.DeepEqual(ForbiddenListSpec{}, forbiddenList) {
+ return nil
+ }
+
+ for key := range metadata {
+ var forbidden, matched bool
+ forbidden = forbiddenList.ExactMatch(key)
+ matched = forbiddenList.RegexMatch(key)
+
+ if forbidden || matched {
+ return NewForbiddenError(
+ key,
+ forbiddenList,
+ )
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/api/forbidden_list_test.go b/pkg/api/forbidden_list_test.go
index f721e89a3..daefb7d4d 100644
--- a/pkg/api/forbidden_list_test.go
+++ b/pkg/api/forbidden_list_test.go
@@ -72,3 +72,50 @@ func TestForbiddenListSpec_RegexMatch(t *testing.T) {
}
}
}
+
+func TestValidateForbidden(t *testing.T) {
+ type tc struct {
+ Keys map[string]string
+ ForbiddenSpec ForbiddenListSpec
+ HasError bool
+ }
+
+ for _, tc := range []tc{
+ {
+ Keys: map[string]string{"foobar": "", "thesecondkey": "", "anotherkey": ""},
+ ForbiddenSpec: ForbiddenListSpec{
+ Exact: []string{"foobar", "somelabelkey1"},
+ },
+ HasError: true,
+ },
+ {
+ Keys: map[string]string{"foobar": ""},
+ ForbiddenSpec: ForbiddenListSpec{
+ Exact: []string{"foobar.io", "somelabelkey1", "test-exact"},
+ },
+ HasError: false,
+ },
+ {
+ Keys: map[string]string{"foobar": "", "barbaz": ""},
+ ForbiddenSpec: ForbiddenListSpec{
+ Regex: "foo.*",
+ },
+ HasError: true,
+ },
+ {
+ Keys: map[string]string{"foobar": "", "another-annotation-key": ""},
+ ForbiddenSpec: ForbiddenListSpec{
+ Regex: "foo1111",
+ },
+ HasError: false,
+ },
+ } {
+ if tc.HasError {
+ assert.Error(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec))
+ }
+
+ if !tc.HasError {
+ assert.NoError(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec))
+ }
+ }
+}
diff --git a/pkg/api/service_options.go b/pkg/api/service_options.go
index ad77217ac..21127d0fa 100644
--- a/pkg/api/service_options.go
+++ b/pkg/api/service_options.go
@@ -12,4 +12,8 @@ type ServiceOptions struct {
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
+ // Define the labels that a Tenant Owner cannot set for their Service resources.
+ ForbiddenLabels ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
+ // Define the annotations that a Tenant Owner cannot set for their Service resources.
+ ForbiddenAnnotations ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
}
diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go
index 70c8265b9..29cf993b5 100644
--- a/pkg/api/zz_generated.deepcopy.go
+++ b/pkg/api/zz_generated.deepcopy.go
@@ -270,6 +270,8 @@ func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
+ in.ForbiddenLabels.DeepCopyInto(&out.ForbiddenLabels)
+ in.ForbiddenAnnotations.DeepCopyInto(&out.ForbiddenAnnotations)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions.
diff --git a/pkg/webhook/namespace/errors.go b/pkg/webhook/namespace/errors.go
index bdfd0fe06..cfaac497d 100644
--- a/pkg/webhook/namespace/errors.go
+++ b/pkg/webhook/namespace/errors.go
@@ -3,30 +3,6 @@
package namespace
-import (
- "fmt"
- "strings"
-
- capsuleapi "github.com/projectcapsule/capsule/pkg/api"
-)
-
-//nolint:predeclared
-func appendForbiddenError(spec *capsuleapi.ForbiddenListSpec) (append string) {
- append += "Forbidden are "
- if len(spec.Exact) > 0 {
- append += fmt.Sprintf("one of the following (%s)", strings.Join(spec.Exact, ", "))
- if len(spec.Regex) > 0 {
- append += " or "
- }
- }
-
- if len(spec.Regex) > 0 {
- append += fmt.Sprintf("matching the regex %s", spec.Regex)
- }
-
- return
-}
-
type namespaceQuotaExceededError struct{}
func NewNamespaceQuotaExceededError() error {
@@ -36,35 +12,3 @@ func NewNamespaceQuotaExceededError() error {
func (namespaceQuotaExceededError) Error() string {
return "Cannot exceed Namespace quota: please, reach out to the system administrators"
}
-
-type namespaceLabelForbiddenError struct {
- label string
- spec *capsuleapi.ForbiddenListSpec
-}
-
-func NewNamespaceLabelForbiddenError(label string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error {
- return &namespaceLabelForbiddenError{
- label: label,
- spec: forbiddenSpec,
- }
-}
-
-func (f namespaceLabelForbiddenError) Error() string {
- return fmt.Sprintf("Label %s is forbidden for namespaces in the current Tenant. %s", f.label, appendForbiddenError(f.spec))
-}
-
-type namespaceAnnotationForbiddenError struct {
- annotation string
- spec *capsuleapi.ForbiddenListSpec
-}
-
-func NewNamespaceAnnotationForbiddenError(annotation string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error {
- return &namespaceAnnotationForbiddenError{
- annotation: annotation,
- spec: forbiddenSpec,
- }
-}
-
-func (f namespaceAnnotationForbiddenError) Error() string {
- return fmt.Sprintf("Annotation %s is forbidden for namespaces in the current Tenant. %s", f.annotation, appendForbiddenError(f.spec))
-}
diff --git a/pkg/webhook/namespace/user_metadata.go b/pkg/webhook/namespace/user_metadata.go
index e2badedbd..b667567e2 100644
--- a/pkg/webhook/namespace/user_metadata.go
+++ b/pkg/webhook/namespace/user_metadata.go
@@ -5,8 +5,8 @@ package namespace
import (
"context"
- "fmt"
+ "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
@@ -14,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
+ "github.com/projectcapsule/capsule/pkg/api"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
@@ -24,48 +25,6 @@ func UserMetadataHandler() capsulewebhook.Handler {
return &userMetadataHandler{}
}
-func (r *userMetadataHandler) validateUserMetadata(tnt *capsulev1beta2.Tenant, recorder record.EventRecorder, labels map[string]string, annotations map[string]string) *admission.Response {
- if tnt.Spec.NamespaceOptions != nil {
- forbiddenLabels := tnt.Spec.NamespaceOptions.ForbiddenLabels
-
- for label := range labels {
- var forbidden, matched bool
- forbidden = forbiddenLabels.ExactMatch(label)
- matched = forbiddenLabels.RegexMatch(label)
-
- if forbidden || matched {
- recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceLabel", fmt.Sprintf("Label %s is forbidden for a namespaces of the current Tenant ", label))
-
- response := admission.Denied(NewNamespaceLabelForbiddenError(label, &forbiddenLabels).Error())
-
- return &response
- }
- }
- }
-
- if tnt.Spec.NamespaceOptions == nil {
- return nil
- }
-
- forbiddenAnnotations := tnt.Spec.NamespaceOptions.ForbiddenLabels
-
- for annotation := range annotations {
- var forbidden, matched bool
- forbidden = forbiddenAnnotations.ExactMatch(annotation)
- matched = forbiddenAnnotations.RegexMatch(annotation)
-
- if forbidden || matched {
- recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceAnnotation", fmt.Sprintf("Annotation %s is forbidden for a namespaces of the current Tenant ", annotation))
-
- response := admission.Denied(NewNamespaceAnnotationForbiddenError(annotation, &forbiddenAnnotations).Error())
-
- return &response
- }
- }
-
- return nil
-}
-
func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
ns := &corev1.Namespace{}
@@ -81,10 +40,27 @@ func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission.
}
}
- labels := ns.GetLabels()
- annotations := ns.GetAnnotations()
+ if tnt.Spec.NamespaceOptions != nil {
+ err := api.ValidateForbidden(ns.ObjectMeta.Annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations)
+ if err != nil {
+ err = errors.Wrap(err, "namespace annotations validation failed")
+ recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
+ response := admission.Denied(err.Error())
+
+ return &response
+ }
+
+ err = api.ValidateForbidden(ns.ObjectMeta.Labels, tnt.Spec.NamespaceOptions.ForbiddenLabels)
+ if err != nil {
+ err = errors.Wrap(err, "namespace labels validation failed")
+ recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
+ response := admission.Denied(err.Error())
- return r.validateUserMetadata(tnt, recorder, labels, annotations)
+ return &response
+ }
+ }
+
+ return nil
}
}
@@ -173,6 +149,26 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
delete(annotations, key)
}
- return r.validateUserMetadata(tnt, recorder, labels, annotations)
+ if tnt.Spec.NamespaceOptions != nil {
+ err := api.ValidateForbidden(annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations)
+ if err != nil {
+ err = errors.Wrap(err, "namespace annotations validation failed")
+ recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
+ response := admission.Denied(err.Error())
+
+ return &response
+ }
+
+ err = api.ValidateForbidden(labels, tnt.Spec.NamespaceOptions.ForbiddenLabels)
+ if err != nil {
+ err = errors.Wrap(err, "namespace labels validation failed")
+ recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
+ response := admission.Denied(err.Error())
+
+ return &response
+ }
+ }
+
+ return nil
}
}
diff --git a/pkg/webhook/service/validating.go b/pkg/webhook/service/validating.go
index 38868b76c..904aa1db3 100644
--- a/pkg/webhook/service/validating.go
+++ b/pkg/webhook/service/validating.go
@@ -8,6 +8,7 @@ import (
"net"
"strings"
+ "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/client-go/tools/record"
@@ -15,6 +16,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
+ "github.com/projectcapsule/capsule/pkg/api"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
@@ -68,6 +70,26 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
return &response
}
+ if tnt.Spec.ServiceOptions != nil {
+ err := api.ValidateForbidden(svc.Annotations, tnt.Spec.ServiceOptions.ForbiddenAnnotations)
+ if err != nil {
+ err = errors.Wrap(err, "service annotations validation failed")
+ recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
+ response := admission.Denied(err.Error())
+
+ return &response
+ }
+
+ err = api.ValidateForbidden(svc.Labels, tnt.Spec.ServiceOptions.ForbiddenLabels)
+ if err != nil {
+ err = errors.Wrap(err, "service labels validation failed")
+ recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
+ response := admission.Denied(err.Error())
+
+ return &response
+ }
+ }
+
if svc.Spec.ExternalIPs == nil || (tnt.Spec.ServiceOptions == nil || tnt.Spec.ServiceOptions.ExternalServiceIPs == nil) {
return nil
}