Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespace labeling for tenant owners #423

Merged
merged 7 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/v1beta1/allowed_list.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

//nolint:dupl
package v1beta1

import (
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/allowed_list_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

//nolint:dupl
package v1beta1

import (
Expand Down
33 changes: 33 additions & 0 deletions api/v1beta1/forbidden_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1

import (
"regexp"
"sort"
"strings"
)

type ForbiddenListSpec struct {
Exact []string `json:"denied,omitempty"`
Regex string `json:"deniedRegex,omitempty"`
}

func (in *ForbiddenListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
})
i := sort.SearchStrings(in.Exact, value)
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
}

func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}
67 changes: 67 additions & 0 deletions api/v1beta1/forbidden_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestForbiddenListSpec_ExactMatch(t *testing.T) {
type tc struct {
In []string
True []string
False []string
}
for _, tc := range []tc{
{
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"bing", "bong"},
},
{
[]string{"one", "two", "three"},
[]string{"one", "two", "three"},
[]string{"a", "b", "c"},
},
{
nil,
nil,
[]string{"any", "value"},
},
} {
a := ForbiddenListSpec{
Exact: tc.In,
}
for _, ok := range tc.True {
assert.True(t, a.ExactMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.ExactMatch(ko))
}
}
}

func TestForbiddenListSpec_RegexMatch(t *testing.T) {
type tc struct {
Regex string
True []string
False []string
}
for _, tc := range []tc{
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
{``, nil, []string{"any", "value"}},
} {
a := ForbiddenListSpec{
Regex: tc.Regex,
}
for _, ok := range tc.True {
assert.True(t, a.RegexMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.RegexMatch(ko))
}
}
}
42 changes: 42 additions & 0 deletions api/v1beta1/namespace_options.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
package v1beta1

import "strings"

type NamespaceOptions struct {
//+kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
}

func (t *Tenant) hasForbiddenNamespaceLabelsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceLabelsAnnotation]; ok {
return true
}
if _, ok := t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
return true
}
return false
}

func (t *Tenant) hasForbiddenNamespaceAnnotationsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsAnnotation]; ok {
return true
}
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
return true
}
return false
}

func (t *Tenant) ForbiddenUserNamespaceLabels() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceLabelsAnnotations() {
return nil
}
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceLabelsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation],
}
}

func (t *Tenant) ForbiddenUserNamespaceAnnotations() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceAnnotationsAnnotations() {
return nil
}
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceAnnotationsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation],
}
}
16 changes: 10 additions & 6 deletions api/v1beta1/tenant_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import (
)

const (
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
ForbiddenNamespaceLabelsAnnotation = "capsule.clastix.io/forbidden-namespace-labels"
ForbiddenNamespaceLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-labels-regexp"
ForbiddenNamespaceAnnotationsAnnotation = "capsule.clastix.io/forbidden-namespace-annotations"
ForbiddenNamespaceAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-annotations-regexp"
)

func UsedQuotaFor(resource fmt.Stringer) string {
Expand Down
20 changes: 20 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions charts/capsule/templates/mutatingwebhookconfiguration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
prometherion marked this conversation as resolved.
Show resolved Hide resolved
resources:
- namespaces
scope: '*'
Expand Down
1 change: 1 addition & 0 deletions config/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None
Expand Down
1 change: 1 addition & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None
Expand Down
2 changes: 1 addition & 1 deletion controllers/rbac/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var (
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"delete"},
Verbs: []string{"delete", "patch"},
},
},
},
Expand Down
46 changes: 38 additions & 8 deletions controllers/tenant/namespaces.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package tenant

import (
Expand Down Expand Up @@ -49,13 +52,23 @@ func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Te

res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
annotations := make(map[string]string)
labels := map[string]string{
"name": namespace,
capsuleLabel: tnt.GetName(),
}

if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations {
annotations[k] = v
}
}

if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels {
labels[k] = v
}
}

if tnt.Spec.NodeSelector != nil {
var selector []string
for k, v := range tnt.Spec.NodeSelector {
Expand Down Expand Up @@ -91,20 +104,37 @@ func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Te
}
}

ns.SetAnnotations(annotations)
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation] = value
}

if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation] = value
}

newLabels := map[string]string{
"name": namespace,
capsuleLabel: tnt.GetName(),
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation] = value
}

if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels {
newLabels[k] = v
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsRegexpAnnotation] = value
}

if ns.Annotations == nil {
ns.SetAnnotations(annotations)
} else {
for k, v := range annotations {
ns.Annotations[k] = v
}
}

ns.SetLabels(newLabels)
if ns.Labels == nil {
ns.SetLabels(labels)
} else {
for k, v := range labels {
ns.Labels[k] = v
}
}

return nil
})
Expand Down
30 changes: 30 additions & 0 deletions docs/operator/use-cases/namespace-labels-and-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Denying user-defined labels or annotations

By default, capsule allows tenant owners to add and modify any label or annotation on their namespaces.

But there are some scenarios, when tenant owners should not have an ability to add or modify specific labels or annotations (for example, this can be labels used in [Kubernetes network policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) which are added by cluster administrator).

Bill, the cluster admin, can deny Alice to add specific labels and annotations on namespaces:

```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
annotations:
capsule.clastix.io/forbidden-namespace-labels: foo.acme.net, bar.acme.net
capsule.clastix.io/forbidden-namespace-labels-regexp: .*.acme.net
capsule.clastix.io/forbidden-namespace-annotations: foo.acme.net, bar.acme.net
capsule.clastix.io/forbidden-namespace-annotations-regexp: .*.acme.net
spec:
owners:
- name: alice
kind: User
EOF
```

# What’s next
This ends our tour in Capsule use cases. As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future.

Stay tuned!
1 change: 1 addition & 0 deletions docs/operator/use-cases/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Use Capsule to address any of the following scenarios:
* [Cordon Tenants](./cordoning-tenant.md)
* [Disable Service Types](./service-type.md)
* [Taint Services](./taint-services.md)
* [Allow adding labels and annotations on namespaces](./namespace-labels-and-annotations.md)
* [Velero Backup Restoration](./velero-backup-restoration.md)

> NB: as we improve Capsule, more use cases about multi-tenancy and cluster governance will be covered.
Expand Down
4 changes: 1 addition & 3 deletions docs/operator/use-cases/taint-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,4 @@ EOF
When Alice creates a service in a namespace, this will inherit the given label and/or annotation.

# What’s next
This ends our tour in Capsule use cases. As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future.

Stay tuned!
See how Bill, the cluster admin, can allow Alice to use specific labels or annotations. [Allow adding labels and annotations on namespaces](./namespace-labels-and-annotations.md).