Compare commits

...

23 Commits

Author SHA1 Message Date
Gonzalo Gabriel Jiménez Fuentes
7a66e8ea93 ci: limit e2e tests to specific paths 2021-09-23 17:57:25 +02:00
Gonzalo Gabriel Jiménez Fuentes
b5eb03ea76 chore: adding auto-generated code 2021-09-23 17:57:25 +02:00
Gonzalo Gabriel Jiménez Fuentes
681b514516 ci: allowing tag creation as trigger to push helm chart 2021-09-23 17:57:25 +02:00
Maksim Fedotov
b28b98a7bc feat: namespace labeling for tenant owners. fix linting issues 2021-09-23 14:10:24 +02:00
Maksim Fedotov
f6bf0ca446 build(installer): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
1081bad7cb docs: namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
79372c7332 build(helm): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
4e8faaf845 build(kustomize): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
d1b008972c test(e2e): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
a14c7609df feat: namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Gonzalo Gabriel Jiménez Fuentes
03456c0b54 fix(ci): allowing tag creation as trigger to push helm chart 2021-09-23 14:01:57 +02:00
Maksim Fedotov
ddfe2219a0 build(helm): update chart version 2021-09-23 11:39:43 +02:00
Maksim Fedotov
6b68363a46 build(helm): additional webhook configuration in chart 2021-09-23 11:39:43 +02:00
alegrey91
357834c5b9 refactor(test): switch from kubernetes version control to NoKindMatchError 2021-09-21 19:14:49 +02:00
Dario Tranchitella
085d9f6503 test(e2e): disabled Ingress wildcard annotation 2021-09-21 19:14:49 +02:00
alegrey91
196e3c910d feat: add deny-wildcard annotation 2021-09-21 19:14:49 +02:00
Bright Zheng
0039c91c23 docs: fix doc minor issues (#425) 2021-09-20 14:35:33 +02:00
Dario Tranchitella
26965a5ea2 fix: skipping indexer if error is a NoKindMatch 2021-09-17 15:43:42 +02:00
Maksim Fedotov
422b6598ba fix: check if user is a member of capsuleUserGroup instead of tenantOwner when cordoning a tenant 2021-09-15 11:14:39 +02:00
Gonzalo Gabriel Jiménez Fuentes
61e6ab4088 fix(hack): jq installation checking 2021-09-13 12:04:49 +02:00
Dario Tranchitella
94c6a64fcb fix: validating Tenant owner name when is a ServiceAccount 2021-09-04 14:17:06 +02:00
Dario Tranchitella
75ebb571e4 fix(chore): ignoring Helm tags 2021-09-01 18:18:07 +02:00
Dario Tranchitella
8f3b3eac29 fix: deleting Pods upon TLS update for HA installations 2021-09-01 18:18:07 +02:00
54 changed files with 1793 additions and 672 deletions

View File

@@ -3,8 +3,26 @@ name: e2e
on:
push:
branches: [ "*" ]
paths:
- '.github/workflows/e2e.yml'
- 'api/*'
- 'controllers/*'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
- 'main.go'
- 'Makefile'
pull_request:
branches: [ "*" ]
paths:
- '.github/workflows/e2e.yml'
- 'api/*'
- 'controllers/*'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
- 'main.go'
- 'Makefile'
jobs:
kind:

View File

@@ -3,8 +3,12 @@ name: Helm Chart
on:
push:
branches: [ "*" ]
tags: [ "helm-v*" ]
pull_request:
branches: [ "*" ]
create:
branches: [ "*" ]
tags: [ "helm-v*" ]
jobs:
lint:

View File

@@ -1,5 +1,5 @@
# Current Operator version
VERSION ?= $$(git describe --abbrev=0 --tags)
VERSION ?= $$(git describe --abbrev=0 --tags --match "v*")
# Default bundle image tag
BUNDLE_IMG ?= quay.io/clastix/capsule:$(VERSION)-bundle

View File

@@ -1,6 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1
import (

View File

@@ -1,6 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1
import (

View File

@@ -0,0 +1,15 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
const (
denyWildcard = "capsule.clastix.io/deny-wildcard"
)
func (t *Tenant) IsWildcardDenied() bool {
if v, ok := t.Annotations[denyWildcard]; ok && v == "true" {
return true
}
return false
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
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.
@@ -7,3 +9,43 @@ type NamespaceOptions struct {
// 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],
}
}

View File

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

View File

@@ -154,6 +154,26 @@ func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ForbiddenListSpec) DeepCopyInto(out *ForbiddenListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForbiddenListSpec.
func (in *ForbiddenListSpec) DeepCopy() *ForbiddenListSpec {
if in == nil {
return nil
}
out := new(ForbiddenListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressOptions) DeepCopyInto(out *IngressOptions) {
*out = *in

View File

@@ -21,7 +21,7 @@ sources:
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0
version: 0.1.1
# This is the version number of the application being deployed.
# This version number should be incremented each time you make changes to the application.

View File

@@ -19,7 +19,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /namespace-owner-reference
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.namespaceOwnerReference.failurePolicy }}
matchPolicy: Equivalent
name: owner.namespace.capsule.clastix.io
namespaceSelector: {}
@@ -32,6 +32,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
scope: '*'

View File

@@ -19,13 +19,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /cordoning
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.cordoning.failurePolicy }}
matchPolicy: Equivalent
name: cordoning.tenant.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.cordoning.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -51,10 +49,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /ingresses
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.ingresses.failurePolicy }}
matchPolicy: Equivalent
name: ingress.capsule.clastix.io
namespaceSelector:
{{- toYaml .Values.webhooks.ingresses.namespaceSelector | nindent 4}}
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
@@ -84,7 +83,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /namespaces
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.namespaces.failurePolicy }}
matchPolicy: Equivalent
name: namespaces.capsule.clastix.io
namespaceSelector: {}
@@ -113,13 +112,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /networkpolicies
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.networkpolicies.failurePolicy }}
matchPolicy: Equivalent
name: networkpolicies.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.networkpolicies.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -144,13 +141,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /pods
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.pods.failurePolicy }}
matchPolicy: Exact
name: pods.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.pods.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -173,12 +168,10 @@ webhooks:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: capsule-system
path: /persistentvolumeclaims
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.persistentvolumeclaims.failurePolicy }}
name: pvc.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.persistentvolumeclaims.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -202,13 +195,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /services
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.services.failurePolicy }}
matchPolicy: Exact
name: services.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.services.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -233,7 +224,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /tenants
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.tenants.failurePolicy }}
matchPolicy: Exact
name: tenants.capsule.clastix.io
namespaceSelector: {}

View File

@@ -42,8 +42,6 @@ jobs:
repository: quay.io/clastix/kubectl
pullPolicy: IfNotPresent
tag: "v1.20.7"
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30
imagePullSecrets: []
serviceAccount:
create: true
@@ -80,3 +78,50 @@ customLabels: {}
# Additional annotations
customAnnotations: {}
# Webhooks configurations
webhooks:
namespaceOwnerReference:
failurePolicy: Fail
cordoning:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
ingresses:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
namespaces:
failurePolicy: Fail
networkpolicies:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
pods:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
persistentvolumeclaims:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
tenants:
failurePolicy: Fail
services:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30

View File

@@ -1411,7 +1411,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/clastix/capsule:v0.1.0
image: quay.io/clastix/capsule:v0.1.1-rc0
imagePullPolicy: IfNotPresent
name: manager
ports:
@@ -1472,6 +1472,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None

View File

@@ -7,4 +7,4 @@ kind: Kustomization
images:
- name: controller
newName: quay.io/clastix/capsule
newTag: v0.1.0
newTag: v0.1.1-rc0

View File

@@ -22,6 +22,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None

View File

@@ -35,7 +35,7 @@ var (
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"delete"},
Verbs: []string{"delete", "patch"},
},
},
},

View File

@@ -9,12 +9,13 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"syscall"
"os"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -112,8 +113,38 @@ func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctr
}
if instance.Name == tlsSecretName && res == controllerutil.OperationResultUpdated {
r.Log.Info("Capsule TLS certificates has been updated, we need to restart the Controller")
_ = syscall.Kill(syscall.Getpid(), syscall.SIGINT)
r.Log.Info("Capsule TLS certificates has been updated, Controller pods must be restarted to load new certificate")
hostname, _ := os.Hostname()
leaderPod := &corev1.Pod{}
if err = r.Client.Get(ctx, types.NamespacedName{Namespace: os.Getenv("NAMESPACE"), Name: hostname}, leaderPod); err != nil {
r.Log.Error(err, "cannot retrieve the leader Pod, probably running in out of the cluster mode")
return reconcile.Result{}, nil
}
podList := &corev1.PodList{}
if err = r.Client.List(ctx, podList, client.MatchingLabels(leaderPod.ObjectMeta.Labels)); err != nil {
r.Log.Error(err, "cannot retrieve list of Capsule pods requiring restart upon TLS update")
return reconcile.Result{}, nil
}
for _, p := range podList.Items {
nonLeaderPod := p
// Skipping this Pod, must be deleted at the end
if nonLeaderPod.GetName() == leaderPod.GetName() {
continue
}
if err = r.Client.Delete(ctx, &nonLeaderPod); err != nil {
r.Log.Error(err, "cannot delete the non-leader Pod due to TLS update")
}
}
if err = r.Client.Delete(ctx, leaderPod); err != nil {
r.Log.Error(err, "cannot delete the leader Pod due to TLS update")
}
}
r.Log.Info("Reconciliation completed, processing back in " + rq.String())

View File

@@ -1,3 +1,6 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tenant
import (
@@ -49,6 +52,10 @@ 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 {
@@ -56,6 +63,12 @@ func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Te
}
}
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 {
@@ -91,20 +104,37 @@ func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Te
}
}
ns.SetAnnotations(annotations)
newLabels := map[string]string{
"name": namespace,
capsuleLabel: tnt.GetName(),
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation] = 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.ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation] = value
}
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation] = value
}
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
})

View File

@@ -36,20 +36,20 @@ subjects:
name: alice
roleRef:
kind: ClusterRole
name: namespace-deleter
name: capsule-namespace-deleter
apiGroup: rbac.authorization.k8s.io
```
So Alice is the admin of the namespaces:
```
kubectl get rolebindings -n oil-production
NAME ROLE AGE
namespace:admin ClusterRole/admin 9m5s
namespace-deleter ClusterRole/admin 9m5s
kubectl get rolebindings -n oil-development
NAME ROLE AGE
namespace:admin ClusterRole/admin 12s
namespace-deleter ClusterRole/capsule-namespace-deleter 12s
```
The said Role Binding resources are automatically created by Capsule controller when Alice creates a namespace in the tenant.
The said Role Binding resources are automatically created by Capsule controller when the tenant owner Alice creates a namespace in the tenant.
Alice can deploy any resource in the namespace, according to the predefined
[`admin` cluster role](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles).

View File

@@ -25,7 +25,7 @@ EOF
Allowed values are: `Always`, `IfNotPresent`, `Never`.
Any attempt of Alice to use a not allowed `imagePullPolicies` value is denied by the Validation Webhook enforcing it.
Any attempt of Alice to use a disallowed `imagePullPolicies` value is denied by the Validation Webhook enforcing it.
# Whats next

View File

@@ -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
```
# Whats 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!

View File

@@ -109,6 +109,7 @@ yes
```
## Assign a robot account as tenant owner
As GitOps methodology is gaining more and more adoption everywhere, it's more likely that an application (Service Account) should act as Tenant Owner. In Capsule, a Tenant can also be owned by a Kubernetes _ServiceAccount_ identity.
The tenant manifest is modified as in the following:
@@ -123,7 +124,6 @@ spec:
owners:
- name: oil-users
kind: Group
owners:
- name: system:serviceaccount:default:robot
kind: ServiceAccount
EOF
@@ -132,7 +132,7 @@ EOF
Bill can create a Service Account called `robot`, for example, in the `default` namespace and leave it to act as Tenant Owner of the `oil` tenant
```
kubectl --as system:serviceaccount:default:robot --as-group capsule.clastix.io auth can-i create namesapces
kubectl --as system:serviceaccount:default:robot --as-group capsule.clastix.io auth can-i create namespaces
yes
```

View File

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

View File

@@ -238,12 +238,15 @@ Alice doesn't have permission to change or delete the resources according to the
```
kubectl -n oil-production auth can-i patch resourcequota
no - no RBAC policy matched
no
kubectl -n oil-production auth can-i delete resourcequota
no
kubectl -n oil-production auth can-i patch limitranges
no - no RBAC policy matched
no
kubectl -n oil-production auth can-i delete limitranges
no
```
# Whats next
See how Bill, the cluster admin, can enforce the PriorityClass of Pods running of Alice's tenant namespaces. [Enforce Pod Priority Classes](./pod-priority-class.md)
See how Bill, the cluster admin, can enforce the PriorityClass of Pods running of Alice's tenant namespaces. [Enforce Pod Priority Classes](./pod-priority-classes.md)

View File

@@ -25,6 +25,4 @@ EOF
When Alice creates a service in a namespace, this will inherit the given label and/or annotation.
# Whats 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).

View File

@@ -0,0 +1,305 @@
//+build e2e
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating an Ingress with a wildcard when it is denied for the Tenant", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "denied-ingress-wildcard",
Annotations: map[string]string{
"capsule.clastix.io/deny-wildcard": "true",
},
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "scott",
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 fail creating an extensions/v1beta1 Ingress with a wildcard hostname", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("extensions-v1beta1")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
ok := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ok",
Namespace: ns.GetName(),
},
Spec: extensionsv1beta1.IngressSpec{
Rules: []extensionsv1beta1.IngressRule{
{
Host: "clastix.io",
IngressRuleValue: extensionsv1beta1.IngressRuleValue{
HTTP: &extensionsv1beta1.HTTPIngressRuleValue{
Paths: []extensionsv1beta1.HTTPIngressPath{
{
Path: "/",
PathType: func(v extensionsv1beta1.PathType) *extensionsv1beta1.PathType {
return &v
}(extensionsv1beta1.PathTypeExact),
Backend: extensionsv1beta1.IngressBackend{
ServiceName: "foo",
ServicePort: intstr.FromInt(8080),
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ok)
}).Should(Succeed())
ko := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ko",
Namespace: ns.GetName(),
},
Spec: extensionsv1beta1.IngressSpec{
Rules: []extensionsv1beta1.IngressRule{
{
Host: "*.clastix.io",
IngressRuleValue: extensionsv1beta1.IngressRuleValue{
HTTP: &extensionsv1beta1.HTTPIngressRuleValue{
Paths: []extensionsv1beta1.HTTPIngressPath{
{
Path: "/",
PathType: func(v extensionsv1beta1.PathType) *extensionsv1beta1.PathType {
return &v
}(extensionsv1beta1.PathTypeExact),
Backend: extensionsv1beta1.IngressBackend{
ServiceName: "foo",
ServicePort: intstr.FromInt(8080),
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ko)
}).ShouldNot(Succeed())
})
It("should fail creating an networking.k8s.io/v1beta1 Ingress with a wildcard hostname", func() {
if err := k8sClient.List(context.Background(), &networkingv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("networking-v1beta1")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
ok := &networkingv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ok",
Namespace: ns.GetName(),
},
Spec: networkingv1beta1.IngressSpec{
Rules: []networkingv1beta1.IngressRule{
{
Host: "clastix.io",
IngressRuleValue: networkingv1beta1.IngressRuleValue{
HTTP: &networkingv1beta1.HTTPIngressRuleValue{
Paths: []networkingv1beta1.HTTPIngressPath{
{
Path: "/",
PathType: func(v networkingv1beta1.PathType) *networkingv1beta1.PathType {
return &v
}(networkingv1beta1.PathTypeExact),
Backend: networkingv1beta1.IngressBackend{
ServiceName: "foo",
ServicePort: intstr.FromInt(8080),
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ok)
}).Should(Succeed())
ko := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ko",
Namespace: ns.GetName(),
},
Spec: extensionsv1beta1.IngressSpec{
Rules: []extensionsv1beta1.IngressRule{
{
Host: "*.clastix.io",
IngressRuleValue: extensionsv1beta1.IngressRuleValue{
HTTP: &extensionsv1beta1.HTTPIngressRuleValue{
Paths: []extensionsv1beta1.HTTPIngressPath{
{
Path: "/",
PathType: func(v extensionsv1beta1.PathType) *extensionsv1beta1.PathType {
return &v
}(extensionsv1beta1.PathTypeExact),
Backend: extensionsv1beta1.IngressBackend{
ServiceName: "foo",
ServicePort: intstr.FromInt(8080),
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ko)
}).ShouldNot(Succeed())
})
It("should fail creating an networking.k8s.io/v1 Ingress with a wildcard hostname", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("networking-v1")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
ok := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ok",
Namespace: ns.GetName(),
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{
{
Host: "clastix.io",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: func(v networkingv1.PathType) *networkingv1.PathType {
return &v
}(networkingv1.PathTypeExact),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "foo",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ok)
}).Should(Succeed())
ko := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-ko",
Namespace: ns.GetName(),
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{
{
Host: "*.clastix.io",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: func(v networkingv1.PathType) *networkingv1.PathType {
return &v
}(networkingv1.PathTypeExact),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "foo",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
},
},
},
},
},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.Background(), ko)
}).ShouldNot(Succeed())
})
})

View File

@@ -7,10 +7,13 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
@@ -53,12 +56,6 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
})
It("should block a non allowed class for extensions/v1beta1", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min >= 22 {
Skip("Running test on Kubernetes " + v + ", extensions/v1beta1 has been deprecated")
}
ns := NewNamespace("ingress-class-disallowed-extensions-v1beta1")
cs := ownerClient(tnt.Spec.Owners[0])
@@ -66,6 +63,13 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("non-specifying at all", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
Eventually(func() (err error) {
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
@@ -83,6 +87,13 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
By("defining as deprecated annotation", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
Eventually(func() (err error) {
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
@@ -103,6 +114,13 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
By("using the ingressClassName", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
Eventually(func() (err error) {
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
@@ -123,12 +141,6 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
})
It("should allow enabled class using the deprecated annotation", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min >= 22 {
Skip("Running test on Kubernetes " + v + ", extensions/v1beta1 has been deprecated")
}
ns := NewNamespace("ingress-class-allowed-annotation-extensions-v1beta1")
cs := ownerClient(tnt.Spec.Owners[0])
@@ -137,6 +149,13 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
for _, c := range tnt.Spec.IngressOptions.AllowedClasses.Exact {
Eventually(func() (err error) {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: c,
@@ -158,13 +177,15 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
})
It("should allow enabled class using the ingressClassName field", func() {
maj, min, v := GetKubernetesSemVer()
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
switch {
case maj == 1 && min < 18:
if maj, min, v := GetKubernetesSemVer(); maj == 1 && min < 18 {
Skip("Running test on Kubernetes " + v + ", doesn't provide .spec.ingressClassName")
case maj == 1 && min >= 22:
Skip("Running test on Kubernetes " + v + ", extensions/v1beta1 has been deprecated")
}
ns := NewNamespace("ingress-class-allowed-annotation-extensions-v1beta1")
@@ -194,12 +215,6 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
})
It("should allow enabled Ingress by regex using the deprecated annotation", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min >= 22 {
Skip("Running test on Kubernetes " + v + ", extensions/v1beta1 has been deprecated")
}
ns := NewNamespace("ingress-class-allowed-annotation-extensions-v1beta1")
cs := ownerClient(tnt.Spec.Owners[0])
ingressClass := "oil-ingress"
@@ -208,6 +223,13 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: ingressClass,
@@ -228,15 +250,6 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
})
It("should allow enabled Ingress by regex using the ingressClassName field", func() {
maj, min, v := GetKubernetesSemVer()
switch {
case maj == 1 && min >= 22:
Skip("Running test on Kubernetes " + v + ", extensions/v1beta1 has been deprecated")
case maj == 1 && min < 18:
Skip("Running test on Kubernetes " + v + ", doesn't provide .spec.ingressClassName")
}
ns := NewNamespace("ingress-class-allowed-annotation-extensions-v1beta1")
cs := ownerClient(tnt.Spec.Owners[0])
ingressClass := "oil-haproxy"
@@ -245,6 +258,17 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
if maj, min, v := GetKubernetesSemVer(); maj == 1 && min < 18 {
Skip("Running test on Kubernetes " + v + ", doesn't provide .spec.ingressClassName")
}
i := &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: ingressClass,
@@ -259,6 +283,6 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1",
}
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), i, metav1.CreateOptions{})
return
}, 600, defaultPollInterval).Should(Succeed())
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})

View File

@@ -7,10 +7,13 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
@@ -52,10 +55,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
})
It("should block a non allowed class", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 19 {
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io/v1")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("ingress-class-disallowed-networking-v1")
@@ -134,10 +138,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
})
It("should allow enabled class using the deprecated annotation for networking.k8s.io/v1", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 19 {
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io/v1")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("ingress-class-allowed-annotation-networking-v1")
@@ -173,10 +178,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
})
It("should allow enabled class using the ingressClassName field", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 19 {
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io/v1")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("ingress-class-allowed-annotation-networking-v1")
@@ -210,10 +216,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
})
It("should allow enabled Ingress by regex using the deprecated annotation", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 19 {
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io/v1")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("ingress-class-allowed-annotation-networking-v1")
@@ -248,10 +255,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
})
It("should allow enabled Ingress by regex using the ingressClassName field", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 19 {
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io/v1")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("ingress-class-allowed-annotation-networking-v1")
@@ -280,6 +288,6 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1"
}
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), i, metav1.CreateOptions{})
return
}, 600, defaultPollInterval).Should(Succeed())
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})

View File

@@ -7,11 +7,14 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -140,82 +143,84 @@ var _ = Describe("when handling Cluster scoped Ingress hostnames collision", fun
})
It("should ensure Cluster scope for Ingress hostname and path collision", func() {
maj, min, _ := GetKubernetesSemVer()
ns1 := NewNamespace("tenant-one-ns")
cs1 := ownerClient(tnt1.Spec.Owners[0])
NamespaceCreation(ns1, tnt1.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt1, defaultTimeoutInterval).Should(ContainElement(ns1.GetName()))
ns2 := NewNamespace("tenant-two-ns")
cs2 := ownerClient(tnt2.Spec.Owners[0])
NamespaceCreation(ns2, tnt2.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt2, defaultTimeoutInterval).Should(ContainElement(ns2.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
_, err = cs1.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/docs")
_, err = cs1.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs2.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/docs")
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
_, err = cs2.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs2.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
return
}).ShouldNot(Succeed())
})
}
_, err = cs2.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
if maj == 1 && min < 22 {
By("testing extensions", func() {
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/foo")
return
}).ShouldNot(Succeed())
})
_, err = cs1.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/bar")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/foo")
_, err = cs2.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs1.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/foo")
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/bar")
_, err = cs2.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs2.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
}
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/foo")
_, err = cs2.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
})
})

View File

@@ -7,11 +7,14 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -116,89 +119,94 @@ var _ = Describe("when disabling Ingress hostnames collision", func() {
})
It("should not check any kind of collision", func() {
maj, min, _ := GetKubernetesSemVer()
ns1 := NewNamespace("namespace-collision-one")
ns2 := NewNamespace("namespace-collision-two")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns1, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns2, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns1.GetName()))
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns2.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-4", "kubernetes.io", "/path")
return
}).Should(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-4", "kubernetes.io", "/path")
return
}).Should(Succeed())
})
}
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
if maj == 1 && min < 22 {
By("testing extensions", func() {
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/docs")
return
}).Should(Succeed())
})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
return
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/docs")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
return
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/docs")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
return
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/docs")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-4", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
return
}).Should(Succeed())
})
}
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-4", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
})
})
})

View File

@@ -7,11 +7,14 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -116,91 +119,96 @@ var _ = Describe("when handling Namespace scoped Ingress hostnames collision", f
})
It("should ensure Namespace scope for Ingress hostname and path collision", func() {
maj, min, _ := GetKubernetesSemVer()
ns1 := NewNamespace("namespace-collision-one")
ns2 := NewNamespace("namespace-collision-two")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns1, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns2, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns1.GetName()))
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns2.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
return
}).Should(Succeed())
// A same Ingress with hostname and path pair can be created in a different Namespace,
// although of the same Tenant
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/path")
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// A same Ingress with hostname and path pair can be created in a different Namespace,
// although of the same Tenant
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/path")
return
}).Should(Succeed())
// ...but a collision occurs if the same pair is created in the same Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but a collision occurs if the same pair is created in the same Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
return
}).ShouldNot(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-4", "kubernetes.io", "/path")
return
}).ShouldNot(Succeed())
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-4", "kubernetes.io", "/path")
return
}).ShouldNot(Succeed())
})
}
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
if maj == 1 && min < 22 {
By("testing extensions", func() {
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/docs")
return
}).ShouldNot(Succeed())
})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
return
}).Should(Succeed())
// A same Ingress with hostname and path pair can be created in a different Namespace,
// although of the same Tenant
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/docs")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but a collision occurs if the same pair is created in the same Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/docs")
return
}).Should(Succeed())
// A same Ingress with hostname and path pair can be created in a different Namespace,
// although of the same Tenant
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
return
}).Should(Succeed())
// ...but a collision occurs if the same pair is created in the same Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/docs")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-4", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
return
}).ShouldNot(Succeed())
})
}
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-4", "cncf.io", "/docs")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
})
})

View File

@@ -7,11 +7,14 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -114,9 +117,7 @@ var _ = Describe("when handling Tenant scoped Ingress hostnames collision", func
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should ensure Tenant scope for Ingress hostname and path collision", func() {
maj, min, _ := GetKubernetesSemVer()
ns1 := NewNamespace("cluster-collision-one")
@@ -129,64 +130,74 @@ var _ = Describe("when handling Tenant scoped Ingress hostnames collision", func
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns1.GetName()))
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns2.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io", "/path")
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/docs")
_, err = cs.NetworkingV1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io", "/docs")
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-3", "kubernetes.io", "/path")
return
}).ShouldNot(Succeed())
})
}
_, err = cs.NetworkingV1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
if maj == 1 && min < 22 {
By("testing extensions", func() {
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/foo")
return
}).ShouldNot(Succeed())
})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/bar")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io", "/foo")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns1.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/foo")
return
}).Should(Succeed())
// Creating a second Ingress with same hostname but a different path in a Namespace managed by the same
// Tenant should not trigger a collision...
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io", "/bar")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
}
return
}).Should(Succeed())
// ...but it happens if hostname and path collide with the first Ingress,
// although in a different Namespace
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-3", "cncf.io", "/foo")
_, err = cs.ExtensionsV1beta1().Ingresses(ns2.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
})
})

View File

@@ -7,12 +7,14 @@ package e2e
import (
"context"
"errors"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -118,152 +120,148 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
})
It("should block a non allowed Hostname", func() {
maj, min, v := GetKubernetesSemVer()
ns := NewNamespace("disallowed-hostname-networking")
cs := ownerClient(tnt.Spec.Owners[0])
if maj == 1 && min > 18 {
ns := NewNamespace("disallowed-hostname-networking")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
By("testing networking.k8s.io", func() {
Eventually(func() (err error) {
obj := networkingIngress("denied-networking", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
return
}
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
Eventually(func() (err error) {
obj := networkingIngress("denied-networking", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
})
It("should block a non allowed Hostname", func() {
maj, min, v := GetKubernetesSemVer()
ns := NewNamespace("disallowed-hostname-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
if maj == 1 && min < 22 {
By("testing extensions", func() {
ns := NewNamespace("disallowed-hostname-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
Eventually(func() (err error) {
obj := extensionsIngress("denied-extensions", "kubernetes.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
})
It("should allow Hostnames in list", func() {
ns := NewNamespace("allowed-hostname-list-networking")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
for i, h := range tnt.Spec.IngressOptions.AllowedHostnames.Exact {
Eventually(func() (err error) {
obj := extensionsIngress("denied-extensions", "kubernetes.io")
obj := networkingIngress(fmt.Sprintf("allowed-networking-%d", i), h)
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
})
It("should allow Hostnames in list", func() {
ns := NewNamespace("allowed-hostname-list-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
for i, h := range tnt.Spec.IngressOptions.AllowedHostnames.Exact {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%d", i), h)
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
return
}
Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
})
It("should allow Hostnames in list", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min > 18 {
ns := NewNamespace("allowed-hostname-list-networking")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
for i, h := range tnt.Spec.IngressOptions.AllowedHostnames.Exact {
Eventually(func() (err error) {
obj := networkingIngress(fmt.Sprintf("allowed-networking-%d", i), h)
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
return
}
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
})
It("should allow Hostnames in list", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 22 {
ns := NewNamespace("allowed-hostname-list-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing extensions", func() {
for i, h := range tnt.Spec.IngressOptions.AllowedHostnames.Exact {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%d", i), h)
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
return
}
Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
})
It("should allow Hostnames in regex", func() {
maj, min, v := GetKubernetesSemVer()
ns := NewNamespace("allowed-hostname-regex-networking")
cs := ownerClient(tnt.Spec.Owners[0])
if maj == 1 && min > 18 {
ns := NewNamespace("allowed-hostname-regex-networking")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := networkingIngress(fmt.Sprintf("allowed-networking-%s", h), fmt.Sprintf("%s.clastix.io", h))
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
By("testing networking.k8s.io", func() {
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
})
}
return
}
Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := networkingIngress(fmt.Sprintf("allowed-networking-%s", h), fmt.Sprintf("%s.clastix.io", h))
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
})
It("should allow Hostnames in regex", func() {
maj, min, v := GetKubernetesSemVer()
ns := NewNamespace("allowed-hostname-regex-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
if maj == 1 && min < 22 {
By("testing extensions", func() {
ns := NewNamespace("allowed-hostname-regex-extensions")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%s", h), fmt.Sprintf("%s.clastix.io", h))
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
By("testing extensions", func() {
if err := k8sClient.List(context.Background(), &extensionsv1beta1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
})
}
}
Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%s", h), fmt.Sprintf("%s.clastix.io", h))
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
})
})

View File

@@ -0,0 +1,84 @@
//+build e2e
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ = Describe("creating a Namespace with user-specified labels and annotations", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-user-metadata-forbidden",
Annotations: map[string]string{
capsulev1beta1.ForbiddenNamespaceLabelsAnnotation: "foo,bar",
capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation: "^gatsby-.*$",
capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation: "foo,bar",
capsulev1beta1.ForbiddenNamespaceAnnotationsRegexpAnnotation: "^gatsby-.*$",
},
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.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("namespace-user-metadata-allowed-labels")
ns.SetLabels(map[string]string{"bim": "baz"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
})
By("specifying non-forbidden annotations", func() {
ns := NewNamespace("namespace-user-metadata-allowed-annotations")
ns.SetAnnotations(map[string]string{"bim": "baz"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
})
})
It("should fail", func() {
By("specifying forbidden labels using exact match", func() {
ns := NewNamespace("namespace-user-metadata-forbidden-labels")
ns.SetLabels(map[string]string{"foo": "bar"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
By("specifying forbidden labels using regex match", func() {
ns := NewNamespace("namespace-user-metadata-forbidden-labels")
ns.SetLabels(map[string]string{"gatsby-foo": "bar"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
By("specifying forbidden annotations using exact match", func() {
ns := NewNamespace("namespace-user-metadata-forbidden-labels")
ns.SetAnnotations(map[string]string{"foo": "bar"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
By("specifying forbidden annotations using regex match", func() {
ns := NewNamespace("namespace-user-metadata-forbidden-labels")
ns.SetAnnotations(map[string]string{"gatsby-foo": "bar"})
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
})
})

View File

@@ -7,12 +7,16 @@ package e2e
import (
"context"
"errors"
"fmt"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -213,8 +217,11 @@ var _ = Describe("adding metadata to Service objects", func() {
})
It("should apply them to EndpointSlice", func() {
if maj, min, v := GetKubernetesSemVer(); maj == 1 && min <= 16 {
Skip("Running test on Kubernetes " + v + ", doesn't provide EndpointSlice resource")
if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
ns := NewNamespace("endpointslice-metadata")

View File

@@ -22,6 +22,12 @@ if [[ ! -x "$(command -v kubectl)" ]]; then
exit 1
fi
# Check if jq is installed
if [[ ! -x "$(command -v jq)" ]]; then
echo "Error: jq not found"
exit 1
fi
USER=$1
TENANT=$2
GROUP=$3

View File

@@ -149,12 +149,12 @@ func main() {
webhooksList := append(
make([]webhook.Webhook, 0),
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass()),
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg))),
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg)),
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg), namespacewebhook.UserMetadataHandler())),
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
route.PVC(pvc.Handler()),
route.Service(service.Handler()),
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
route.Tenant(tenant.NameHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter()),
route.Tenant(tenant.NameHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler()),
route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
route.Cordoning(tenant.CordoningHandler(cfg)),
)
@@ -224,7 +224,7 @@ func main() {
ctx := ctrl.SetupSignalHandler()
if err = indexer.AddToManager(ctx, manager); err != nil {
if err = indexer.AddToManager(ctx, setupLog, manager); err != nil {
setupLog.Error(err, "unable to setup indexers")
os.Exit(1)
}

View File

@@ -5,17 +5,20 @@ package indexer
import (
"context"
"fmt"
"github.com/go-logr/logr"
"github.com/pkg/errors"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"github.com/clastix/capsule/pkg/indexer/ingress"
"github.com/clastix/capsule/pkg/indexer/namespace"
"github.com/clastix/capsule/pkg/indexer/tenant"
"github.com/clastix/capsule/pkg/webhook/utils"
)
type CustomIndexer interface {
@@ -24,26 +27,24 @@ type CustomIndexer interface {
Func() client.IndexerFunc
}
func AddToManager(ctx context.Context, mgr manager.Manager) error {
indexers := append([]CustomIndexer{},
func AddToManager(ctx context.Context, log logr.Logger, mgr manager.Manager) error {
indexers := []CustomIndexer{
tenant.NamespacesReference{},
tenant.OwnerReference{},
namespace.OwnerReference{},
)
majorVer, minorVer, _, _ := utils.GetK8sVersion()
if majorVer == 1 && minorVer < 22 {
indexers = append(indexers,
ingress.HostnamePath{Obj: &extensionsv1beta1.Ingress{}},
ingress.HostnamePath{Obj: &networkingv1beta1.Ingress{}},
)
}
if majorVer == 1 && minorVer >= 19 {
indexers = append(indexers, ingress.HostnamePath{Obj: &networkingv1.Ingress{}})
ingress.HostnamePath{Obj: &extensionsv1beta1.Ingress{}},
ingress.HostnamePath{Obj: &networkingv1beta1.Ingress{}},
ingress.HostnamePath{Obj: &networkingv1.Ingress{}},
}
for _, f := range indexers {
if err := mgr.GetFieldIndexer().IndexField(ctx, f.Object(), f.Field(), f.Func()); err != nil {
missingAPIError := &meta.NoKindMatchError{}
if errors.As(err, &missingAPIError) {
log.Info(fmt.Sprintf("skipping setup of Indexer %T for object %T", f, f.Object()), "error", err.Error())
continue
}
return err
}
}

View File

@@ -0,0 +1,82 @@
package ingress
import (
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/pkg/webhook/utils"
)
type wildcard struct {
}
func Wildcard() capsulewebhook.Handler {
return &wildcard{}
}
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)
}
}
func (h *wildcard) 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 *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)
}
}
func (h *wildcard) wildcardHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder, decoder *admission.Decoder) *admission.Response {
tntList := &capsulev1beta1.TenantList{}
if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", req.Namespace),
}); err != nil {
return utils.ErroredResponse(err)
}
// resource is not inside a Tenant namespace
if len(tntList.Items) == 0 {
return nil
}
tnt := tntList.Items[0]
// Check if Annotation in manifest has value "capsule.clastix.io/deny-wildcard" set to "true".
if tnt.IsWildcardDenied() {
// Retrieve ingress resource from request.
ingress, err := ingressFromRequest(req, decoder)
if err != nil {
return utils.ErroredResponse(err)
}
// Loop over all the hosts present on the ingress.
for host := range ingress.HostnamePathsPairs() {
// Check if one of the host has wildcard.
if strings.HasPrefix(host, "*") {
// In case of wildcard, generate an event and then return.
recorder.Eventf(&tnt, corev1.EventTypeWarning, "Wildcard denied", "%s %s/%s cannot be %s", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
response := admission.Denied(fmt.Sprintf("Wildcard denied for tenant %s\n", tnt.GetName()))
return &response
}
}
}
return nil
}

View File

@@ -3,6 +3,27 @@
package namespace
import (
"fmt"
"strings"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
func appendForbiddenError(spec *capsulev1beta1.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 {
@@ -12,3 +33,35 @@ 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 *capsulev1beta1.ForbiddenListSpec
}
func NewNamespaceLabelForbiddenError(label string, forbiddenSpec *capsulev1beta1.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 *capsulev1beta1.ForbiddenListSpec
}
func NewNamespaceAnnotationForbiddenError(annotation string, forbiddenSpec *capsulev1beta1.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))
}

View File

@@ -69,7 +69,7 @@ func (r *freezedHandler) OnDelete(c client.Client, _ *admission.Decoder, recorde
tnt := tntList.Items[0]
if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) {
if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name)
response := admission.Denied("the selected Tenant is freezed")
@@ -101,7 +101,7 @@ func (r *freezedHandler) OnUpdate(c client.Client, decoder *admission.Decoder, r
tnt := tntList.Items[0]
if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) {
if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName())
response := admission.Denied("the selected Tenant is freezed")

View File

@@ -0,0 +1,138 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package namespace
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"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"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/pkg/webhook/utils"
)
type userMetadataHandler struct {
}
func UserMetadataHandler() capsulewebhook.Handler {
return &userMetadataHandler{}
}
func (r *userMetadataHandler) validateUserMetadata(tnt *capsulev1beta1.Tenant, recorder record.EventRecorder, labels map[string]string, annotations map[string]string) *admission.Response {
if tnt.ForbiddenUserNamespaceLabels() != nil {
forbiddenLabels := tnt.ForbiddenUserNamespaceLabels()
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.ForbiddenUserNamespaceAnnotations() != nil {
forbiddenAnnotations := tnt.ForbiddenUserNamespaceLabels()
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{}
if err := decoder.Decode(req, ns); err != nil {
return utils.ErroredResponse(err)
}
tnt := &capsulev1beta1.Tenant{}
for _, objectRef := range ns.ObjectMeta.OwnerReferences {
// retrieving the selected Tenant
if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil {
return utils.ErroredResponse(err)
}
}
labels := ns.GetLabels()
annotations := ns.GetAnnotations()
return r.validateUserMetadata(tnt, recorder, labels, annotations)
}
}
func (r *userMetadataHandler) 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 (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
oldNs := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
return utils.ErroredResponse(err)
}
newNs := &corev1.Namespace{}
if err := decoder.Decode(req, newNs); err != nil {
return utils.ErroredResponse(err)
}
tnt := &capsulev1beta1.Tenant{}
for _, objectRef := range newNs.ObjectMeta.OwnerReferences {
// retrieving the selected Tenant
if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil {
return utils.ErroredResponse(err)
}
}
var labels, annotations map[string]string
for key, value := range newNs.GetLabels() {
if _, ok := oldNs.GetLabels()[key]; !ok {
if labels == nil {
labels = make(map[string]string)
}
labels[key] = value
}
}
for key, value := range newNs.GetAnnotations() {
if _, ok := oldNs.GetAnnotations()[key]; !ok {
if annotations == nil {
annotations = make(map[string]string)
}
annotations[key] = value
}
}
return r.validateUserMetadata(tnt, recorder, labels, annotations)
}
}

View File

@@ -11,7 +11,6 @@ import (
"sort"
"strings"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
@@ -20,6 +19,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/webhook/utils"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/pkg/configuration"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
@@ -35,119 +36,9 @@ func Handler(cfg configuration.Configuration) capsulewebhook.Handler {
}
}
func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
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 {
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
ln, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
// If we already had TenantName label on NS -> assign to it
if label, ok := ns.ObjectMeta.Labels[ln]; ok {
// retrieving the selected Tenant
tnt := &capsulev1beta1.Tenant{}
if err = clt.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
// Tenant owner must adhere to user that asked for NS creation
if !h.isTenantOwner(tnt.Spec.Owners, req.UserInfo) {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
return &response
}
// Patching the response
response := h.patchResponseForOwnerRef(tnt, ns, recorder)
return &response
}
// If we forceTenantPrefix -> find Tenant from NS name
var tenants sortedTenants
// Find tenants belonging to user (it can be regular user or ServiceAccount)
if strings.HasPrefix(req.UserInfo.Username, "system:serviceaccount:") {
var tntList *capsulev1beta1.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "ServiceAccount", req.UserInfo.Username, clt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
} else {
var tntList *capsulev1beta1.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
// Find tenants belonging to user groups
{
for _, group := range req.UserInfo.Groups {
tntList, err := h.listTenantsForOwnerKind(ctx, "Group", group, clt)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
}
sort.Sort(sort.Reverse(tenants))
if len(tenants) == 0 {
response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
return &response
}
if len(tenants) == 1 {
response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
return &response
}
if h.cfg.ForceTenantPrefix() {
for _, tnt := range tenants {
if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
return &response
}
}
response := admission.Denied("The Namespace prefix used doesn't match any available Tenant")
return &response
}
response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
return &response
return h.setOwnerRef(ctx, req, client, decoder, recorder)
}
}
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
@@ -158,10 +49,124 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
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 nil
return h.setOwnerRef(ctx, req, client, decoder, recorder)
}
}
func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) *admission.Response {
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
ln, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
// If we already had TenantName label on NS -> assign to it
if label, ok := ns.ObjectMeta.Labels[ln]; ok {
// retrieving the selected Tenant
tnt := &capsulev1beta1.Tenant{}
if err = client.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
// Tenant owner must adhere to user that asked for NS creation
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
return &response
}
// Patching the response
response := h.patchResponseForOwnerRef(tnt, ns, recorder)
return &response
}
// If we forceTenantPrefix -> find Tenant from NS name
var tenants sortedTenants
// Find tenants belonging to user (it can be regular user or ServiceAccount)
if strings.HasPrefix(req.UserInfo.Username, "system:serviceaccount:") {
var tntList *capsulev1beta1.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "ServiceAccount", req.UserInfo.Username, client); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
} else {
var tntList *capsulev1beta1.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, client); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
// Find tenants belonging to user groups
{
for _, group := range req.UserInfo.Groups {
tntList, err := h.listTenantsForOwnerKind(ctx, "Group", group, client)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
}
sort.Sort(sort.Reverse(tenants))
if len(tenants) == 0 {
response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
return &response
}
if len(tenants) == 1 {
response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
return &response
}
if h.cfg.ForceTenantPrefix() {
for _, tnt := range tenants {
if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
return &response
}
}
response := admission.Denied("The Namespace prefix used doesn't match any available Tenant")
return &response
}
response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
return &response
}
func (h *handler) patchResponseForOwnerRef(tenant *capsulev1beta1.Tenant, ns *corev1.Namespace, recorder record.EventRecorder) admission.Response {
scheme := runtime.NewScheme()
_ = capsulev1beta1.AddToScheme(scheme)
@@ -189,25 +194,6 @@ func (h *handler) listTenantsForOwnerKind(ctx context.Context, ownerKind string,
return tntList, err
}
func (h *handler) isTenantOwner(owners capsulev1beta1.OwnerListSpec, userInfo authenticationv1.UserInfo) bool {
for _, owner := range owners {
switch owner.Kind {
case "User", "ServiceAccount":
if userInfo.Username == owner.Name {
return true
}
case "Group":
for _, group := range userInfo.Groups {
if group == owner.Name {
return true
}
}
}
}
return false
}
type sortedTenants []capsulev1beta1.Tenant
func (s sortedTenants) Len() int {

View File

@@ -4,7 +4,7 @@ import (
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
)
// +kubebuilder:webhook:path=/namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io
// +kubebuilder:webhook:path=/namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create;update,versions=v1,name=owner.namespace.capsule.clastix.io
type webhook struct {
handlers []capsulewebhook.Handler

View File

@@ -44,15 +44,12 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
}
tnt := tntList.Items[0]
if tnt.IsCordoned() && utils.IsCapsuleUser(req, h.configuration.UserGroups()) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
if tnt.IsCordoned() {
if utils.RequestFromOwnerOrSA(tnt, req, h.configuration.UserGroups()) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
return &response
}
return &response
}
return nil

View File

@@ -0,0 +1,66 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tenant
import (
"context"
"fmt"
"regexp"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/pkg/webhook/utils"
)
type saNameHandler struct {
}
func ServiceAccountNameHandler() capsulewebhook.Handler {
return &saNameHandler{}
}
func (h *saNameHandler) validateServiceAccountName(req admission.Request, decoder *admission.Decoder) *admission.Response {
tenant := &capsulev1beta1.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {
return utils.ErroredResponse(err)
}
compiler := regexp.MustCompile(`^.*:.*:.*(:.*)?$`)
for _, owner := range tenant.Spec.Owners {
if owner.Kind != "ServiceAccount" {
continue
}
if !compiler.MatchString(owner.Name) {
response := admission.Denied(fmt.Sprintf("owner name %s is not a valid Service Account name ", owner.Name))
return &response
}
}
return nil
}
func (h *saNameHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.validateServiceAccountName(req, decoder)
}
}
func (h *saNameHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
}
}
func (h *saNameHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.validateServiceAccountName(req, decoder)
}
}

View File

@@ -11,7 +11,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/configuration"
"github.com/clastix/capsule/pkg/utils"
"github.com/clastix/capsule/pkg/webhook"
)
@@ -27,26 +26,9 @@ type handler struct {
handlers []webhook.Handler
}
// If the user performing action is not a Capsule user, can be skipped
func (h handler) isCapsuleUser(req admission.Request) bool {
groupList := utils.NewUserGroupList(req.UserInfo.Groups)
// if the user is a ServiceAccount belonging to the kube-system namespace, definitely, it's not a Capsule user
// and we can skip the check in case of Capsule user group assigned to system:authenticated
// (ref: https://github.com/clastix/capsule/issues/234)
if groupList.Find("system:serviceaccounts:kube-system") {
return false
}
for _, group := range h.configuration.UserGroups() {
if groupList.Find(group) {
return true
}
}
return false
}
func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
if !h.isCapsuleUser(req) {
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
return nil
}
@@ -62,7 +44,7 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, rec
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
if !h.isCapsuleUser(req) {
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
return nil
}
@@ -78,7 +60,7 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
if !h.isCapsuleUser(req) {
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
return nil
}

View File

@@ -0,0 +1,23 @@
package utils
import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/pkg/utils"
)
func IsCapsuleUser(req admission.Request, userGroups []string) bool {
groupList := utils.NewUserGroupList(req.UserInfo.Groups)
// if the user is a ServiceAccount belonging to the kube-system namespace, definitely, it's not a Capsule user
// and we can skip the check in case of Capsule user group assigned to system:authenticated
// (ref: https://github.com/clastix/capsule/issues/234)
if groupList.Find("system:serviceaccounts:kube-system") {
return false
}
for _, group := range userGroups {
if groupList.Find(group) {
return true
}
}
return false
}

View File

@@ -1,32 +0,0 @@
package utils
import (
"strings"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/pkg/utils"
)
func RequestFromOwnerOrSA(tenant capsulev1beta1.Tenant, req admission.Request, userGroups []string) bool {
for _, owner := range tenant.Spec.Owners {
switch {
case (owner.Kind == "User" || owner.Kind == "ServiceAccount") && req.UserInfo.Username == owner.Name:
return true
case owner.Kind == "Group":
groupList := utils.NewUserGroupList(req.UserInfo.Groups)
for _, group := range userGroups {
if groupList.Find(group) {
return true
}
}
}
}
for _, group := range req.UserInfo.Groups {
if len(req.Namespace) > 0 && strings.HasPrefix(group, "system:serviceaccounts:"+req.Namespace) {
return true
}
}
return false
}

View File

@@ -0,0 +1,26 @@
package utils
import (
authenticationv1 "k8s.io/api/authentication/v1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
func IsTenantOwner(owners capsulev1beta1.OwnerListSpec, userInfo authenticationv1.UserInfo) bool {
for _, owner := range owners {
switch owner.Kind {
case "User", "ServiceAccount":
if userInfo.Username == owner.Name {
return true
}
case "Group":
for _, group := range userInfo.Groups {
if group == owner.Name {
return true
}
}
}
}
return false
}