mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-04-05 02:08:05 +00:00
Compare commits
45 Commits
v0.1.0
...
issues/451
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f28a0202 | ||
|
|
fa8e805842 | ||
|
|
8df66fc232 | ||
|
|
c2218912eb | ||
|
|
e361e2d424 | ||
|
|
260b60d263 | ||
|
|
e0d5e6feb2 | ||
|
|
0784dc7177 | ||
|
|
b17c6c4636 | ||
|
|
52cf597041 | ||
|
|
b8dcded882 | ||
|
|
6a175e9017 | ||
|
|
3c609f84db | ||
|
|
7c3a59c4e4 | ||
|
|
d3e3b8a881 | ||
|
|
7a8148bd58 | ||
|
|
405d3ac52d | ||
|
|
f92acf9a9d | ||
|
|
bbb7b850d6 | ||
|
|
0f7284d190 | ||
|
|
7db263b2b6 | ||
|
|
0a8f50f761 | ||
|
|
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:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ bin
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode
|
||||
|
||||
**/*.kubeconfig
|
||||
**/*.crt
|
||||
|
||||
55
Makefile
55
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
|
||||
@@ -45,7 +45,7 @@ manager: generate fmt vet
|
||||
|
||||
# Run against the configured Kubernetes cluster in ~/.kube/config
|
||||
run: generate manifests
|
||||
go run ./main.go
|
||||
go run .
|
||||
|
||||
# Creates the single file to install Capsule without any external dependency
|
||||
installer: manifests kustomize
|
||||
@@ -78,6 +78,57 @@ manifests: controller-gen
|
||||
generate: controller-gen
|
||||
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
|
||||
|
||||
# Setup development env
|
||||
# Usage:
|
||||
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
|
||||
# For example:
|
||||
# LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
||||
define TLS_CNF
|
||||
[ req ]
|
||||
default_bits = 4096
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
[ req_distinguished_name ]
|
||||
countryName = SG
|
||||
stateOrProvinceName = SG
|
||||
localityName = SG
|
||||
organizationName = CAPSULE
|
||||
commonName = CAPSULE
|
||||
[ req_ext ]
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = $(LAPTOP_HOST_IP)
|
||||
endef
|
||||
export TLS_CNF
|
||||
dev-setup:
|
||||
kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
|
||||
mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||
echo "$${TLS_CNF}" > _tls.cnf
|
||||
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
||||
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
|
||||
-extensions req_ext \
|
||||
-config _tls.cnf \
|
||||
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
|
||||
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
|
||||
rm -f _tls.cnf
|
||||
export WEBHOOK_URL="https://$${LAPTOP_HOST_IP}:9443"; \
|
||||
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
|
||||
kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]" && \
|
||||
kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/cordoning\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/ingresses\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespaces\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/networkpolicies\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]";
|
||||
|
||||
# Build the docker image
|
||||
docker-build: test
|
||||
docker build . -t ${IMG} --build-arg GIT_HEAD_COMMIT=$(GIT_HEAD_COMMIT) \
|
||||
|
||||
17
README.md
17
README.md
@@ -150,9 +150,6 @@ Error from server (Forbidden): pods is forbidden:
|
||||
User "bob" cannot list resource "pods" in API group "" in the namespace "kube-system"
|
||||
```
|
||||
|
||||
# Documentation
|
||||
Please, check the project [documentation](./docs/index.md) for more cool things you can do with Capsule.
|
||||
|
||||
# Removal
|
||||
Similar to `deploy`, you can get rid of Capsule using the `remove` target.
|
||||
|
||||
@@ -160,15 +157,21 @@ Similar to `deploy`, you can get rid of Capsule using the `remove` target.
|
||||
$ make remove
|
||||
```
|
||||
|
||||
# Documentation
|
||||
Please, check the project [documentation](./docs/index.md) for more cool things you can do with Capsule.
|
||||
|
||||
# Contribution
|
||||
Capsule is Open Source with Apache 2 license and any contribution is welcome.
|
||||
|
||||
Please refer to the corresponding docs:
|
||||
- [contributing.md](./docs/contributing.md) for the general guide; and
|
||||
- [dev-guide.md](./docs/dev-guide.md) for how to set up the development env to get started.
|
||||
|
||||
# FAQ
|
||||
- Q. How to pronounce Capsule?
|
||||
|
||||
A. It should be pronounced as `/ˈkæpsjuːl/`.
|
||||
|
||||
- Q. Can I contribute?
|
||||
|
||||
A. Absolutely! Capsule is Open Source with Apache 2 license and any contribution is welcome. Please refer to the corresponding [section](./docs/operator/contributing.md) in the documentation.
|
||||
|
||||
- Q. Is it production grade?
|
||||
|
||||
A. Although under frequent development and improvements, Capsule is ready to be used in production environments as currently, people are using it in public and private deployments. Check out the [release](https://github.com/clastix/capsule/releases) page for a detailed list of available versions.
|
||||
|
||||
@@ -200,17 +200,17 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
}
|
||||
}
|
||||
if len(t.Spec.NetworkPolicies) > 0 {
|
||||
dst.Spec.NetworkPolicies = &capsulev1beta1.NetworkPolicySpec{
|
||||
dst.Spec.NetworkPolicies = capsulev1beta1.NetworkPolicySpec{
|
||||
Items: t.Spec.NetworkPolicies,
|
||||
}
|
||||
}
|
||||
if len(t.Spec.LimitRanges) > 0 {
|
||||
dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{
|
||||
dst.Spec.LimitRanges = capsulev1beta1.LimitRangesSpec{
|
||||
Items: t.Spec.LimitRanges,
|
||||
}
|
||||
}
|
||||
if len(t.Spec.ResourceQuota) > 0 {
|
||||
dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{
|
||||
dst.Spec.ResourceQuota = capsulev1beta1.ResourceQuotaSpec{
|
||||
Scope: func() capsulev1beta1.ResourceQuotaScope {
|
||||
if v, ok := t.GetAnnotations()[resourceQuotaScopeAnnotation]; ok {
|
||||
switch v {
|
||||
@@ -500,13 +500,13 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
Regex: src.Spec.ContainerRegistries.Regex,
|
||||
}
|
||||
}
|
||||
if src.Spec.NetworkPolicies != nil {
|
||||
if len(src.Spec.NetworkPolicies.Items) > 0 {
|
||||
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
|
||||
}
|
||||
if src.Spec.LimitRanges != nil {
|
||||
if len(src.Spec.LimitRanges.Items) > 0 {
|
||||
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
|
||||
}
|
||||
if src.Spec.ResourceQuota != nil {
|
||||
if len(src.Spec.ResourceQuota.Items) > 0 {
|
||||
t.Annotations[resourceQuotaScopeAnnotation] = string(src.Spec.ResourceQuota.Scope)
|
||||
t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
|
||||
}
|
||||
|
||||
@@ -240,13 +240,13 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
ContainerRegistries: v1beta1AllowedListSpec,
|
||||
NodeSelector: nodeSelector,
|
||||
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{
|
||||
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{
|
||||
Items: networkPolicies,
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{
|
||||
Items: limitRanges,
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{
|
||||
Scope: capsulev1beta1.ResourceQuotaScopeNamespace,
|
||||
Items: resourceQuotas,
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -24,11 +24,11 @@ type TenantSpec struct {
|
||||
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
NetworkPolicies *NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
NetworkPolicies NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
LimitRanges *LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
LimitRanges LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
|
||||
ResourceQuota *ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
ResourceQuota ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
|
||||
|
||||
@@ -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
|
||||
@@ -460,21 +480,9 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.NetworkPolicies != nil {
|
||||
in, out := &in.NetworkPolicies, &out.NetworkPolicies
|
||||
*out = new(NetworkPolicySpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.LimitRanges != nil {
|
||||
in, out := &in.LimitRanges, &out.LimitRanges
|
||||
*out = new(LimitRangesSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ResourceQuota != nil {
|
||||
in, out := &in.ResourceQuota, &out.ResourceQuota
|
||||
*out = new(ResourceQuotaSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
|
||||
in.LimitRanges.DeepCopyInto(&out.LimitRanges)
|
||||
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
|
||||
if in.AdditionalRoleBindings != nil {
|
||||
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
|
||||
*out = make([]AdditionalRoleBindingsSpec, len(*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.3
|
||||
|
||||
# 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,13 +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:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
{{- toYaml .Values.webhooks.ingresses.namespaceSelector | nindent 4}}
|
||||
objectSelector: {}
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -84,7 +80,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 +109,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 +138,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:
|
||||
@@ -171,14 +163,12 @@ webhooks:
|
||||
caBundle: Cg==
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: capsule-system
|
||||
namespace: {{ .Release.Namespace }}
|
||||
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 +192,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 +221,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())
|
||||
|
||||
@@ -68,28 +68,22 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
return
|
||||
}
|
||||
|
||||
if instance.Spec.NetworkPolicies != nil {
|
||||
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies.Items))
|
||||
if err = r.syncNetworkPolicies(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
return
|
||||
}
|
||||
r.Log.Info("Starting processing of Network Policies")
|
||||
if err = r.syncNetworkPolicies(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
return
|
||||
}
|
||||
|
||||
if instance.Spec.LimitRanges != nil {
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
if err = r.syncLimitRanges(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
return
|
||||
}
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
if err = r.syncLimitRanges(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
return
|
||||
}
|
||||
|
||||
if instance.Spec.ResourceQuota != nil {
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
if err = r.syncResourceQuotas(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
return
|
||||
}
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
if err = r.syncResourceQuotas(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
return
|
||||
}
|
||||
|
||||
r.Log.Info("Ensuring additional RoleBindings for owner")
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -121,7 +121,7 @@ func (r *Manager) ownerRoleBinding(tenant *capsulev1beta1.Tenant) error {
|
||||
newLabels := map[string]string{tl: tenant.Name}
|
||||
|
||||
for _, owner := range tenant.Spec.Owners {
|
||||
if owner.Kind == "ServiceAccount" {
|
||||
if owner.Kind == rbacv1.ServiceAccountKind {
|
||||
splitName := strings.Split(owner.Name, ":")
|
||||
subjects = append(subjects, rbacv1.Subject{
|
||||
Kind: owner.Kind.String(),
|
||||
|
||||
BIN
docs/assets/dev-env.png
Normal file
BIN
docs/assets/dev-env.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
63
docs/contributing.md
Normal file
63
docs/contributing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# How to contribute to Capsule
|
||||
|
||||
First, thanks for your interest in Capsule, any contribution is welcome!
|
||||
|
||||
## Development environment setup
|
||||
|
||||
The first step is to set up your local development environment.
|
||||
|
||||
Please follow the [Capsule Development Guide](dev-guide.md) for details.
|
||||
|
||||
## Code convention
|
||||
|
||||
The changes must follow the Pull Request method where a _GitHub Action_ will
|
||||
check the `golangci-lint`, so ensure your changes respect the coding standard.
|
||||
|
||||
### golint
|
||||
|
||||
You can easily check them issuing the _Make_ recipe `golint`.
|
||||
|
||||
```
|
||||
# make golint
|
||||
golangci-lint run -c .golangci.yml
|
||||
```
|
||||
|
||||
> Enabled linters and related options are defined in the [.golanci.yml file](../../.golangci.yml)
|
||||
|
||||
### goimports
|
||||
|
||||
Also, the Go import statements must be sorted following the best practice:
|
||||
|
||||
```
|
||||
<STANDARD LIBRARY>
|
||||
|
||||
<EXTERNAL PACKAGES>
|
||||
|
||||
<LOCAL PACKAGES>
|
||||
```
|
||||
|
||||
To help you out you can use the _Make_ recipe `goimports`
|
||||
|
||||
```
|
||||
# make goimports
|
||||
goimports -w -l -local "github.com/clastix/capsule" .
|
||||
```
|
||||
|
||||
### Commits
|
||||
|
||||
All the Pull Requests must refer to an already open issue: this is the first phase to contribute also for informing maintainers about the issue.
|
||||
|
||||
Commit's first line should not exceed 50 columns.
|
||||
|
||||
A commit description is welcomed to explain more the changes: just ensure
|
||||
to put a blank line and an arbitrary number of maximum 72 characters long
|
||||
lines, at most one blank line between them.
|
||||
|
||||
Please, split changes into several and documented small commits: this will help us to perform a better review. Commits must follow the Conventional Commits Specification, a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with Semantic Versioning, by describing the features, fixes, and breaking changes made in commit messages. See [Conventional Commits Specification](https://www.conventionalcommits.org) to learn about Conventional Commits.
|
||||
|
||||
> In case of errors or need of changes to previous commits,
|
||||
> fix them squashing to make changes atomic.
|
||||
|
||||
### Miscellanea
|
||||
|
||||
Please, add a new single line at end of any file as the current coding style.
|
||||
359
docs/dev-guide.md
Normal file
359
docs/dev-guide.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Capsule Development Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Tools
|
||||
|
||||
Make sure you have these tools installed:
|
||||
|
||||
- [Go 1.16+](https://golang.org/dl/)
|
||||
- [Operator SDK 1.7.2+](https://github.com/operator-framework/operator-sdk), or [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
|
||||
- [KinD](https://github.com/kubernetes-sigs/kind) or [k3d](https://k3d.io/), with `kubectl`
|
||||
- [ngrok](https://ngrok.com/) (if you want to run locally with remote Kubernetes)
|
||||
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
- OpenSSL
|
||||
|
||||
### Kubernetes Cluster
|
||||
|
||||
A lightweight Kubernetes within your laptop can be very handy for Kubernetes-native development like Capsule.
|
||||
|
||||
#### By `k3d`
|
||||
|
||||
```sh
|
||||
# Install K3d cli by brew in Mac, or your preferred way
|
||||
$ brew install k3d
|
||||
|
||||
# Export your laptop's IP, e.g. retrieving it by: ifconfig
|
||||
# Do change this IP to yours
|
||||
$ export LAPTOP_HOST_IP=192.168.10.101
|
||||
|
||||
# Spin up a bare minimum cluster
|
||||
# Refer to here for more options: https://k3d.io/v4.4.8/usage/commands/k3d_cluster_create/
|
||||
$ k3d cluster create k3s-capsule --servers 1 --agents 1 --no-lb --k3s-server-arg --tls-san=${LAPTOP_HOST_IP}
|
||||
|
||||
# This will create a cluster with 1 server and 1 worker node
|
||||
$ kubectl get nodes
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
k3d-k3s-capsule-server-0 Ready control-plane,master 2m13s v1.21.2+k3s1
|
||||
k3d-k3s-capsule-agent-0 Ready <none> 2m3s v1.21.2+k3s1
|
||||
|
||||
# Or 2 Docker containers if you view it from Docker perspective
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
5c26ad840c62 rancher/k3s:v1.21.2-k3s1 "/bin/k3s agent" 53 seconds ago Up 45 seconds k3d-k3s-capsule-agent-0
|
||||
753998879b28 rancher/k3s:v1.21.2-k3s1 "/bin/k3s server --t…" 53 seconds ago Up 51 seconds 0.0.0.0:49708->6443/tcp k3d-k3s-capsule-server-0
|
||||
```
|
||||
|
||||
#### By `kind`
|
||||
|
||||
```sh
|
||||
# # Install kind cli by brew in Mac, or your preferred way
|
||||
$ brew install kind
|
||||
|
||||
# Prepare a kind config file with necessary customization
|
||||
$ cat > kind.yaml <<EOF
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
networking:
|
||||
apiServerAddress: "0.0.0.0"
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: ClusterConfiguration
|
||||
metadata:
|
||||
name: config
|
||||
apiServer:
|
||||
certSANs:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- kubernetes
|
||||
- kubernetes.default.svc
|
||||
- kubernetes.default.svc.cluster.local
|
||||
- kind
|
||||
- 0.0.0.0
|
||||
- ${LAPTOP_HOST_IP}
|
||||
- role: worker
|
||||
EOF
|
||||
|
||||
# Spin up a bare minimum cluster with 1 master 1 worker node
|
||||
$ kind create cluster --name kind-capsule --config kind.yaml
|
||||
|
||||
# This will create a cluster with 1 server and 1 worker node
|
||||
$ kubectl get nodes
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
kind-capsule-control-plane Ready control-plane,master 84s v1.21.1
|
||||
kind-capsule-worker Ready <none> 56s v1.21.1
|
||||
|
||||
# Or 2 Docker containers if you view it from Docker perspective
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
7b329fd3a838 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute 0.0.0.0:54894->6443/tcp kind-capsule-control-plane
|
||||
7d50f1633555 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute kind-capsule-worker
|
||||
```
|
||||
|
||||
## Fork & clone the repository
|
||||
|
||||
The `fork-clone-contribute-pr` flow is common for contributing to OSS projects like Kubernetes, Capsule.
|
||||
|
||||
Let's assume you've forked it into your GitHub namespace, say `myuser`, and then you can clone it with Git protocol.
|
||||
Do remember to change the `myuser` to yours.
|
||||
|
||||
```sh
|
||||
$ git clone git@github.com:myuser/capsule.git && cd capsule
|
||||
```
|
||||
|
||||
It's a good practice to add the upsteam as the remote too so we can easily fetch and merge the upstream to our fork:
|
||||
|
||||
```sh
|
||||
$ git remote add upstream https://github.com/clastix/capsule.git
|
||||
$ git remote -vv
|
||||
origin git@github.com:myuser/capsule.git (fetch)
|
||||
origin git@github.com:myuser/capsule.git (push)
|
||||
upstream https://github.com/clastix/capsule.git (fetch)
|
||||
upstream https://github.com/clastix/capsule.git (push)
|
||||
```
|
||||
|
||||
## Build & deploy Capsule
|
||||
|
||||
```sh
|
||||
# Download the project dependencies
|
||||
$ go mod download
|
||||
|
||||
# Build the Capsule image
|
||||
$ make docker-build
|
||||
|
||||
# Retrieve the built image version
|
||||
$ export CAPSULE_IMAGE_VESION=`docker images --format '{{.Tag}}' quay.io/clastix/capsule`
|
||||
|
||||
# If k3s, load the image into cluster by
|
||||
$ k3d image import --cluster k3s-capsule capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
|
||||
# If Kind, load the image into cluster by
|
||||
$ kind load docker-image --name kind-capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
|
||||
|
||||
# deploy all the required manifests
|
||||
# Note: 1) please retry if you saw errors; 2) if you want to clean it up first, run: make remove
|
||||
$ make deploy
|
||||
|
||||
# Make sure the controller is running
|
||||
$ kubectl get pod -n capsule-system
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
capsule-controller-manager-5c6b8445cf-566dc 1/1 Running 0 23s
|
||||
|
||||
# Check the logs if needed
|
||||
$ kubectl -n capsule-system logs --all-containers -l control-plane=controller-manager
|
||||
|
||||
# You may have a try to deploy a Tenant too to make sure it works end to end
|
||||
$ kubectl apply -f - <<EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
EOF
|
||||
|
||||
# There shouldn't be any errors and you should see the newly created tenant
|
||||
$ kubectl get tenants
|
||||
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
oil Active 0 14s
|
||||
```
|
||||
|
||||
As of now, a complete Capsule environment has been set up in `kind`- or `k3d`-powered cluster, and the `capsule-controller-manager` is running as a deployment serving as:
|
||||
|
||||
- The reconcilers for CRDs and;
|
||||
- A series of webhooks
|
||||
|
||||
|
||||
## Set up development env
|
||||
|
||||
During development, we prefer that the code is running within our IDE locally, instead of running as the normal Pod(s) within the Kubernetes cluster.
|
||||
|
||||
Such a setup can be illustrated as below diagram:
|
||||
|
||||

|
||||
|
||||
To achieve that, there are some necessary steps we need to walk through, which have been made as a `make` target within our `Makefile`.
|
||||
|
||||
So the TL;DR answer is:
|
||||
|
||||
```sh
|
||||
# If you haven't installed or run `make deploy` before, do it first
|
||||
# Note: please retry if you saw errors
|
||||
$ make deploy
|
||||
|
||||
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
|
||||
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
||||
$ LAPTOP_HOST_IP="<YOUR_LAPTOP_IP>" make dev-setup
|
||||
```
|
||||
|
||||
|
||||
This is a very common setup for typical Kubernetes Operator development so we'd better walk them through with more details here.
|
||||
|
||||
1. Scaling down the deployed Pod(s) to 0
|
||||
|
||||
We need to scale the existing replicas of `capsule-controller-manager` to 0 to avoid reconciliation competition between the Pod(s) and the code running outside of the cluster, in our preferred IDE for example.
|
||||
|
||||
```sh
|
||||
$ kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
|
||||
deployment.apps/capsule-controller-manager scaled
|
||||
```
|
||||
|
||||
2. Preparing TLS certificate for the webhooks
|
||||
|
||||
Running webhooks requires TLS, we can prepare the TLS key pair in our development env to handle HTTPS requests.
|
||||
|
||||
```sh
|
||||
# Prepare a simple OpenSSL config file
|
||||
# Do remember to export LAPTOP_HOST_IP before running this command
|
||||
$ cat > _tls.cnf <<EOF
|
||||
[ req ]
|
||||
default_bits = 4096
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
[ req_distinguished_name ]
|
||||
countryName = SG
|
||||
stateOrProvinceName = SG
|
||||
localityName = SG
|
||||
organizationName = CAPSULE
|
||||
commonName = CAPSULE
|
||||
[ req_ext ]
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = ${LAPTOP_HOST_IP}
|
||||
EOF
|
||||
|
||||
# Create this dir to mimic the Pod mount point
|
||||
$ mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||
|
||||
# Generate the TLS cert/key under /tmp/k8s-webhook-server/serving-certs
|
||||
$ openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
||||
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
|
||||
-extensions req_ext \
|
||||
-config _tls.cnf \
|
||||
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
|
||||
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
|
||||
|
||||
# Clean it up
|
||||
$ rm -f _tls.cnf
|
||||
```
|
||||
|
||||
3. Patching the Webhooks
|
||||
|
||||
By default, the webhooks will be registered with the services, which will route to the Pods, inside the cluster.
|
||||
|
||||
We need to _delegate_ the controllers' and webbooks' services to the code running in our IDE by patching the `MutatingWebhookConfiguration` and `ValidatingWebhookConfiguration`.
|
||||
|
||||
```sh
|
||||
# Export your laptop's IP with the 9443 port exposed by controllers/webhooks' services
|
||||
$ export WEBHOOK_URL="https://${LAPTOP_HOST_IP}:9443"
|
||||
|
||||
# Export the cert we just generated as the CA bundle for webhook TLS
|
||||
$ export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`
|
||||
|
||||
# Patch the MutatingWebhookConfiguration webhook
|
||||
$ kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"${CA_BUNDLE}\"}}\
|
||||
]"
|
||||
|
||||
# Verify it if you want
|
||||
$ kubectl get MutatingWebhookConfiguration capsule-mutating-webhook-configuration -o yaml
|
||||
|
||||
# Patch the ValidatingWebhookConfiguration webhooks
|
||||
# Note: there is a list of validating webhook endpoints, not just one
|
||||
$ kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/cordoning\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/ingresses\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/namespaces\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/networkpolicies\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/pods\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/services\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/tenants\",'caBundle':\"${CA_BUNDLE}\"}}\
|
||||
]"
|
||||
|
||||
# Verify it if you want
|
||||
$ kubectl get ValidatingWebhookConfiguration capsule-validating-webhook-configuration -o yaml
|
||||
```
|
||||
|
||||
## Run Capsule outside the cluster
|
||||
|
||||
Now we can run Capsule controllers with webhooks outside of the Kubernetes cluster:
|
||||
|
||||
```sh
|
||||
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/
|
||||
$ go run .
|
||||
```
|
||||
|
||||
To verify that, we can open a new console and create a new Tenant:
|
||||
|
||||
```sh
|
||||
$ kubectl apply -f - <<EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: gas
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
EOF
|
||||
```
|
||||
|
||||
We should see output like:
|
||||
```log
|
||||
tenant.capsule.clastix.io/gas created
|
||||
```
|
||||
|
||||
And could see logs in the `make run` console like:
|
||||
```log
|
||||
...
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.520+0800","logger":"controllers.Tenant","msg":"Ensuring all Namespaces are collected","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Starting processing of Namespaces","Request.Name":"gas","items":0}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring additional RoleBindings for owner","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring RoleBinding for owner","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring Namespace count","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.533+0800","logger":"controllers.Tenant","msg":"Tenant reconciling completed","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.540+0800","logger":"controllers.Tenant","msg":"Ensuring all Namespaces are collected","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Starting processing of Namespaces","Request.Name":"gas","items":0}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring additional RoleBindings for owner","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring RoleBinding for owner","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring Namespace count","Request.Name":"gas"}
|
||||
{"level":"info","ts":"2021-09-28T21:10:30.554+0800","logger":"controllers.Tenant","msg":"Tenant reconciling completed","Request.Name":"gas"}
|
||||
```
|
||||
|
||||
## Work in your preferred IDE
|
||||
|
||||
Now it's time to work through our familiar inner loop for development in our preferred IDE.
|
||||
|
||||
For example, if you're using [Visual Studio Code](https://code.visualstudio.com), this `launch.json` file can be a good start.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"args": [
|
||||
"--zap-encoder=console",
|
||||
"--zap-log-level=debug",
|
||||
"--configuration-name=capsule-default"
|
||||
],
|
||||
"env": {
|
||||
"NAMESPACE": "capsule-system",
|
||||
"TMPDIR": "/tmp/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Please refer to [contributing.md](contributing.md) for more details while contributing.
|
||||
@@ -1,251 +0,0 @@
|
||||
# How to contribute to Capsule
|
||||
First, thanks for your interest in Capsule, any contribution is welcome!
|
||||
|
||||
The first step is to set up your local development environment as stated below:
|
||||
|
||||
## Setting up the development environment
|
||||
The following dependencies are mandatory:
|
||||
|
||||
- [Go 1.16](https://golang.org/dl/)
|
||||
- [OperatorSDK 1.7.2](https://github.com/operator-framework/operator-sdk)
|
||||
- [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
|
||||
- [KinD](https://github.com/kubernetes-sigs/kind)
|
||||
- [ngrok](https://ngrok.com/) (if you want to run locally)
|
||||
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
|
||||
### Installing Go dependencies
|
||||
After cloning Capsule on any folder, access it and issue the following command
|
||||
to ensure all dependencies are properly downloaded.
|
||||
|
||||
```
|
||||
go mod download
|
||||
```
|
||||
|
||||
### Installing Operator SDK
|
||||
Some operations, like the Docker image build process or the code-generation of
|
||||
the CRDs manifests, as well the deep copy functions, require _Operator SDK_:
|
||||
the binary has to be installed into your `PATH`.
|
||||
|
||||
### Installing Kubebuilder
|
||||
With the latest release of OperatorSDK there's a more tighten integration with
|
||||
Kubebuilder and its opinionated testing suite: ensure to download the latest
|
||||
binaries available from the _Releases_ GitHub page and place them into the
|
||||
`/usr/local/kubebuilder/bin` folder, ensuring this is also in your `PATH`.
|
||||
|
||||
### Installing KinD
|
||||
Capsule can run on any certified Kubernetes installation and locally
|
||||
the whole development is performed on _KinD_, also knows as
|
||||
[Kubernetes in Docker](https://github.com/kubernetes-sigs/kind).
|
||||
|
||||
> N.B.: Docker is a hard requirement since it's based on it
|
||||
|
||||
According to your operative system and architecture, download the right binary
|
||||
and place it on your `PATH`.
|
||||
|
||||
Once done, you're ready to bootstrap in a glance of seconds, a fully functional
|
||||
Kubernetes cluster.
|
||||
|
||||
```
|
||||
# kind create cluster --name capsule
|
||||
Creating cluster "capsule" ...
|
||||
✓ Ensuring node image (kindest/node:v1.18.2) 🖼
|
||||
✓ Preparing nodes 📦
|
||||
✓ Writing configuration 📜
|
||||
✓ Starting control-plane 🕹️
|
||||
✓ Installing CNI 🔌
|
||||
✓ Installing StorageClass 💾
|
||||
Set kubectl context to "kind-capsule"
|
||||
You can now use your cluster with:
|
||||
|
||||
kubectl cluster-info --context kind-capsule
|
||||
|
||||
Thanks for using kind! 😊
|
||||
```
|
||||
|
||||
The current `KUBECONFIG` will be populated with the `cluster-admin`
|
||||
certificates and the context changed to the just born Kubernetes cluster.
|
||||
|
||||
### Build the Docker image and push it to KinD
|
||||
From the root path, issue the _make_ recipe:
|
||||
|
||||
```
|
||||
# make docker-build
|
||||
```
|
||||
|
||||
The image `quay.io/clastix/capsule:<tag>` will be available locally. Built image `<tag>` is resulting last one available [release](https://github.com/clastix/capsule/releases).
|
||||
|
||||
Push it to _KinD_ with the following command:
|
||||
|
||||
```
|
||||
# kind load docker-image --nodes capsule-control-plane --name capsule quay.io/clastix/capsule:<tag>
|
||||
```
|
||||
|
||||
### Deploy the Kubernetes manifests
|
||||
With the current `kind-capsule` context enabled, deploy all the required
|
||||
manifests issuing the following command:
|
||||
|
||||
```
|
||||
make deploy
|
||||
```
|
||||
|
||||
This will install all the required Kubernetes resources, automatically.
|
||||
|
||||
You can check if Capsule is running tailing the logs:
|
||||
|
||||
```
|
||||
# kubectl -n capsule-system logs --all-containers -f -l control-plane=controller-manager
|
||||
```
|
||||
|
||||
Since Capsule is built using _OperatorSDK_, logging is handled by the zap
|
||||
module: log verbosity of the Capsule controller can be increased passing
|
||||
the `--zap-log-level` option with a value from `1` to `10` or the
|
||||
[basic keywords](https://godoc.org/go.uber.org/zap/zapcore#Level) although
|
||||
it is suggested to use the `--zap-devel` flag to get also stack traces.
|
||||
|
||||
> CA generation
|
||||
>
|
||||
> You could notice a restart of the Capsule pod upon installation, that's ok:
|
||||
> Capsule is generating the CA and populating the Secret containing the TLS
|
||||
> certificate to handle the webhooks and there's the need the reload the whole
|
||||
> application to serve properly HTTPS requests.
|
||||
|
||||
### Run Capsule locally
|
||||
Debugging remote applications is always struggling but Operators just need
|
||||
access to the Kubernetes API Server.
|
||||
|
||||
#### Scaling down the remote Pod
|
||||
First, ensure the Capsule pod is not running scaling down the Deployment.
|
||||
|
||||
```
|
||||
# kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
|
||||
deployment.apps/capsule-controller-manager scaled
|
||||
```
|
||||
|
||||
> This is mandatory since Capsule uses Leader Election
|
||||
|
||||
#### Providing TLS certificate for webhooks
|
||||
The next step is to replicate the same environment Capsule is expecting in the Pod,
|
||||
it means creating a fake certificate to handle HTTP requests.
|
||||
|
||||
``` bash
|
||||
mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||
kubectl -n capsule-system get secret capsule-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/k8s-webhook-server/serving-certs/tls.crt
|
||||
kubectl -n capsule-system get secret capsule-tls -o jsonpath='{.data.tls\.key}' | base64 -d > /tmp/k8s-webhook-server/serving-certs/tls.key
|
||||
```
|
||||
|
||||
> We're using the certificates generate upon the first installation of Capsule:
|
||||
> it means the Secret will be populated at the first start-up.
|
||||
> If you plan to run it locally since the beginning, it means you will require
|
||||
> to provide a self-signed certificate in the said directory.
|
||||
|
||||
#### Starting NGROK
|
||||
In another session, we need a `ngrok` session, mandatory to debug also webhooks
|
||||
(YMMV).
|
||||
|
||||
```
|
||||
# ngrok http https://localhost:9443
|
||||
ngrok by @inconshreveable
|
||||
|
||||
Session Status online
|
||||
Account Dario Tranchitella (Plan: Free)
|
||||
Version 2.3.35
|
||||
Region United States (us)
|
||||
Web Interface http://127.0.01:4040
|
||||
Forwarding http://cdb72b99348c.ngrok.io -> https://localhost:9443
|
||||
Forwarding https://cdb72b99348c.ngrok.io -> https://localhost:9443
|
||||
Connections ttl opn rt1 rt5 p50 p90
|
||||
0 0 0.00 0.00 0.00 0.00
|
||||
```
|
||||
|
||||
What we need is the _ngrok_ URL (in this case, `https://cdb72b99348c.ngrok.io`)
|
||||
since we're going to use this default URL as the `url` parameter for the
|
||||
_Dynamic Admissions Control Webhooks_.
|
||||
|
||||
#### Patching the MutatingWebhookConfiguration
|
||||
Now it's time to patch the _MutatingWebhookConfiguration_ and the
|
||||
_ValidatingWebhookConfiguration_ too, adding the said `ngrok` URL as base for
|
||||
each defined webhook, as following:
|
||||
|
||||
```diff
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
|
||||
name: capsule-mutating-webhook-configuration
|
||||
webhooks:
|
||||
- name: owner.namespace.capsule.clastix.io
|
||||
failurePolicy: Fail
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
apiVersions: ["v1"]
|
||||
operations: ["CREATE"]
|
||||
resources: ["namespaces"]
|
||||
clientConfig:
|
||||
+ url: https://cdb72b99348c.ngrok.io/mutate-v1-namespace-owner-reference
|
||||
- caBundle:
|
||||
- service:
|
||||
- namespace: system
|
||||
- name: capsule
|
||||
- path: /mutate-v1-namespace-owner-reference
|
||||
...
|
||||
```
|
||||
|
||||
#### Run Capsule
|
||||
Finally, it's time to run locally Capsule using your preferred IDE (or not):
|
||||
from the project root path, you can issue the following command.
|
||||
|
||||
```
|
||||
make run
|
||||
```
|
||||
|
||||
All the logs will start to flow in your standard output, feel free to attach
|
||||
your debugger to set breakpoints as well!
|
||||
|
||||
## Code convention
|
||||
The changes must follow the Pull Request method where a _GitHub Action_ will
|
||||
check the `golangci-lint`, so ensure your changes respect the coding standard.
|
||||
|
||||
### golint
|
||||
You can easily check them issuing the _Make_ recipe `golint`.
|
||||
|
||||
```
|
||||
# make golint
|
||||
golangci-lint run -c .golangci.yml
|
||||
```
|
||||
|
||||
> Enabled linters and related options are defined in the [.golanci.yml file](../../.golangci.yml)
|
||||
|
||||
### goimports
|
||||
Also, the Go import statements must be sorted following the best practice:
|
||||
|
||||
```
|
||||
<STANDARD LIBRARY>
|
||||
|
||||
<EXTERNAL PACKAGES>
|
||||
|
||||
<LOCAL PACKAGES>
|
||||
```
|
||||
|
||||
To help you out you can use the _Make_ recipe `goimports`
|
||||
|
||||
```
|
||||
# make goimports
|
||||
goimports -w -l -local "github.com/clastix/capsule" .
|
||||
```
|
||||
|
||||
### Commits
|
||||
All the Pull Requests must refer to an already open issue: this is the first phase to contribute also for informing maintainers about the issue.
|
||||
|
||||
Commit's first line should not exceed 50 columns.
|
||||
|
||||
A commit description is welcomed to explain more the changes: just ensure
|
||||
to put a blank line and an arbitrary number of maximum 72 characters long
|
||||
lines, at most one blank line between them.
|
||||
|
||||
Please, split changes into several and documented small commits: this will help us to perform a better review. Commits must follow the Conventional Commits Specification, a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with Semantic Versioning, by describing the features, fixes, and breaking changes made in commit messages. See [Conventional Commits Specification](https://www.conventionalcommits.org) to learn about Conventional Commits.
|
||||
|
||||
> In case of errors or need of changes to previous commits,
|
||||
> fix them squashing to make changes atomic.
|
||||
|
||||
### Miscellanea
|
||||
Please, add a new single line at end of any file as the current coding style.
|
||||
@@ -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).
|
||||
|
||||
32
docs/operator/use-cases/deny-wildcard-hostnames.md
Normal file
32
docs/operator/use-cases/deny-wildcard-hostnames.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Deny Wildcard Hostnames
|
||||
|
||||
Bill, the cluster admin, can deny the use of wildcard hostnames.
|
||||
|
||||
Let's assume that we had a big organization, having a domain `bigorg.com` and there are two tenants, `gas` and `oil`.
|
||||
|
||||
As a tenant-owner of `gas`, Alice create ingress with the host like `- host: "*.bigorg.com"`. That can lead to big problems for the `oil` tenant because Alice can deliberately create ingress with host: `oil.bigorg.com`.
|
||||
|
||||
To avoid this kind of problems, Bill can deny the use of wildcard hostnames in the following way:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: gas
|
||||
annotations:
|
||||
capsule.clastix.io/deny-wildcard: true
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
EOF
|
||||
```
|
||||
|
||||
Doing this, Alice will not be able to use `oil.bigorg.com`, being the tenant-owner of `gas`.
|
||||
|
||||
# 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!
|
||||
@@ -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
|
||||
|
||||
|
||||
28
docs/operator/use-cases/namespace-labels-and-annotations.md
Normal file
28
docs/operator/use-cases/namespace-labels-and-annotations.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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
|
||||
Let's check it out how to restore Tenants after a Velero Backup. [Velero Backup Restoration](./velero-backup-restoration.md).
|
||||
@@ -20,7 +20,7 @@ To simplify the usage of Capsule in this scenario, we'll work with the following
|
||||
|
||||
Use Capsule to address any of the following scenarios:
|
||||
|
||||
* [Onboard Tenants](./onboarding.md)
|
||||
* [Assign Tenant Ownership](./tenant-ownership.md)
|
||||
* [Create Namespaces](./create-namespaces.md)
|
||||
* [Assign Permissions](./permissions.md)
|
||||
* [Enforce Resources Quotas and Limits](./resources-quota-limits.md)
|
||||
@@ -40,7 +40,9 @@ 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)
|
||||
* [Deny Wildcard Hostnames](./deny-wildcard-hostnames.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).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Onboard a new tenant
|
||||
# Tenant ownership
|
||||
Bill, the cluster admin, receives a new request from Acme Corp.'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the Acme Corp. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
|
||||
|
||||
To keep things simple, we assume that Bill just creates a client certificate for authentication using X.509 Certificate Signing Request, so Alice's certificate has `"/CN=alice/O=capsule.clastix.io"`.
|
||||
@@ -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,9 +132,30 @@ 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
|
||||
```
|
||||
|
||||
The service account has to be part of Capsule group, so Bill has to set in the `CapsuleConfiguration`
|
||||
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1alpha1
|
||||
kind: CapsuleConfiguration
|
||||
metadata:
|
||||
name: default
|
||||
spec:
|
||||
userGroups:
|
||||
- capsule.clastix.io
|
||||
- system:serviceaccounts:default
|
||||
```
|
||||
|
||||
because, by default, each service account is a member of following groups:
|
||||
|
||||
```
|
||||
system:serviceaccounts
|
||||
system:serviceaccounts:{service-account-namespace}
|
||||
system:authenticated
|
||||
```
|
||||
|
||||
# What’s next
|
||||
See how a tenant owner, creates new namespaces. [Create namespaces](./create-namespaces.md).
|
||||
@@ -20,4 +20,8 @@ Additionally, you can also specify a selected range of tenants to be restored:
|
||||
./velero-restore.sh --tenant "gas oil" restore
|
||||
```
|
||||
|
||||
In this way, only the tenants **gas** and **oil** will be restored.
|
||||
In this way, only the tenants **gas** and **oil** will be restored.
|
||||
|
||||
# What's next
|
||||
|
||||
See how Bill, the cluster admin, can deny wildcard hostnames to a Tenant. [Deny Wildcard Hostnames](./deny-wildcard-hostnames.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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -38,7 +38,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
|
||||
"glusterfs",
|
||||
},
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
{
|
||||
Limits: []corev1.LimitRangeItem{
|
||||
{
|
||||
@@ -56,7 +56,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
{
|
||||
Egress: []networkingv1.NetworkPolicyEgressRule{
|
||||
{
|
||||
@@ -77,7 +77,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
{
|
||||
Hard: map[corev1.ResourceName]resource.Quantity{
|
||||
corev1.ResourcePods: resource.MustParse("10"),
|
||||
|
||||
@@ -33,7 +33,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
{
|
||||
Limits: []corev1.LimitRangeItem{
|
||||
{
|
||||
@@ -79,7 +79,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
{
|
||||
Hard: map[corev1.ResourceName]resource.Quantity{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("8"),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
@@ -48,7 +47,6 @@ var _ = BeforeSuite(func(done Done) {
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
UseExistingCluster: func(v bool) *bool {
|
||||
return &v
|
||||
}(true),
|
||||
|
||||
@@ -33,7 +33,7 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() {
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
{
|
||||
Limits: []corev1.LimitRangeItem{
|
||||
{
|
||||
@@ -79,7 +79,7 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() {
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
{
|
||||
Ingress: []networkingv1.NetworkPolicyIngressRule{
|
||||
{
|
||||
@@ -127,7 +127,7 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() {
|
||||
NodeSelector: map[string]string{
|
||||
"kubernetes.io/os": "linux",
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
{
|
||||
Hard: map[corev1.ResourceName]resource.Quantity{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("8"),
|
||||
|
||||
@@ -33,7 +33,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
|
||||
{
|
||||
Limits: []corev1.LimitRangeItem{
|
||||
{
|
||||
@@ -79,7 +79,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
|
||||
{
|
||||
Ingress: []networkingv1.NetworkPolicyIngressRule{
|
||||
{
|
||||
@@ -127,7 +127,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
|
||||
NodeSelector: map[string]string{
|
||||
"kubernetes.io/os": "linux",
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
|
||||
{
|
||||
Hard: map[corev1.ResourceName]resource.Quantity{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("8"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses Kubernetes CertificateSigningRequest (CSR) to generate a
|
||||
# certificate signed by the Kubernetes CA itself.
|
||||
@@ -10,17 +10,18 @@
|
||||
# Exit immediately if a command exits with a non-zero status.
|
||||
set -e
|
||||
|
||||
# Check if OpenSSL is installed
|
||||
if [[ ! -x "$(command -v openssl)" ]]; then
|
||||
echo "Error: openssl not found"
|
||||
exit 1
|
||||
fi
|
||||
function check_command() {
|
||||
local command=$1
|
||||
|
||||
# Check if kubectl is installed
|
||||
if [[ ! -x "$(command -v kubectl)" ]]; then
|
||||
echo "Error: kubectl not found"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v $command &> /dev/null; then
|
||||
echo "Error: ${command} not found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_command openssl
|
||||
check_command kubectl
|
||||
check_command jq
|
||||
|
||||
USER=$1
|
||||
TENANT=$2
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
42
pkg/indexer/tenant/status.go
Normal file
42
pkg/indexer/tenant/status.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
func ListByStatus(ctx context.Context, clt client.Client, state string) (tenantList *capsulev1beta1.TenantList, err error) {
|
||||
tenantList = &capsulev1beta1.TenantList{}
|
||||
|
||||
if err = clt.List(ctx, tenantList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.state", state),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type State struct {
|
||||
}
|
||||
|
||||
func (o State) Object() client.Object {
|
||||
return &capsulev1beta1.Tenant{}
|
||||
}
|
||||
|
||||
func (o State) Field() string {
|
||||
return ".status.state"
|
||||
}
|
||||
|
||||
func (o State) Func() client.IndexerFunc {
|
||||
return func(object client.Object) []string {
|
||||
return []string{string(object.(*capsulev1beta1.Tenant).Status.State)}
|
||||
}
|
||||
}
|
||||
43
pkg/metrics/tenant_status.go
Normal file
43
pkg/metrics/tenant_status.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/clastix/capsule/api/v1beta1"
|
||||
"github.com/clastix/capsule/pkg/indexer/tenant"
|
||||
)
|
||||
|
||||
func NewActiveTenantCollector(ctx context.Context, clt client.Client) prometheus.Collector {
|
||||
return prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
Namespace: "capsule",
|
||||
Subsystem: "tenant",
|
||||
Name: "active",
|
||||
Help: "sum of active Tenant resources in Active state",
|
||||
}, func() float64 {
|
||||
list, err := tenant.ListByStatus(ctx, clt, string(v1beta1.TenantStateActive))
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return float64(len(list.Items))
|
||||
})
|
||||
}
|
||||
|
||||
func NewCordonedTenantCollector(ctx context.Context, clt client.Client) prometheus.Collector {
|
||||
return prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
Namespace: "capsule",
|
||||
Subsystem: "tenant",
|
||||
Name: "cordoned",
|
||||
Help: "sum of Tenant resources in Cordoned state",
|
||||
}, func() float64 {
|
||||
list, err := tenant.ListByStatus(ctx, clt, string(v1beta1.TenantStateCordoned))
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return float64(len(list.Items))
|
||||
})
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -54,7 +54,7 @@ func (r registry) Tag() string {
|
||||
|
||||
func NewRegistry(value string) Registry {
|
||||
reg := make(registry)
|
||||
r := regexp.MustCompile(`(((?P<registry>[a-zA-Z0-9-.]+)\/)?((?P<repository>[a-zA-Z0-9-.]+)\/))?(?P<image>[a-zA-Z0-9-.]+)(:(?P<tag>[a-zA-Z0-9-.]+))?`)
|
||||
r := regexp.MustCompile(`(((?P<registry>[a-zA-Z0-9-._]+)\/)?((?P<repository>[a-zA-Z0-9-._]+)\/))?(?P<image>[a-zA-Z0-9-._]+)(:(?P<tag>[a-zA-Z0-9-._]+))?`)
|
||||
match := r.FindStringSubmatch(value)
|
||||
for i, name := range r.SubexpNames() {
|
||||
if i > 0 && i <= len(match) {
|
||||
|
||||
@@ -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