mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 20:46:42 +00:00
feat(manager): add forbidden annotations, forbidden labels to service options
Signed-off-by: Siarhei Rasiukevich <s.rasiukevich@gmail.com>
This commit is contained in:
committed by
Dario Tranchitella
parent
8695dfb7a2
commit
b27780d74c
@@ -3133,6 +3133,28 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
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
|
||||
allowedServices:
|
||||
description: Block or deny certain type of Services. Optional.
|
||||
properties:
|
||||
|
||||
@@ -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
|
||||
@@ -3125,6 +3147,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
|
||||
|
||||
@@ -4822,6 +4822,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.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantspecserviceoptionsforbiddenannotations-1">forbiddenAnnotations</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Define the annotations that a Tenant Owner cannot set for their Service resources.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantspecserviceoptionsforbiddenlabels-1">forbiddenLabels</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Define the labels that a Tenant Owner cannot set for their Service resources.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
@@ -4931,6 +4945,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.serviceOptions.forbiddenAnnotations
|
||||
|
||||
|
||||
|
||||
Define the annotations that a Tenant Owner cannot set for their Service resources.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>denied</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>deniedRegex</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.serviceOptions.forbiddenLabels
|
||||
|
||||
|
||||
|
||||
Define the labels that a Tenant Owner cannot set for their Service resources.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>denied</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>deniedRegex</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.storageClasses
|
||||
|
||||
|
||||
@@ -6681,6 +6761,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.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantspecserviceoptionsforbiddenannotations">forbiddenAnnotations</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Define the annotations that a Tenant Owner cannot set for their Service resources.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantspecserviceoptionsforbiddenlabels">forbiddenLabels</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Define the labels that a Tenant Owner cannot set for their Service resources.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
@@ -6790,6 +6884,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.serviceOptions.forbiddenAnnotations
|
||||
|
||||
|
||||
|
||||
Define the annotations that a Tenant Owner cannot set for their Service resources.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>denied</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>deniedRegex</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.serviceOptions.forbiddenLabels
|
||||
|
||||
|
||||
|
||||
Define the labels that a Tenant Owner cannot set for their Service resources.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>denied</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>deniedRegex</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### Tenant.spec.storageClasses
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
//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() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "non-forbidden-labels",
|
||||
})
|
||||
svc.SetLabels(map[string]string{"bim": "baz"})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
})
|
||||
By("specifying non-forbidden annotations", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "non-forbidden-annotations",
|
||||
})
|
||||
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() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-labels-exact",
|
||||
})
|
||||
svc.SetLabels(map[string]string{"foo": "bar"})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
|
||||
})
|
||||
By("specifying forbidden labels using regex match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-labels-regex",
|
||||
})
|
||||
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() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-annotations-exact",
|
||||
})
|
||||
svc.SetAnnotations(map[string]string{"foo": "bar"})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
|
||||
})
|
||||
By("specifying forbidden annotations using regex match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-annotations-regex",
|
||||
})
|
||||
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() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-labels-exact-match",
|
||||
})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
Consistently(func() error {
|
||||
svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
svc.SetLabels(map[string]string{"foo": "bar"})
|
||||
|
||||
_, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
})
|
||||
By("specifying forbidden labels using regex match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-labels-regex-match",
|
||||
})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
Consistently(func() error {
|
||||
svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc.SetLabels(map[string]string{"gatsby-foo": "bar"})
|
||||
|
||||
_, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 3*time.Second, time.Second).ShouldNot(Succeed())
|
||||
})
|
||||
By("specifying forbidden annotations using exact match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-annotations-exact-match",
|
||||
})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
Consistently(func() error {
|
||||
svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc.SetAnnotations(map[string]string{"foo": "bar"})
|
||||
|
||||
_, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
})
|
||||
By("specifying forbidden annotations using regex match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
svc := NewService(types.NamespacedName{
|
||||
Namespace: ns.GetName(),
|
||||
Name: "forbidden-annotations-regex-match",
|
||||
})
|
||||
ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
Consistently(func() error {
|
||||
svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"})
|
||||
|
||||
_, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,28 @@ const (
|
||||
defaultPollInterval = time.Second
|
||||
)
|
||||
|
||||
func NewService(svc types.NamespacedName) *corev1.Service {
|
||||
return &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svc.Name,
|
||||
Namespace: svc.Namespace,
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: []corev1.ServicePort{
|
||||
{Port: int32(80)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ServiceCreation(svc *corev1.Service, owner capsulev1beta2.OwnerSpec, timeout time.Duration) AsyncAssertion {
|
||||
cs := ownerClient(owner)
|
||||
return Eventually(func() (err error) {
|
||||
_, err = cs.CoreV1().Services(svc.Namespace).Create(context.TODO(), svc, metav1.CreateOptions{})
|
||||
return
|
||||
}, timeout, defaultPollInterval)
|
||||
}
|
||||
|
||||
func NewNamespace(name string) *corev1.Namespace {
|
||||
if len(name) == 0 {
|
||||
name = rand.String(10)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -290,6 +290,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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 r.validateUserMetadata(tnt, recorder, labels, annotations)
|
||||
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 &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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user