mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-17 17:10:39 +00:00
Compare commits
23 Commits
helm-v0.1.
...
helm-v0.1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a66e8ea93 | ||
|
|
b5eb03ea76 | ||
|
|
681b514516 | ||
|
|
b28b98a7bc | ||
|
|
f6bf0ca446 | ||
|
|
1081bad7cb | ||
|
|
79372c7332 | ||
|
|
4e8faaf845 | ||
|
|
d1b008972c | ||
|
|
a14c7609df | ||
|
|
03456c0b54 | ||
|
|
ddfe2219a0 | ||
|
|
6b68363a46 | ||
|
|
357834c5b9 | ||
|
|
085d9f6503 | ||
|
|
196e3c910d | ||
|
|
0039c91c23 | ||
|
|
26965a5ea2 | ||
|
|
422b6598ba | ||
|
|
61e6ab4088 | ||
|
|
94c6a64fcb | ||
|
|
75ebb571e4 | ||
|
|
8f3b3eac29 |
18
.github/workflows/e2e.yml
vendored
18
.github/workflows/e2e.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/helm.yml
vendored
4
.github/workflows/helm.yml
vendored
@@ -3,8 +3,12 @@ name: Helm Chart
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
tags: [ "helm-v*" ]
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
create:
|
||||
branches: [ "*" ]
|
||||
tags: [ "helm-v*" ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
|
||||
15
api/v1beta1/deny_wildcard.go
Normal file
15
api/v1beta1/deny_wildcard.go
Normal 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
|
||||
}
|
||||
33
api/v1beta1/forbidden_list.go
Normal file
33
api/v1beta1/forbidden_list.go
Normal 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
|
||||
}
|
||||
67
api/v1beta1/forbidden_list_test.go
Normal file
67
api/v1beta1/forbidden_list_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: '*'
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,4 +7,4 @@ kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: quay.io/clastix/capsule
|
||||
newTag: v0.1.0
|
||||
newTag: v0.1.1-rc0
|
||||
|
||||
@@ -22,6 +22,7 @@ webhooks:
|
||||
- v1
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- namespaces
|
||||
sideEffects: None
|
||||
|
||||
@@ -35,7 +35,7 @@ var (
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"namespaces"},
|
||||
Verbs: []string{"delete"},
|
||||
Verbs: []string{"delete", "patch"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
# What’s next
|
||||
|
||||
|
||||
30
docs/operator/use-cases/namespace-labels-and-annotations.md
Normal file
30
docs/operator/use-cases/namespace-labels-and-annotations.md
Normal 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
|
||||
```
|
||||
|
||||
# What’s next
|
||||
This ends our tour in Capsule use cases. As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future.
|
||||
|
||||
Stay tuned!
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
# What’s 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)
|
||||
|
||||
@@ -25,6 +25,4 @@ EOF
|
||||
When Alice creates a service in a namespace, this will inherit the given label and/or annotation.
|
||||
|
||||
# What’s next
|
||||
This ends our tour in Capsule use cases. As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future.
|
||||
|
||||
Stay tuned!
|
||||
See how Bill, the cluster admin, can allow Alice to use specific labels or annotations. [Allow adding labels and annotations on namespaces](./namespace-labels-and-annotations.md).
|
||||
|
||||
305
e2e/disable_ingress_wildcard_test.go
Normal file
305
e2e/disable_ingress_wildcard_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
84
e2e/namespace_user_metadata_test.go
Normal file
84
e2e/namespace_user_metadata_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
8
main.go
8
main.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
82
pkg/webhook/ingress/validate_wildcard.go
Normal file
82
pkg/webhook/ingress/validate_wildcard.go
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
138
pkg/webhook/namespace/user_metadata.go
Normal file
138
pkg/webhook/namespace/user_metadata.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
66
pkg/webhook/tenant/serviceaccount_format.go
Normal file
66
pkg/webhook/tenant/serviceaccount_format.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
23
pkg/webhook/utils/is_capsule_user.go
Normal file
23
pkg/webhook/utils/is_capsule_user.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
26
pkg/webhook/utils/is_tenant_owner.go
Normal file
26
pkg/webhook/utils/is_tenant_owner.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user