Compare commits

..

72 Commits

Author SHA1 Message Date
Dario Tranchitella
c0d4aab582 build(helm): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
6761fb93dc build(kustomize): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
bf9e0f6b10 test: PriorityClass proxy operations conversion 2021-07-21 16:48:13 +02:00
Dario Tranchitella
f937942c49 feat: capsule-proxy operations for PriorityClass resources 2021-07-21 16:48:13 +02:00
Dario Tranchitella
89d7f301c6 build(helm): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
2a6ff09340 build(kustomize): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
35f48107fc test(e2e): aligning tests to new v1beta1 structure and ExternalName case 2021-07-21 14:34:56 +02:00
Dario Tranchitella
7aa62b6f1d test: conversion for new Service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
58645f39bb chore(samples): example for ServiceOptions 2021-07-21 14:34:56 +02:00
Dario Tranchitella
0e55823a0c feat: toggling ExternalName service 2021-07-21 14:34:56 +02:00
Maksim Fedotov
ba690480a7 refactor: use OwnerListSpec to store tenant owners information 2021-07-20 11:21:40 +02:00
Maksim Fedotov
faa2306a30 chore: support multiple groups in create-{user}/{user-openshift}.sh scripts 2021-07-20 11:21:40 +02:00
Dario Tranchitella
c1448c82e9 build(installer): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
776a56b5bc build(helm): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e4883bb737 build(kustomize): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e70afb5e77 feat: add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
ee7af18f98 docs: bare installation of Capsule using kubectl 2021-07-19 15:21:56 +02:00
Dario Tranchitella
ac7de3bf88 chore(github): updating steps for single YAML file installer diffs 2021-07-19 15:21:56 +02:00
Dario Tranchitella
8883b15aa9 chore: single YAML file installer 2021-07-19 15:21:56 +02:00
Dario Tranchitella
e23132c820 chore(kustomize): using single YAML file to install Capsule 2021-07-19 15:21:56 +02:00
Dario Tranchitella
bec59a585e build(kustomize): updating to v0.1.0-rc3 2021-07-19 15:21:56 +02:00
Dario Tranchitella
9c649ac7eb chore(kustomize): adding v1beta1 Tenant 2021-07-19 15:05:22 +02:00
Dario Tranchitella
3455aed503 fix(samples): Tenant v1beta1 example 2021-07-19 15:05:22 +02:00
Dario Tranchitella
ad1edf57ac fix(samples): removing empty file 2021-07-19 15:05:22 +02:00
Dario Tranchitella
d64dcb5a44 fix: preserving v1alpha1 enable node ports false value avoiding CRD default 2021-07-19 08:15:24 +02:00
Dave
76d7697703 docs: minor improvements 2021-07-16 17:52:16 +02:00
alegrey91
96f4f31c17 docs(velero): add brief explanation about new cli flag 2021-07-16 09:19:36 +02:00
alegrey91
c3f9dfe652 feat(velero): improve usage function 2021-07-16 09:19:36 +02:00
alegrey91
502e9a556f feat(velero): add possibility to specify a tenant list by cli 2021-07-16 09:19:36 +02:00
alegrey91
6f208a6e0e fix(velero): fix wrong argument behaviour 2021-07-16 09:19:36 +02:00
alegrey91
1fb52003d5 fix(velero): add possibility to fix also apiVersion parameter 2021-07-16 09:19:36 +02:00
Dario Tranchitella
98e1640d9b fix: avoid nil slice during resource conversion 2021-07-14 20:54:43 +02:00
Maksim Fedotov
eb19a7a89f chore: fix linting issues 2021-07-12 11:27:13 +02:00
Maksim Fedotov
db8b8ac1d9 test(e2e): support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Maksim Fedotov
663ce93a3e build(helm): support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Maksim Fedotov
a6408f26b0 feat: support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Dario Tranchitella
1aa026c977 chore(github): no need of fundings 2021-07-08 11:36:15 +02:00
Dario Tranchitella
6008373960 bug: ensuring to update the conversion webhook CA bundle 2021-07-05 17:58:49 +02:00
Dario Tranchitella
414c03a874 feat: reconciliation for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
4d34a9e3d7 build(helm): support for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
cb9b560926 build(kustomize): support for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
ef75d0496a feat(api): Tenant state 2021-07-05 16:28:39 +02:00
alegrey91
e1e75a093b docs(velero): add documentation about velero-restore script 2021-07-05 15:53:27 +02:00
alegrey91
80143ffd50 feat(velero): add script to manage velero backup restoration 2021-07-05 15:53:27 +02:00
Dario Tranchitella
3d54810f19 chore: bump-up to latest version 2021-07-05 13:55:39 +02:00
Dario Tranchitella
09dfe33a6a bug(kustomize): fixing JSON path for kustomize-based installation 2021-07-05 13:55:39 +02:00
Dario Tranchitella
01ea36b462 chore: updating kustomize 2021-07-05 13:55:39 +02:00
Dario Tranchitella
bd448d8c29 test(e2e): avoiding flaky tests for ingress hostnames collision 2021-07-02 11:16:52 +02:00
Maksim Fedotov
b58ca3a7d7 chore: v1beta1 goimports and formatting 2021-07-02 10:14:06 +02:00
Maksim Fedotov
52fb0948cb feat(v1beta1): add conversion webhook 2021-07-02 10:14:06 +02:00
Maksim Fedotov
1b0fa587eb chore: remove unused functions for v1alpha1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
92655f1872 build(helm): update crds to use v1beta1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
44bf846260 test(e2e): update tests to use v1beta1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
e6b433dcd7 feat(v1beta1): update code to use v1beta1 version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
3e0882dbc8 refactor: domains is now API utils 2021-07-02 10:14:06 +02:00
Dario Tranchitella
416609362d feat(v1beta1): tenant spec
feat(v1beta1): remove unused structs and functions from v1beta1. Rename v1alpha1 structs to follow new naming. Move v1alpha1 structs to separate files
2021-07-02 10:14:06 +02:00
Dario Tranchitella
3d714dc124 build(kustomize)!: adding the conversion endpoint for v1beta1 2021-07-02 10:14:06 +02:00
Dario Tranchitella
bd01881dd3 feat(v1beta1): scaffolding the Convertible interface 2021-07-02 10:14:06 +02:00
Dario Tranchitella
ac6af13b07 feat(v1beta1): registering conversion webhook 2021-07-02 10:14:06 +02:00
Dario Tranchitella
8fb4b7d4a1 feat: scaffolding v1beta1 Tenant version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
d4280b8d7e chore(makefile): ensure validation for each version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
6e39b17e7c chore(operatorsdk): required scaffolding for v1alpha2 2021-07-02 10:14:06 +02:00
Dario Tranchitella
b1a9603faa fix: ensuring single reconciliation for Capsule RoleBinding resources 2021-07-01 16:34:18 +02:00
alessio
0d4201a6c2 docs(helm): update documentation about hostNetwork 2021-06-29 11:03:07 +02:00
alegrey91
1734c906a9 build(helm): add hostNetwork for manager pod 2021-06-29 11:03:07 +02:00
Dario Tranchitella
184f054f2f test(e2e): adding further tests for collisions 2021-06-27 22:40:23 +02:00
Dario Tranchitella
126449b796 build(helm): fixing pairing between values and collision CRD keys 2021-06-27 22:40:23 +02:00
Dario Tranchitella
284e7da66f build(helm): support for admission review version to v1 2021-06-27 22:36:55 +02:00
Dario Tranchitella
99e1589828 build(helm)!: using multiple handlers per webhook 2021-06-27 22:36:55 +02:00
Dario Tranchitella
7cc2c3f4e9 build(kustomize)!: using multiple handlers per webhook 2021-06-27 22:36:55 +02:00
Dario Tranchitella
ba07f99c6e refactor!: using multiple handers per route 2021-06-27 22:36:55 +02:00
Petr Ruzicka
d79972691e docs: Amazon EKS documentation 2021-06-27 21:07:41 +02:00
170 changed files with 9580 additions and 3718 deletions

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [prometherion]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -23,6 +23,8 @@ jobs:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Go modules
uses: actions/cache@v1
env:
@@ -35,10 +37,10 @@ jobs:
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
- run: make manifests
- name: Checking if manifests are disaligned
- run: make installer
- name: Checking if YAML installer file is not aligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
- name: Checking if manifests generated untracked files
- name: Checking if YAML installer generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted
run: test -z "$(git diff 2> /dev/null)"

View File

@@ -15,7 +15,7 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# Image URL to use all building/pushing image targets
IMG ?= quay.io/clastix/capsule:$(VERSION)
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
CRD_OPTIONS ?= "crd:preserveUnknownFields=false"
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
@@ -47,22 +47,26 @@ manager: generate fmt vet
run: generate manifests
go run ./main.go
# Creates the single file to install Capsule without any external dependency
installer: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > config/install.yaml
# Install CRDs into a cluster
install: manifests kustomize
install: installer
$(KUSTOMIZE) build config/crd | kubectl apply -f -
# Uninstall CRDs from a cluster
uninstall: manifests kustomize
uninstall: installer
$(KUSTOMIZE) build config/crd | kubectl delete -f -
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
deploy: installer
kubectl apply -f config/install.yaml
# Remove controller in the configured Kubernetes cluster in ~/.kube/config
remove: manifests kustomize
$(KUSTOMIZE) build config/default | kubectl delete -f -
remove: installer
kubectl delete -f config/install.yaml
kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
@@ -87,37 +91,27 @@ docker-build: test
docker-push:
docker push ${IMG}
# find or download controller-gen
# download controller-gen if necessary
controller-gen:
ifeq (, $(shell which controller-gen))
@{ \
set -e ;\
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$CONTROLLER_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
}
CONTROLLER_GEN=$(GOBIN)/controller-gen
else
CONTROLLER_GEN=$(shell which controller-gen)
endif
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0)
kustomize:
ifeq (, $(shell which kustomize))
@{ \
set -e ;\
KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$KUSTOMIZE_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\
rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\
}
KUSTOMIZE=$(GOBIN)/kustomize
else
KUSTOMIZE=$(shell which kustomize)
endif
KUSTOMIZE = $(shell pwd)/bin/kustomize
kustomize: ## Download kustomize locally if necessary.
$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7)
# go-get-tool will 'go get' any package $2 and install it to $1.
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-get-tool
@[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef
# Generate bundle manifests and metadata, then validate generated files.
bundle: manifests

40
PROJECT
View File

@@ -1,25 +1,39 @@
domain: github.com/clastix/capsule
layout: go.kubebuilder.io/v3
domain: clastix.io
layout:
- go.kubebuilder.io/v3
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: capsule
repo: github.com/clastix/capsule
resources:
- api:
crdVersion: v1
controller: false
domain: github.com/clastix/capsule
group: capsule.clastix.io
namespaced: false
controller: true
domain: clastix.io
group: capsule
kind: Tenant
path: github.com/clastix/capsule/api/v1alpha1
version: v1alpha1
webhooks:
conversion: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: false
controller: true
domain: clastix.io
group: capsule
kind: CapsuleConfiguration
path: github.com/clastix/capsule/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
controller: true
domain: github.com/clastix/capsule
group: capsule.clastix.io
namespaced: false
domain: clastix.io
group: capsule
kind: Tenant
path: github.com/clastix/capsule/api/v1alpha1
version: v1alpha1
path: github.com/clastix/capsule/api/v1beta1
version: v1beta1
version: "3"
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}

View File

@@ -61,17 +61,28 @@ Make sure you have access to a Kubernetes cluster as administrator.
There are two ways to install Capsule:
* Use the Helm Chart available [here](./charts/capsule/README.md)
* Use [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
* Use the [single YAML file installer](./config/install.yaml)
## Install with kustomize
Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
## Install with the single YAML file installer
Ensure you have `kubectl` installed in your `PATH`.
Clone this repository and move to the repo folder:
```
$ git clone https://github.com/clastix/capsule
$ cd capsule
$ make deploy
$ kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/install.yaml
namespace/capsule-system created
customresourcedefinition.apiextensions.k8s.io/capsuleconfigurations.capsule.clastix.io created
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding created
secret/capsule-ca created
secret/capsule-tls created
service/capsule-controller-manager-metrics-service created
service/capsule-webhook-service created
deployment.apps/capsule-controller-manager created
capsuleconfiguration.capsule.clastix.io/capsule-default created
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration created
validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration created
```
It will install the Capsule controller in a dedicated namespace `capsule-system`.

View File

@@ -0,0 +1,9 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import rbacv1 "k8s.io/api/rbac/v1"
type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}

View File

@@ -0,0 +1,500 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
const (
podAllowedImagePullPolicyAnnotation = "capsule.clastix.io/allowed-image-pull-policy"
podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed"
podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex"
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableExternalNameAnnotation = "capsule.clastix.io/enable-external-name"
ownerGroupsAnnotation = "owners.capsule.clastix.io/group"
ownerUsersAnnotation = "owners.capsule.clastix.io/user"
ownerServiceAccountAnnotation = "owners.capsule.clastix.io/serviceaccount"
enableNodeListingAnnotation = "capsule.clastix.io/enable-node-listing"
enableNodeUpdateAnnotation = "capsule.clastix.io/enable-node-update"
enableNodeDeletionAnnotation = "capsule.clastix.io/enable-node-deletion"
enableStorageClassListingAnnotation = "capsule.clastix.io/enable-storageclass-listing"
enableStorageClassUpdateAnnotation = "capsule.clastix.io/enable-storageclass-update"
enableStorageClassDeletionAnnotation = "capsule.clastix.io/enable-storageclass-deletion"
enableIngressClassListingAnnotation = "capsule.clastix.io/enable-ingressclass-listing"
enableIngressClassUpdateAnnotation = "capsule.clastix.io/enable-ingressclass-update"
enableIngressClassDeletionAnnotation = "capsule.clastix.io/enable-ingressclass-deletion"
enablePriorityClassListingAnnotation = "capsule.clastix.io/enable-priorityclass-listing"
enablePriorityClassUpdateAnnotation = "capsule.clastix.io/enable-priorityclass-update"
enablePriorityClassDeletionAnnotation = "capsule.clastix.io/enable-priorityclass-deletion"
)
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
var serviceKindToAnnotationMap = map[capsulev1beta1.ProxyServiceKind][]string{
capsulev1beta1.NodesProxy: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
capsulev1beta1.StorageClassesProxy: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
capsulev1beta1.IngressClassesProxy: {enableIngressClassListingAnnotation, enableIngressClassUpdateAnnotation, enableIngressClassDeletionAnnotation},
capsulev1beta1.PriorityClassesProxy: {enablePriorityClassListingAnnotation, enablePriorityClassUpdateAnnotation, enablePriorityClassDeletionAnnotation},
}
var annotationToOperationMap = map[string]capsulev1beta1.ProxyOperation{
enableNodeListingAnnotation: capsulev1beta1.ListOperation,
enableNodeUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableNodeDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableStorageClassListingAnnotation: capsulev1beta1.ListOperation,
enableStorageClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableStorageClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableIngressClassListingAnnotation: capsulev1beta1.ListOperation,
enableIngressClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableIngressClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enablePriorityClassListingAnnotation: capsulev1beta1.ListOperation,
enablePriorityClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enablePriorityClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
}
var annotationToOwnerKindMap = map[string]capsulev1beta1.OwnerKind{
ownerUsersAnnotation: capsulev1beta1.UserOwner,
ownerGroupsAnnotation: capsulev1beta1.GroupOwner,
ownerServiceAccountAnnotation: capsulev1beta1.ServiceAccountOwner,
}
annotations := t.GetAnnotations()
var operations = make(map[string]map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
for serviceKind, operationAnnotations := range serviceKindToAnnotationMap {
for _, operationAnnotation := range operationAnnotations {
val, ok := annotations[operationAnnotation]
if ok {
for _, owner := range strings.Split(val, ",") {
if _, exists := operations[owner]; !exists {
operations[owner] = make(map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
}
operations[owner][serviceKind] = append(operations[owner][serviceKind], annotationToOperationMap[operationAnnotation])
}
}
}
}
var owners capsulev1beta1.OwnerListSpec
var getProxySettingsForOwner = func(ownerName string) (settings []capsulev1beta1.ProxySettings) {
ownerOperations, ok := operations[ownerName]
if ok {
for k, v := range ownerOperations {
settings = append(settings, capsulev1beta1.ProxySettings{
Kind: k,
Operations: v,
})
}
}
return
}
owners = append(owners, capsulev1beta1.OwnerSpec{
Kind: capsulev1beta1.OwnerKind(t.Spec.Owner.Kind),
Name: t.Spec.Owner.Name,
ProxyOperations: getProxySettingsForOwner(t.Spec.Owner.Name),
})
for ownerAnnotation, ownerKind := range annotationToOwnerKindMap {
val, ok := annotations[ownerAnnotation]
if ok {
for _, owner := range strings.Split(val, ",") {
owners = append(owners, capsulev1beta1.OwnerSpec{
Kind: ownerKind,
Name: owner,
ProxyOperations: getProxySettingsForOwner(owner),
})
}
}
}
return owners
}
func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*capsulev1beta1.Tenant)
annotations := t.GetAnnotations()
// ObjectMeta
dst.ObjectMeta = t.ObjectMeta
// Spec
dst.Spec.NamespaceQuota = t.Spec.NamespaceQuota
dst.Spec.NodeSelector = t.Spec.NodeSelector
dst.Spec.Owners = t.convertV1Alpha1OwnerToV1Beta1()
if t.Spec.NamespacesMetadata != nil {
dst.Spec.NamespacesMetadata = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.NamespacesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if t.Spec.ServicesMetadata != nil {
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
},
}
}
}
if t.Spec.StorageClasses != nil {
dst.Spec.StorageClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.StorageClasses.Exact,
Regex: t.Spec.StorageClasses.Regex,
}
}
if t.Spec.IngressClasses != nil {
dst.Spec.IngressClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressClasses.Exact,
Regex: t.Spec.IngressClasses.Regex,
}
}
if t.Spec.IngressHostnames != nil {
dst.Spec.IngressHostnames = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressHostnames.Exact,
Regex: t.Spec.IngressHostnames.Regex,
}
}
if t.Spec.ContainerRegistries != nil {
dst.Spec.ContainerRegistries = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.ContainerRegistries.Exact,
Regex: t.Spec.ContainerRegistries.Regex,
}
}
if len(t.Spec.NetworkPolicies) > 0 {
dst.Spec.NetworkPolicies = &capsulev1beta1.NetworkPolicySpec{
Items: t.Spec.NetworkPolicies,
}
}
if len(t.Spec.LimitRanges) > 0 {
dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{
Items: t.Spec.LimitRanges,
}
}
if len(t.Spec.ResourceQuota) > 0 {
dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{
Items: t.Spec.ResourceQuota,
}
}
if len(t.Spec.AdditionalRoleBindings) > 0 {
for _, rb := range t.Spec.AdditionalRoleBindings {
dst.Spec.AdditionalRoleBindings = append(dst.Spec.AdditionalRoleBindings, capsulev1beta1.AdditionalRoleBindingsSpec{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if t.Spec.ExternalServiceIPs != nil {
dst.Spec.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: make([]capsulev1beta1.AllowedIP, len(t.Spec.ExternalServiceIPs.Allowed)),
}
for i, IP := range t.Spec.ExternalServiceIPs.Allowed {
dst.Spec.ExternalServiceIPs.Allowed[i] = capsulev1beta1.AllowedIP(IP)
}
}
pullPolicies, ok := annotations[podAllowedImagePullPolicyAnnotation]
if ok {
for _, policy := range strings.Split(pullPolicies, ",") {
dst.Spec.ImagePullPolicies = append(dst.Spec.ImagePullPolicies, capsulev1beta1.ImagePullPolicySpec(policy))
}
}
priorityClasses := capsulev1beta1.AllowedListSpec{}
priorityClassAllowed, ok := annotations[podPriorityAllowedAnnotation]
if ok {
priorityClasses.Exact = strings.Split(priorityClassAllowed, ",")
}
priorityClassesRegexp, ok := annotations[podPriorityAllowedRegexAnnotation]
if ok {
priorityClasses.Regex = priorityClassesRegexp
}
if !reflect.ValueOf(priorityClasses).IsZero() {
dst.Spec.PriorityClasses = &priorityClasses
}
enableNodePorts, ok := annotations[enableNodePortsAnnotation]
if ok {
val, err := strconv.ParseBool(enableNodePorts)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.NodePort = pointer.BoolPtr(val)
}
enableExternalName, ok := annotations[enableExternalNameAnnotation]
if ok {
val, err := strconv.ParseBool(enableExternalName)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
}
// Status
dst.Status = capsulev1beta1.TenantStatus{
Size: t.Status.Size,
Namespaces: t.Status.Namespaces,
}
// Remove unneeded annotations
delete(dst.ObjectMeta.Annotations, podAllowedImagePullPolicyAnnotation)
delete(dst.ObjectMeta.Annotations, podPriorityAllowedAnnotation)
delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation)
delete(dst.ObjectMeta.Annotations, enableExternalNameAnnotation)
delete(dst.ObjectMeta.Annotations, ownerGroupsAnnotation)
delete(dst.ObjectMeta.Annotations, ownerUsersAnnotation)
delete(dst.ObjectMeta.Annotations, ownerServiceAccountAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassDeletionAnnotation)
return nil
}
func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
var ownersAnnotations = map[string][]string{
ownerGroupsAnnotation: nil,
ownerUsersAnnotation: nil,
ownerServiceAccountAnnotation: nil,
}
var proxyAnnotations = map[string][]string{
enableNodeListingAnnotation: nil,
enableNodeUpdateAnnotation: nil,
enableNodeDeletionAnnotation: nil,
enableStorageClassListingAnnotation: nil,
enableStorageClassUpdateAnnotation: nil,
enableStorageClassDeletionAnnotation: nil,
enableIngressClassListingAnnotation: nil,
enableIngressClassUpdateAnnotation: nil,
enableIngressClassDeletionAnnotation: nil,
}
for i, owner := range src.Spec.Owners {
if i == 0 {
t.Spec.Owner = OwnerSpec{
Name: owner.Name,
Kind: Kind(owner.Kind),
}
} else {
switch owner.Kind {
case capsulev1beta1.UserOwner:
ownersAnnotations[ownerUsersAnnotation] = append(ownersAnnotations[ownerUsersAnnotation], owner.Name)
case capsulev1beta1.GroupOwner:
ownersAnnotations[ownerGroupsAnnotation] = append(ownersAnnotations[ownerGroupsAnnotation], owner.Name)
case capsulev1beta1.ServiceAccountOwner:
ownersAnnotations[ownerServiceAccountAnnotation] = append(ownersAnnotations[ownerServiceAccountAnnotation], owner.Name)
}
}
for _, setting := range owner.ProxyOperations {
switch setting.Kind {
case capsulev1beta1.NodesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableNodeListingAnnotation] = append(proxyAnnotations[enableNodeListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableNodeUpdateAnnotation] = append(proxyAnnotations[enableNodeUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableNodeDeletionAnnotation] = append(proxyAnnotations[enableNodeDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.PriorityClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enablePriorityClassListingAnnotation] = append(proxyAnnotations[enablePriorityClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enablePriorityClassUpdateAnnotation] = append(proxyAnnotations[enablePriorityClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enablePriorityClassDeletionAnnotation] = append(proxyAnnotations[enablePriorityClassDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.StorageClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableStorageClassListingAnnotation] = append(proxyAnnotations[enableStorageClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableStorageClassUpdateAnnotation] = append(proxyAnnotations[enableStorageClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableStorageClassDeletionAnnotation] = append(proxyAnnotations[enableStorageClassDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.IngressClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableIngressClassListingAnnotation] = append(proxyAnnotations[enableIngressClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableIngressClassUpdateAnnotation] = append(proxyAnnotations[enableIngressClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableIngressClassDeletionAnnotation] = append(proxyAnnotations[enableIngressClassDeletionAnnotation], owner.Name)
}
}
}
}
}
for k, v := range ownersAnnotations {
if len(v) > 0 {
t.Annotations[k] = strings.Join(v, ",")
}
}
for k, v := range proxyAnnotations {
if len(v) > 0 {
t.Annotations[k] = strings.Join(v, ",")
}
}
}
func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*capsulev1beta1.Tenant)
// ObjectMeta
t.ObjectMeta = src.ObjectMeta
// Spec
t.Spec.NamespaceQuota = src.Spec.NamespaceQuota
t.Spec.NodeSelector = src.Spec.NodeSelector
if t.Annotations == nil {
t.Annotations = make(map[string]string)
}
t.convertV1Beta1OwnerToV1Alpha1(src)
if src.Spec.NamespacesMetadata != nil {
t.Spec.NamespacesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.NamespacesMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations,
}
}
if src.Spec.StorageClasses != nil {
t.Spec.StorageClasses = &AllowedListSpec{
Exact: src.Spec.StorageClasses.Exact,
Regex: src.Spec.StorageClasses.Regex,
}
}
if src.Spec.IngressClasses != nil {
t.Spec.IngressClasses = &AllowedListSpec{
Exact: src.Spec.IngressClasses.Exact,
Regex: src.Spec.IngressClasses.Regex,
}
}
if src.Spec.IngressHostnames != nil {
t.Spec.IngressHostnames = &AllowedListSpec{
Exact: src.Spec.IngressHostnames.Exact,
Regex: src.Spec.IngressHostnames.Regex,
}
}
if src.Spec.ContainerRegistries != nil {
t.Spec.ContainerRegistries = &AllowedListSpec{
Exact: src.Spec.ContainerRegistries.Exact,
Regex: src.Spec.ContainerRegistries.Regex,
}
}
if src.Spec.NetworkPolicies != nil {
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
}
if src.Spec.LimitRanges != nil {
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
}
if src.Spec.ResourceQuota != nil {
t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
}
if len(src.Spec.AdditionalRoleBindings) > 0 {
for _, rb := range src.Spec.AdditionalRoleBindings {
t.Spec.AdditionalRoleBindings = append(t.Spec.AdditionalRoleBindings, AdditionalRoleBindingsSpec{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if src.Spec.ExternalServiceIPs != nil {
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: make([]AllowedIP, len(src.Spec.ExternalServiceIPs.Allowed)),
}
for i, IP := range src.Spec.ExternalServiceIPs.Allowed {
t.Spec.ExternalServiceIPs.Allowed[i] = AllowedIP(IP)
}
}
if len(src.Spec.ImagePullPolicies) != 0 {
var pullPolicies []string
for _, policy := range src.Spec.ImagePullPolicies {
pullPolicies = append(pullPolicies, string(policy))
}
t.Annotations[podAllowedImagePullPolicyAnnotation] = strings.Join(pullPolicies, ",")
}
if src.Spec.PriorityClasses != nil {
if len(src.Spec.PriorityClasses.Exact) != 0 {
t.Annotations[podPriorityAllowedAnnotation] = strings.Join(src.Spec.PriorityClasses.Exact, ",")
}
if src.Spec.PriorityClasses.Regex != "" {
t.Annotations[podPriorityAllowedRegexAnnotation] = src.Spec.PriorityClasses.Regex
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AllowedServices != nil {
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
}
// Status
t.Status = TenantStatus{
Size: src.Status.Size,
Namespaces: src.Status.Namespaces,
}
return nil
}

View File

@@ -0,0 +1,373 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
var namespaceQuota int32 = 5
var nodeSelector = map[string]string{
"foo": "bar",
}
var v1alpha1AdditionalMetadataSpec = &AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"foo": "bar",
},
AdditionalAnnotations: map[string]string{
"foo": "bar",
},
}
var v1alpha1AllowedListSpec = &AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
var v1beta1AdditionalMetadataSpec = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"foo": "bar",
},
AdditionalAnnotations: map[string]string{
"foo": "bar",
},
}
var v1beta1ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
},
}
var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
var networkPolicies = []networkingv1.NetworkPolicySpec{
{
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "tenant-resources",
},
},
},
{
PodSelector: &metav1.LabelSelector{},
},
{
IPBlock: &networkingv1.IPBlock{
CIDR: "192.168.0.0/12",
},
},
},
},
},
},
}
var limitRanges = []corev1.LimitRangeSpec{
{
Limits: []corev1.LimitRangeItem{
{
Type: corev1.LimitTypePod,
Min: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
Max: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
}
var resourceQuotas = []corev1.ResourceQuotaSpec{
{
Hard: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceLimitsCPU: resource.MustParse("8"),
corev1.ResourceLimitsMemory: resource.MustParse("16Gi"),
corev1.ResourceRequestsCPU: resource.MustParse("8"),
corev1.ResourceRequestsMemory: resource.MustParse("16Gi"),
},
Scopes: []corev1.ResourceQuotaScope{
corev1.ResourceQuotaScopeNotTerminating,
},
},
}
var v1beta1Tnt = capsulev1beta1.Tenant{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "alice",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
},
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Kind: "User",
Name: "alice",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List", "Update", "Delete"},
},
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Update", "Delete"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update", "Delete"},
},
},
},
{
Kind: "User",
Name: "bob",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "User",
Name: "jack",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "PriorityClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "Group",
Name: "owner-foo",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "Group",
Name: "owner-bar",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
},
},
{
Kind: "ServiceAccount",
Name: "system:serviceaccount:oil-production:default",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
},
},
{
Kind: "ServiceAccount",
Name: "system:serviceaccount:gas-production:gas",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
},
},
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1beta1AdditionalMetadataSpec,
ServiceOptions: v1beta1ServiceOptions,
StorageClasses: v1beta1AllowedListSpec,
IngressClasses: v1beta1AllowedListSpec,
IngressHostnames: v1beta1AllowedListSpec,
ContainerRegistries: v1beta1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{
Items: networkPolicies,
},
LimitRanges: &capsulev1beta1.LimitRangesSpec{
Items: limitRanges,
},
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{
Items: resourceQuotas,
},
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
{
Kind: "Group",
APIGroup: "rbac.authorization.k8s.io",
Name: "system:authenticated",
},
},
},
},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
},
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"},
PriorityClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{"default"},
Regex: "^tier-.*$",
},
},
Status: capsulev1beta1.TenantStatus{
Size: 1,
Namespaces: []string{"foo", "bar"},
},
}
var v1alpha1Tnt = Tenant{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "alice",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent",
enableExternalNameAnnotation: "false",
enableNodePortsAnnotation: "false",
podPriorityAllowedAnnotation: "default",
podPriorityAllowedRegexAnnotation: "^tier-.*$",
ownerGroupsAnnotation: "owner-foo,owner-bar",
ownerUsersAnnotation: "bob,jack",
ownerServiceAccountAnnotation: "system:serviceaccount:oil-production:default,system:serviceaccount:gas-production:gas",
enableNodeUpdateAnnotation: "alice,system:serviceaccount:oil-production:default",
enableNodeDeletionAnnotation: "alice,jack",
enableStorageClassListingAnnotation: "bob,jack",
enableStorageClassUpdateAnnotation: "alice,system:serviceaccount:gas-production:gas",
enableStorageClassDeletionAnnotation: "alice,owner-bar",
enableIngressClassListingAnnotation: "alice,owner-foo,owner-bar",
enableIngressClassUpdateAnnotation: "alice,bob",
enableIngressClassDeletionAnnotation: "alice,jack",
enablePriorityClassListingAnnotation: "jack",
},
},
Spec: TenantSpec{
Owner: OwnerSpec{
Name: "alice",
Kind: "User",
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1alpha1AdditionalMetadataSpec,
ServicesMetadata: v1alpha1AdditionalMetadataSpec,
StorageClasses: v1alpha1AllowedListSpec,
IngressClasses: v1alpha1AllowedListSpec,
IngressHostnames: v1alpha1AllowedListSpec,
ContainerRegistries: v1alpha1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: networkPolicies,
LimitRanges: limitRanges,
ResourceQuota: resourceQuotas,
AdditionalRoleBindings: []AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
{
Kind: "Group",
APIGroup: "rbac.authorization.k8s.io",
Name: "system:authenticated",
},
},
},
},
ExternalServiceIPs: &ExternalServiceIPsSpec{
Allowed: []AllowedIP{"192.168.0.1"},
},
},
Status: TenantStatus{
Size: 1,
Namespaces: []string{"foo", "bar"},
},
}
return v1alpha1Tnt, v1beta1Tnt
}
func TestConversionHub_ConvertTo(t *testing.T) {
var v1beta1ConvertedTnt = capsulev1beta1.Tenant{}
v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs()
err := v1alpha1Tnt.ConvertTo(&v1beta1ConvertedTnt)
if assert.NoError(t, err) {
sort.Slice(v1beta1tnt.Spec.Owners, func(i, j int) bool {
return v1beta1tnt.Spec.Owners[i].Name < v1beta1tnt.Spec.Owners[j].Name
})
sort.Slice(v1beta1ConvertedTnt.Spec.Owners, func(i, j int) bool {
return v1beta1ConvertedTnt.Spec.Owners[i].Name < v1beta1ConvertedTnt.Spec.Owners[j].Name
})
for _, owner := range v1beta1tnt.Spec.Owners {
sort.Slice(owner.ProxyOperations, func(i, j int) bool {
return owner.ProxyOperations[i].Kind < owner.ProxyOperations[j].Kind
})
}
for _, owner := range v1beta1ConvertedTnt.Spec.Owners {
sort.Slice(owner.ProxyOperations, func(i, j int) bool {
return owner.ProxyOperations[i].Kind < owner.ProxyOperations[j].Kind
})
}
assert.Equal(t, v1beta1tnt, v1beta1ConvertedTnt)
}
}
func TestConversionHub_ConvertFrom(t *testing.T) {
var v1alpha1ConvertedTnt = Tenant{}
v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs()
err := v1alpha1ConvertedTnt.ConvertFrom(&v1beta1tnt)
if assert.NoError(t, err) {
assert.EqualValues(t, v1alpha1Tnt, v1alpha1ConvertedTnt)
}
}

View File

@@ -1,9 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package domain
type AllowedList interface {
ExactMatch(value string) bool
RegexMatch(value string) bool
}

View File

@@ -1,38 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package domain
import (
"regexp"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
)
const (
podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed"
podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex"
)
func NewPodPriority(object metav1.Object) (allowed *v1alpha1.AllowedListSpec) {
annotations := object.GetAnnotations()
if v, ok := annotations[podPriorityAllowedAnnotation]; ok {
allowed = &v1alpha1.AllowedListSpec{}
allowed.Exact = strings.Split(v, ",")
}
if v, ok := annotations[podPriorityAllowedRegexAnnotation]; ok {
if _, err := regexp.Compile(v); err == nil {
if allowed == nil {
allowed = &v1alpha1.AllowedListSpec{}
}
allowed.Regex = v
}
}
return
}

View File

@@ -1,65 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package domain
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRegistry(t *testing.T) {
type tc struct {
registry string
repo string
image string
tag string
}
for name, tc := range map[string]tc{
"docker.io/my-org/my-repo:v0.0.1": {
registry: "docker.io",
repo: "my-org",
image: "my-repo",
tag: "v0.0.1",
},
"unnamed/repository:1.2.3": {
registry: "docker.io",
repo: "unnamed",
image: "repository",
tag: "1.2.3",
},
"quay.io/clastix/capsule:v1.0.0": {
registry: "quay.io",
repo: "clastix",
image: "capsule",
tag: "v1.0.0",
},
"docker.io/redis:alpine": {
registry: "docker.io",
repo: "",
image: "redis",
tag: "alpine",
},
"nginx:alpine": {
registry: "docker.io",
repo: "",
image: "nginx",
tag: "alpine",
},
"nginx": {
registry: "docker.io",
repo: "",
image: "nginx",
tag: "latest",
},
} {
t.Run(name, func(t *testing.T) {
r := NewRegistry(name)
assert.Equal(t, tc.registry, r.Registry())
assert.Equal(t, tc.repo, r.Repository())
assert.Equal(t, tc.image, r.Image())
assert.Equal(t, tc.tag, r.Tag())
})
}
}

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -1,29 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"sort"
)
type IngressHostnamesList []string
func (hostnames IngressHostnamesList) Len() int {
return len(hostnames)
}
func (hostnames IngressHostnamesList) Swap(i, j int) {
hostnames[i], hostnames[j] = hostnames[j], hostnames[i]
}
func (hostnames IngressHostnamesList) Less(i, j int) bool {
return hostnames[i] < hostnames[j]
}
func (hostnames IngressHostnamesList) IsStringInList(value string) (ok bool) {
sort.Sort(hostnames)
i := sort.SearchStrings(hostnames, value)
ok = i < hostnames.Len() && hostnames[i] == value
return
}

17
api/v1alpha1/owner.go Normal file
View File

@@ -0,0 +1,17 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
}
// +kubebuilder:validation:Enum=User;Group
type Kind string
func (k Kind) String() string {
return string(k)
}

View File

@@ -6,35 +6,17 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type AdditionalMetadata struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}
type IngressHostnamesSpec struct {
Allowed IngressHostnamesList `json:"allowed"`
AllowedRegex string `json:"allowedRegex"`
}
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPs struct {
Allowed []AllowedIP `json:"allowed"`
}
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
Owner OwnerSpec `json:"owner"`
//+kubebuilder:validation:Minimum=1
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata,omitempty"`
ServicesMetadata AdditionalMetadata `json:"servicesMetadata,omitempty"`
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
ServicesMetadata *AdditionalMetadataSpec `json:"servicesMetadata,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
@@ -43,27 +25,8 @@ type TenantSpec struct {
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges []corev1.LimitRangeSpec `json:"limitRanges,omitempty"`
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
AdditionalRoleBindings []AdditionalRoleBindings `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPs `json:"externalServiceIPs,omitempty"`
}
type AdditionalRoleBindings struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
}
// +kubebuilder:validation:Enum=User;Group
type Kind string
func (k Kind) String() string {
return string(k)
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
}
// TenantStatus defines the observed state of Tenant

View File

@@ -0,0 +1,21 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"io/ioutil"
ctrl "sigs.k8s.io/controller-runtime"
)
func (t *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := ioutil.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(t).
Complete()
}

View File

@@ -9,13 +9,13 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.AdditionalLabels != nil {
in, out := &in.AdditionalLabels, &out.AdditionalLabels
@@ -33,32 +33,32 @@ func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadata.
func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadata)
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindings) DeepCopyInto(out *AdditionalRoleBindings) {
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]rbacv1.Subject, len(*in))
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindings.
func (in *AdditionalRoleBindings) DeepCopy() *AdditionalRoleBindings {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindings)
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
@@ -162,7 +162,7 @@ func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPs) DeepCopyInto(out *ExternalServiceIPs) {
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
@@ -171,51 +171,12 @@ func (in *ExternalServiceIPs) DeepCopyInto(out *ExternalServiceIPs) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPs.
func (in *ExternalServiceIPs) DeepCopy() *ExternalServiceIPs {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPs)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in IngressHostnamesList) DeepCopyInto(out *IngressHostnamesList) {
{
in := &in
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesList.
func (in IngressHostnamesList) DeepCopy() IngressHostnamesList {
if in == nil {
return nil
}
out := new(IngressHostnamesList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressHostnamesSpec) DeepCopyInto(out *IngressHostnamesSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesSpec.
func (in *IngressHostnamesSpec) DeepCopy() *IngressHostnamesSpec {
if in == nil {
return nil
}
out := new(IngressHostnamesSpec)
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
@@ -303,8 +264,16 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(int32)
**out = **in
}
in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata)
in.ServicesMetadata.DeepCopyInto(&out.ServicesMetadata)
if in.NamespacesMetadata != nil {
in, out := &in.NamespacesMetadata, &out.NamespacesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServicesMetadata != nil {
in, out := &in.ServicesMetadata, &out.ServicesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(AllowedListSpec)
@@ -334,7 +303,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.NetworkPolicies != nil {
in, out := &in.NetworkPolicies, &out.NetworkPolicies
*out = make([]v1.NetworkPolicySpec, len(*in))
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -355,14 +324,14 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindings, len(*in))
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPs)
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import rbacv1 "k8s.io/api/rbac/v1"
type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}

View File

@@ -0,0 +1,33 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"regexp"
"sort"
"strings"
)
type AllowedListSpec struct {
Exact []string `json:"allowed,omitempty"`
Regex string `json:"allowedRegex,omitempty"`
}
func (in *AllowedListSpec) 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 AllowedListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}

View File

@@ -0,0 +1,67 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAllowedListSpec_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 := AllowedListSpec{
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 TestAllowedListSpec_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 := AllowedListSpec{
Regex: tc.Regex,
}
for _, ok := range tc.True {
assert.True(t, a.RegexMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.RegexMatch(ko))
}
}
}

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -0,0 +1,23 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1beta1 contains API Schema definitions for the capsule v1beta1 API group
//+kubebuilder:object:generate=true
//+groupName=capsule.clastix.io
package v1beta1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1beta1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
type ImagePullPolicySpec string
func (i ImagePullPolicySpec) String() string {
return string(i)
}

View File

@@ -0,0 +1,10 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import corev1 "k8s.io/api/core/v1"
type LimitRangesSpec struct {
Items []corev1.LimitRangeSpec `json:"items,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
networkingv1 "k8s.io/api/networking/v1"
)
type NetworkPolicySpec struct {
Items []networkingv1.NetworkPolicySpec `json:"items,omitempty"`
}

54
api/v1beta1/owner.go Normal file
View File

@@ -0,0 +1,54 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type OwnerSpec struct {
// Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
Kind OwnerKind `json:"kind"`
// Name of tenant owner.
Name string `json:"name"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
}
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
type OwnerKind string
func (k OwnerKind) String() string {
return string(k)
}
type ProxySettings struct {
Kind ProxyServiceKind `json:"kind"`
Operations []ProxyOperation `json:"operations"`
}
// +kubebuilder:validation:Enum=List;Update;Delete
type ProxyOperation string
func (p ProxyOperation) String() string {
return string(p)
}
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses;PriorityClasses
type ProxyServiceKind string
func (p ProxyServiceKind) String() string {
return string(p)
}
const (
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
PriorityClassesProxy ProxyServiceKind = "PriorityClasses"
ListOperation ProxyOperation = "List"
UpdateOperation ProxyOperation = "Update"
DeleteOperation ProxyOperation = "Delete"
UserOwner OwnerKind = "User"
GroupOwner OwnerKind = "Group"
ServiceAccountOwner OwnerKind = "ServiceAccount"
)

34
api/v1beta1/owner_list.go Normal file
View File

@@ -0,0 +1,34 @@
package v1beta1
import (
"sort"
)
type OwnerListSpec []OwnerSpec
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
})
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
}
return
}
type ByKindAndName OwnerListSpec
func (b ByKindAndName) Len() int {
return len(b)
}
func (b ByKindAndName) Less(i, j int) bool {
if b[i].Kind.String() != b[j].Kind.String() {
return b[i].Kind.String() < b[j].Kind.String()
}
return b[i].Name < b[j].Name
}
func (b ByKindAndName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

View File

@@ -0,0 +1,83 @@
package v1beta1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOwnerListSpec_FindOwner(t *testing.T) {
var bla = OwnerSpec{
Kind: UserOwner,
Name: "bla",
ProxyOperations: []ProxySettings{
{
Kind: IngressClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var bar = OwnerSpec{
Kind: GroupOwner,
Name: "bar",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var baz = OwnerSpec{
Kind: UserOwner,
Name: "baz",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Update"},
},
},
}
var fim = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "fim",
ProxyOperations: []ProxySettings{
{
Kind: NodesProxy,
Operations: []ProxyOperation{"List"},
},
},
}
var bom = OwnerSpec{
Kind: GroupOwner,
Name: "bom",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
{
Kind: NodesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var qip = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "qip",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"List", "Delete"},
},
},
}
var owners = OwnerListSpec{bom, qip, bla, bar, baz, fim}
assert.Equal(t, owners.FindOwner("bom", GroupOwner), bom)
assert.Equal(t, owners.FindOwner("qip", ServiceAccountOwner), qip)
assert.Equal(t, owners.FindOwner("bla", UserOwner), bla)
assert.Equal(t, owners.FindOwner("bar", GroupOwner), bar)
assert.Equal(t, owners.FindOwner("baz", UserOwner), baz)
assert.Equal(t, owners.FindOwner("fim", ServiceAccountOwner), fim)
assert.Equal(t, owners.FindOwner("notfound", ServiceAccountOwner), OwnerSpec{})
}

View File

@@ -0,0 +1,10 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import corev1 "k8s.io/api/core/v1"
type ResourceQuotaSpec struct {
Items []corev1.ResourceQuotaSpec `json:"items,omitempty"`
}

View File

@@ -0,0 +1,20 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type ServiceOptions struct {
// Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Block or deny certain type of Services. Optional.
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
}
type AllowedServices struct {
//+kubebuilder:default=true
// Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
NodePort *bool `json:"nodePort,omitempty"`
//+kubebuilder:default=true
// Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
ExternalName *bool `json:"externalName,omitempty"`
}

View File

@@ -0,0 +1,25 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"fmt"
)
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"
)
func UsedQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/used-" + resource.String()
}
func HardQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/hard-" + resource.String()
}

View File

@@ -0,0 +1,42 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"sort"
corev1 "k8s.io/api/core/v1"
)
func (t *Tenant) IsCordoned() bool {
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
return true
}
return false
}
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if t.Spec.NamespaceQuota == nil {
return false
}
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
}
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
var l []string
for _, ns := range namespaces {
if ns.Status.Phase == corev1.NamespaceActive {
l = append(l, ns.GetName())
}
}
sort.Strings(l)
t.Status.Namespaces = l
t.Status.Size = uint(len(l))
}
func (t *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return t.Spec.Owners.FindOwner(name, kind).ProxyOperations
}

View File

@@ -0,0 +1,31 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"fmt"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func GetTypeLabel(t runtime.Object) (label string, err error) {
switch v := t.(type) {
case *Tenant:
return "capsule.clastix.io/tenant", nil
case *corev1.LimitRange:
return "capsule.clastix.io/limit-range", nil
case *networkingv1.NetworkPolicy:
return "capsule.clastix.io/network-policy", nil
case *corev1.ResourceQuota:
return "capsule.clastix.io/resource-quota", nil
case *rbacv1.RoleBinding:
return "capsule.clastix.io/role-binding", nil
default:
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}
return
}

View File

@@ -0,0 +1,23 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Enum=cordoned;active
type tenantState string
const (
TenantStateActive tenantState = "active"
TenantStateCordoned tenantState = "cordoned"
)
// Returns the observed state of the Tenant
type TenantStatus struct {
//+kubebuilder:default=active
// The operational state of the Tenant. Possible values are "active", "cordoned".
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.
Size uint `json:"size"`
// List of namespaces assigned to the Tenant.
Namespaces []string `json:"namespaces,omitempty"`
}

View File

@@ -0,0 +1,80 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
// Specifies the owners of the Tenant. Mandatory.
Owners OwnerListSpec `json:"owners"`
//+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.
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *ServiceOptions `json:"serviceOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
// Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
// 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"`
// 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"`
// 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"`
// 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"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,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.
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceQuota",description="The max amount of Namespaces can be created"
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API
type Tenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TenantSpec `json:"spec,omitempty"`
Status TenantStatus `json:"status,omitempty"`
}
func (t *Tenant) Hub() {}
//+kubebuilder:object:root=true
// TenantList contains a list of Tenant
type TenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Tenant `json:"items"`
}
func init() {
SchemeBuilder.Register(&Tenant{}, &TenantList{})
}

View File

@@ -0,0 +1,484 @@
// +build !ignore_autogenerated
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package v1beta1
import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.AdditionalLabels != nil {
in, out := &in.AdditionalLabels, &out.AdditionalLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.AdditionalAnnotations != nil {
in, out := &in.AdditionalAnnotations, &out.AdditionalAnnotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*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 AllowedListSpec.
func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
if in == nil {
return nil
}
out := new(AllowedListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedServices) DeepCopyInto(out *AllowedServices) {
*out = *in
if in.NodePort != nil {
in, out := &in.NodePort, &out.NodePort
*out = new(bool)
**out = **in
}
if in.ExternalName != nil {
in, out := &in.ExternalName, &out.ExternalName
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedServices.
func (in *AllowedServices) DeepCopy() *AllowedServices {
if in == nil {
return nil
}
out := new(AllowedServices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ByKindAndName) DeepCopyInto(out *ByKindAndName) {
{
in := &in
*out = make(ByKindAndName, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ByKindAndName.
func (in ByKindAndName) DeepCopy() ByKindAndName {
if in == nil {
return nil
}
out := new(ByKindAndName)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]AllowedIP, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LimitRangesSpec) DeepCopyInto(out *LimitRangesSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.LimitRangeSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitRangesSpec.
func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
if in == nil {
return nil
}
out := new(LimitRangesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicySpec.
func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec {
if in == nil {
return nil
}
out := new(NetworkPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in OwnerListSpec) DeepCopyInto(out *OwnerListSpec) {
{
in := &in
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerListSpec.
func (in OwnerListSpec) DeepCopy() OwnerListSpec {
if in == nil {
return nil
}
out := new(OwnerListSpec)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
if in.ProxyOperations != nil {
in, out := &in.ProxyOperations, &out.ProxyOperations
*out = make([]ProxySettings, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec.
func (in *OwnerSpec) DeepCopy() *OwnerSpec {
if in == nil {
return nil
}
out := new(OwnerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxySettings) DeepCopyInto(out *ProxySettings) {
*out = *in
if in.Operations != nil {
in, out := &in.Operations, &out.Operations
*out = make([]ProxyOperation, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxySettings.
func (in *ProxySettings) DeepCopy() *ProxySettings {
if in == nil {
return nil
}
out := new(ProxySettings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceQuotaSpec) DeepCopyInto(out *ResourceQuotaSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.ResourceQuotaSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaSpec.
func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec {
if in == nil {
return nil
}
out := new(ResourceQuotaSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
*out = *in
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedServices != nil {
in, out := &in.AllowedServices, &out.AllowedServices
*out = new(AllowedServices)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions.
func (in *ServiceOptions) DeepCopy() *ServiceOptions {
if in == nil {
return nil
}
out := new(ServiceOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Tenant) DeepCopyInto(out *Tenant) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tenant.
func (in *Tenant) DeepCopy() *Tenant {
if in == nil {
return nil
}
out := new(Tenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Tenant) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantList) DeepCopyInto(out *TenantList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Tenant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantList.
func (in *TenantList) DeepCopy() *TenantList {
if in == nil {
return nil
}
out := new(TenantList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.NamespaceQuota != nil {
in, out := &in.NamespaceQuota, &out.NamespaceQuota
*out = new(int32)
**out = **in
}
if in.NamespacesMetadata != nil {
in, out := &in.NamespacesMetadata, &out.NamespacesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServiceOptions != nil {
in, out := &in.ServiceOptions, &out.ServiceOptions
*out = new(ServiceOptions)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressClasses != nil {
in, out := &in.IngressClasses, &out.IngressClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressHostnames != nil {
in, out := &in.IngressHostnames, &out.IngressHostnames
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*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)
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
if in.PriorityClasses != nil {
in, out := &in.PriorityClasses, &out.PriorityClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.
func (in *TenantSpec) DeepCopy() *TenantSpec {
if in == nil {
return nil
}
out := new(TenantSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = *in
if in.Namespaces != nil {
in, out := &in.Namespaces, &out.Namespaces
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.
func (in *TenantStatus) DeepCopy() *TenantStatus {
if in == nil {
return nil
}
out := new(TenantStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -60,6 +60,7 @@ Here the values you can override:
Parameter | Description | Default
--- | --- | ---
`manager.hostNetwork` | Specifies if the container should be started in `hostNetwork` mode. | `false`
`manager.options.logLevel` | Set the log verbosity of the controller with a value from 1 to 10.| `4`
`manager.options.forceTenantPrefix` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash | `false`
`manager.options.capsuleUserGroup` | Override the Capsule user group | `capsule.clastix.io`

File diff suppressed because it is too large Load Diff

View File

@@ -9,5 +9,5 @@ spec:
- {{ . }}
{{- end}}
protectedNamespaceRegex: {{ .Values.manager.options.protectedNamespaceRegex | quote }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}

View File

@@ -23,6 +23,9 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
{{- if .Values.manager.hostNetwork }}
hostNetwork: true
{{- end }}
priorityClassName: {{ .Values.priorityClassName }}
{{- with .Values.nodeSelector }}
nodeSelector:

View File

@@ -6,29 +6,30 @@ metadata:
{{- include "capsule.labels" . | nindent 4 }}
webhooks:
- admissionReviewVersions:
- v1beta1
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /mutate-v1-namespace-owner-reference
path: /namespace-owner-reference
port: 443
failurePolicy: Fail
matchPolicy: Exact
matchPolicy: Equivalent
name: owner.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
reinvocationPolicy: Never
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.mutatingWebhooksTimeoutSeconds }}

View File

@@ -6,361 +6,23 @@ metadata:
{{- include "capsule.labels" . | nindent 4 }}
webhooks:
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-imagepullpolicy
path: /cordoning
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: validating-image-pull-policy.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-ingress
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: ingress-v1beta1.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-ingress
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: ingress-v1.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- ingresses
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validate-v1-namespace-freezed
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: freezed.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- namespaces
scope: '*'
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validate-v1-namespace-quota
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: quota.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-network-policy
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: validating.network-policy.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- networkpolicies
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: system
path: /validating-v1-podpriority
failurePolicy: Ignore
name: podpriority.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-pvc
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: pvc.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-tenant
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: tenant.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- tenants
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-namespace-tenant-prefix
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: prefix.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-registry
port: 443
failurePolicy: Ignore
matchPolicy: Exact
name: pod.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-external-service-ips
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: validating-external-service-ips.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /tenant-cordoning
port: 443
failurePolicy: Ignore
matchPolicy: Equivalent
name: cordoning.tenant.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- '*'
@@ -373,5 +35,216 @@ webhooks:
resources:
- '*'
scope: Namespaced
sideEffects: NoneOnDryRun
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /ingresses
port: 443
failurePolicy: Fail
matchPolicy: Equivalent
name: ingress.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
- extensions
apiVersions:
- v1
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /namespaces
port: 443
failurePolicy: Fail
matchPolicy: Equivalent
name: namespaces.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- namespaces
scope: '*'
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /networkpolicies
port: 443
failurePolicy: Fail
matchPolicy: Equivalent
name: networkpolicies.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- networkpolicies
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /pods
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: pods.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: capsule-system
path: /persistentvolumeclaims
failurePolicy: Fail
name: pvc.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /services
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: services.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /tenants
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: tenants.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- tenants
scope: '*'
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}

View File

@@ -7,6 +7,14 @@ manager:
repository: quay.io/clastix/capsule
pullPolicy: IfNotPresent
tag: ''
# Specifies if the container should be started in hostNetwork mode.
#
# Required for use in some managed kubernetes clusters (such as AWS EKS) with custom
# CNI (such as calico), because control-plane managed by AWS cannot communicate
# with pods' IP CIDR and admission webhooks are not working
hostNetwork: false
# Additional Capsule options
options:
logLevel: '4'

View File

@@ -563,6 +563,646 @@ spec:
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- description: The actual state of the Tenant
jsonPath: .status.state
name: State
type: string
- description: The max amount of Namespaces can be created
jsonPath: .spec.namespaceQuota
name: Namespace quota
type: integer
- description: The total amount of Namespaces in use
jsonPath: .status.size
name: Namespace count
type: integer
- description: Node Selector applied to Pods
jsonPath: .spec.nodeSelector
name: Node selector
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1beta1
schema:
openAPIV3Schema:
description: Tenant is the Schema for the tenants API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: TenantSpec defines the desired state of Tenant
properties:
additionalRoleBindings:
description: 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.
items:
properties:
clusterRoleName:
type: string
subjects:
description: kubebuilder:validation:Minimum=1
items:
description: Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, or a value for non-objects such as user and group names.
properties:
apiGroup:
description: APIGroup holds the API group of the referenced subject. Defaults to "" for ServiceAccount subjects. Defaults to "rbac.authorization.k8s.io" for User and Group subjects.
type: string
kind:
description: Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". If the Authorizer does not recognized the kind value, the Authorizer should report an error.
type: string
name:
description: Name of the object being referenced.
type: string
namespace:
description: Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty the Authorizer should report an error.
type: string
required:
- kind
- name
type: object
type: array
required:
- clusterRoleName
- subjects
type: object
type: array
containerRegistries:
description: Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
type: object
externalServiceIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
properties:
allowed:
items:
pattern: ^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$
type: string
type: array
required:
- allowed
type: object
imagePullPolicies:
description: 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.
items:
enum:
- Always
- Never
- IfNotPresent
type: string
type: array
ingressClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
type: object
ingressHostnames:
description: Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
type: object
limitRanges:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
description: LimitRangeSpec defines a min/max usage limit for resources that match on kind.
properties:
limits:
description: Limits is the list of LimitRangeItem objects that are enforced.
items:
description: LimitRangeItem defines a min/max usage limit for any resource that matches on kind.
properties:
default:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Default resource requirement limit value by resource name if resource limit is omitted.
type: object
defaultRequest:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: DefaultRequest is the default resource requirement request value by resource name if resource request is omitted.
type: object
max:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Max usage constraints on this kind by resource name.
type: object
maxLimitRequestRatio:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: MaxLimitRequestRatio if specified, the named resource must have a request and limit that are both non-zero where limit divided by request is less than or equal to the enumerated value; this represents the max burst for the named resource.
type: object
min:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Min usage constraints on this kind by resource name.
type: object
type:
description: Type of resource that this limit applies to.
type: string
required:
- type
type: object
type: array
required:
- limits
type: object
type: array
type: object
namespaceQuota:
description: 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.
format: int32
minimum: 1
type: integer
namespacesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
type: object
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
description: NetworkPolicySpec provides the specification of a NetworkPolicy
properties:
egress:
description: List of egress rules to be applied to the selected pods. Outgoing traffic is allowed if there are no NetworkPolicies selecting the pod (and cluster policy otherwise allows the traffic), OR if the traffic matches at least one egress rule across all of the NetworkPolicy objects whose podSelector matches the pod. If this field is empty then this NetworkPolicy limits all outgoing traffic (and serves solely to ensure that the pods it selects are isolated by default). This field is beta-level in 1.8
items:
description: NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. This type is beta-level in 1.8
properties:
ports:
description: List of destination ports for outgoing traffic. Each item in this list is combined using a logical OR. If this field is empty or missing, this rule matches all ports (traffic not restricted by port). If this field is present and contains at least one item, then this rule allows traffic only if the traffic matches at least one port in the list.
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This can either be a numerical or named port on a pod. If this field is not provided, this matches all port names and numbers.
x-kubernetes-int-or-string: true
protocol:
default: TCP
description: The protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP.
type: string
type: object
type: array
to:
description: List of destinations for outgoing traffic of pods selected for this rule. Items in this list are combined using a logical OR operation. If this field is empty or missing, this rule matches all destinations (traffic not restricted by destination). If this field is present and contains at least one item, this rule allows traffic only if the traffic matches at least one item in the to list.
items:
description: NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of fields are allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular IPBlock. If this field is set then neither of the other fields can be.
properties:
cidr:
description: CIDR is a string representing the IP Block Valid examples are "192.168.1.1/24" or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that should not be included within an IP Block Valid examples are "192.168.1.1/24" or "2001:db9::/64" Except values will be rejected if they are outside the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. \n If PodSelector is also set, then the NetworkPolicyPeer as a whole selects the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects all Pods in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects Pods. This field follows standard label selector semantics; if present but empty, it selects all pods. \n If NamespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the Pods matching PodSelector in the policy's own Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
type: object
type: array
type: object
type: array
ingress:
description: List of ingress rules to be applied to the selected pods. Traffic is allowed to a pod if there are no NetworkPolicies selecting the pod (and cluster policy otherwise allows the traffic), OR if the traffic source is the pod's local node, OR if the traffic matches at least one ingress rule across all of the NetworkPolicy objects whose podSelector matches the pod. If this field is empty then this NetworkPolicy does not allow any traffic (and serves solely to ensure that the pods it selects are isolated by default)
items:
description: NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from.
properties:
from:
description: List of sources which should be able to access the pods selected for this rule. Items in this list are combined using a logical OR operation. If this field is empty or missing, this rule matches all sources (traffic not restricted by source). If this field is present and contains at least one item, this rule allows traffic only if the traffic matches at least one item in the from list.
items:
description: NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of fields are allowed
properties:
ipBlock:
description: IPBlock defines policy on a particular IPBlock. If this field is set then neither of the other fields can be.
properties:
cidr:
description: CIDR is a string representing the IP Block Valid examples are "192.168.1.1/24" or "2001:db9::/64"
type: string
except:
description: Except is a slice of CIDRs that should not be included within an IP Block Valid examples are "192.168.1.1/24" or "2001:db9::/64" Except values will be rejected if they are outside the CIDR range
items:
type: string
type: array
required:
- cidr
type: object
namespaceSelector:
description: "Selects Namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. \n If PodSelector is also set, then the NetworkPolicyPeer as a whole selects the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects all Pods in the Namespaces selected by NamespaceSelector."
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
podSelector:
description: "This is a label selector which selects Pods. This field follows standard label selector semantics; if present but empty, it selects all pods. \n If NamespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the Pods matching PodSelector in the policy's own Namespace."
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
type: object
type: array
ports:
description: List of ports which should be made accessible on the pods selected for this rule. Each item in this list is combined using a logical OR. If this field is empty or missing, this rule matches all ports (traffic not restricted by port). If this field is present and contains at least one item, then this rule allows traffic only if the traffic matches at least one port in the list.
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
port:
anyOf:
- type: integer
- type: string
description: The port on the given protocol. This can either be a numerical or named port on a pod. If this field is not provided, this matches all port names and numbers.
x-kubernetes-int-or-string: true
protocol:
default: TCP
description: The protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP.
type: string
type: object
type: array
type: object
type: array
podSelector:
description: Selects the pods to which this NetworkPolicy object applies. The array of ingress rules is applied to any pods selected by this field. Multiple network policies can select the same set of pods. In this case, the ingress rules for each are combined additively. This field is NOT optional and follows standard label selector semantics. An empty podSelector matches all pods in this namespace.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
policyTypes:
description: List of rule types that the NetworkPolicy relates to. Valid options are "Ingress", "Egress", or "Ingress,Egress". If this field is not specified, it will default based on the existence of Ingress or Egress rules; policies that contain an Egress section are assumed to affect Egress, and all policies (whether or not they contain an Ingress section) are assumed to affect Ingress. If you want to write an egress-only policy, you must explicitly specify policyTypes [ "Egress" ]. Likewise, if you want to write a policy that specifies that no egress is allowed, you must specify a policyTypes value that include "Egress" (since such a policy would not include an Egress section and would otherwise default to just [ "Ingress" ]). This field is beta-level in 1.8
items:
description: Policy Type string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
- podSelector
type: object
type: array
type: object
nodeSelector:
additionalProperties:
type: string
description: 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.
type: object
owners:
description: Specifies the owners of the Tenant. Mandatory.
items:
properties:
kind:
description: Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of tenant owner.
type: string
proxySettings:
description: Proxy settings for tenant owner.
items:
properties:
kind:
enum:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
enum:
- List
- Update
- Delete
type: string
type: array
required:
- kind
- operations
type: object
type: array
required:
- kind
- name
type: object
type: array
priorityClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
type: object
resourceQuotas:
description: 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.
properties:
items:
items:
description: ResourceQuotaSpec defines the desired hard limits to enforce for Quota.
properties:
hard:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: 'hard is the set of desired hard limits for each named resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/'
type: object
scopeSelector:
description: scopeSelector is also a collection of filters like scopes that must match each object tracked by a quota but expressed using ScopeSelectorOperator in combination with possible values. For a resource to match, both scopes AND scopeSelector (if specified in spec), must be matched.
properties:
matchExpressions:
description: A list of scope selector requirements by scope of the resources.
items:
description: A scoped-resource selector requirement is a selector that contains values, a scope name, and an operator that relates the scope name and values.
properties:
operator:
description: Represents a scope's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist.
type: string
scopeName:
description: The name of the scope that the selector applies to.
type: string
values:
description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- operator
- scopeName
type: object
type: array
type: object
scopes:
description: A collection of filters that must match each object tracked by a quota. If not specified, the quota matches all objects.
items:
description: A ResourceQuotaScope defines a filter that must match each object tracked by a quota
type: string
type: array
type: object
type: array
type: object
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
type: object
type: object
allowedServices:
description: Block or deny certain type of Services. Optional.
properties:
externalName:
default: true
description: Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
nodePort:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
type: object
type: object
storageClasses:
description: Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
type: object
required:
- owners
type: object
status:
description: Returns the observed state of the Tenant
properties:
namespaces:
description: List of namespaces assigned to the Tenant.
items:
type: string
type: array
size:
description: How many namespaces are assigned to the Tenant.
type: integer
state:
default: active
description: The operational state of the Tenant. Possible values are "active", "cordoned".
enum:
- cordoned
- active
type: string
required:
- size
- state
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -9,3 +9,6 @@ resources:
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml
patchesStrategicMerge:
- patches/webhook_in_tenants.yaml

View File

@@ -4,13 +4,15 @@ nameReference:
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/name
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/namespace
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:

View File

@@ -0,0 +1,17 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tenants.capsule.clastix.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1alpha1
- v1beta1

1642
config/install.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,136 @@
---
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: gas
spec:
additionalRoleBindings:
-
clusterRoleName: tenant-sample-viewer
subjects:
-
kind: User
name: bob
containerRegistries:
allowed:
- docker.io
- quay.io
allowedRegex: ^\w+.gcr.io$
serviceOptions:
additionalMetadata:
additionalAnnotations:
capsule.clastix.io/bgp: "true"
additionalLabels:
capsule.clastix.io/pool: gas
allowedServices:
nodePort: false
externalName: false
externalServiceIPs:
allowed:
- 10.20.0.0/16
- "10.96.42.42"
imagePullPolicies:
- Always
ingressClasses:
allowed:
- default
allowedRegex: ^\w+-lb$
ingressHostnames:
allowed:
- gas.acmecorp.com
allowedRegex: ^.*acmecorp.com$
limitRanges:
items:
-
limits:
-
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Pod
-
default:
cpu: 200m
memory: 100Mi
defaultRequest:
cpu: 100m
memory: 10Mi
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Container
-
max:
storage: 10Gi
min:
storage: 1Gi
type: PersistentVolumeClaim
namespaceQuota: 3
namespacesMetadata:
additionalAnnotations:
capsule.clastix.io/backup: "false"
additionalLabels:
capsule.clastix.io/tenant: gas
networkPolicies:
items:
-
egress:
-
to:
-
ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.0.0/12
ingress:
-
from:
-
namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: gas
-
podSelector: {}
-
ipBlock:
cidr: 192.168.0.0/12
podSelector: {}
policyTypes:
- Ingress
- Egress
nodeSelector:
kubernetes.io/os: linux
owners:
-
kind: User
name: bob
priorityClasses:
allowed:
- shared-nodes
allowedRegex: ^\w-gas$
resourceQuotas:
items:
-
hard:
limits.cpu: "8"
limits.memory: 16Gi
requests.cpu: "8"
requests.memory: 16Gi
scopes:
- NotTerminating
-
hard:
pods: "10"
-
hard:
requests.storage: 100Gi
storageClasses:
allowed:
- default
allowedRegex: ^\w+fs$

View File

@@ -2,3 +2,4 @@
resources:
- capsule_v1alpha1_capsuleconfiguration.yaml
- capsule_v1alpha1_tenant.yaml
- capsule_v1beta1_tenant.yaml

View File

@@ -12,7 +12,7 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /mutate-v1-namespace-owner-reference
path: /namespace-owner-reference
failurePolicy: Fail
name: owner.namespace.capsule.clastix.io
rules:
@@ -39,205 +39,7 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-imagepullpolicy
failurePolicy: Fail
name: validating-image-pull-policy.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-ingress
failurePolicy: Fail
name: ingress-v1beta1.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-ingress
failurePolicy: Fail
name: ingress-v1.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- ingresses
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-v1-namespace-freezed
failurePolicy: Fail
name: freezed.namespace.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- namespaces
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-v1-namespace-quota
failurePolicy: Fail
name: quota.namespace.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-network-policy
failurePolicy: Fail
name: validating.network-policy.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- networkpolicies
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-podpriority
failurePolicy: Ignore
name: podpriority.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-pvc
failurePolicy: Fail
name: pvc.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-registry
failurePolicy: Ignore
name: pod.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-external-service-ips
failurePolicy: Fail
name: validating-external-service-ips.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /tenant-cordoning
path: /cordoning
failurePolicy: Fail
name: cordoning.tenant.capsule.clastix.io
rules:
@@ -258,19 +60,21 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-tenant
path: /ingresses
failurePolicy: Fail
name: tenant.capsule.clastix.io
name: ingress.capsule.clastix.io
rules:
- apiGroups:
- capsule.clastix.io
- networking.k8s.io
- extensions
apiVersions:
- v1alpha1
- v1beta1
- v1
operations:
- CREATE
- UPDATE
resources:
- tenants
- ingresses
sideEffects: None
- admissionReviewVersions:
- v1
@@ -278,9 +82,50 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-namespace-tenant-prefix
path: /namespaces
failurePolicy: Fail
name: prefix.namespace.capsule.clastix.io
name: namespaces.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- namespaces
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /networkpolicies
failurePolicy: Fail
name: networkpolicies.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- networkpolicies
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /pods
failurePolicy: Fail
name: pods.capsule.clastix.io
rules:
- apiGroups:
- ""
@@ -289,5 +134,65 @@ webhooks:
operations:
- CREATE
resources:
- namespaces
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /persistentvolumeclaims
failurePolicy: Fail
name: pvc.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /services
failurePolicy: Fail
name: services.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /tenants
failurePolicy: Fail
name: tenants.capsule.clastix.io
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- tenants
sideEffects: None

View File

@@ -11,13 +11,13 @@
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/2/namespaceSelector
path: /webhooks/3/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/3/namespaceSelector
path: /webhooks/4/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
@@ -35,32 +35,20 @@
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/7/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/8/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/8/rules/0/scope
path: /webhooks/0/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/9/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/10/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/10/rules/0/scope
path: /webhooks/1/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/3/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/4/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/5/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/6/rules/0/scope
value: Namespaced

View File

@@ -15,7 +15,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
@@ -54,7 +54,7 @@ func forOptionPerInstanceName(instanceName string) builder.ForOption {
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
For(&capsulev1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
Complete(r)
}

View File

@@ -23,7 +23,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
@@ -79,7 +79,7 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) (
return r.filterByNames(genericEvent.Object.GetName())
},
})).
Watches(source.NewKindWithCache(&v1alpha1.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{
Watches(source.NewKindWithCache(&capsulev1alpha1.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{
UpdateFunc: func(updateEvent event.UpdateEvent, limitingInterface workqueue.RateLimitingInterface) {
if updateEvent.ObjectNew.GetName() == configurationName {
if crbErr := r.EnsureClusterRoleBindings(); crbErr != nil {

View File

@@ -11,11 +11,13 @@ import (
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
v1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -37,10 +39,45 @@ func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}
// By default helm doesn't allow to use templates in CRD (https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you).
// In order to overcome this, we are setting conversion strategy in helm chart to None, and then update it with CA and namespace information.
func (r *CAReconciler) UpdateCustomResourceDefinition(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
crd := &apiextensionsv1.CustomResourceDefinition{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "tenants.capsule.clastix.io"}, crd)
if err != nil {
r.Log.Error(err, "cannot retrieve CustomResourceDefinition")
return err
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, crd, func() error {
crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
Strategy: "Webhook",
Webhook: &apiextensionsv1.WebhookConversion{
ClientConfig: &apiextensionsv1.WebhookClientConfig{
Service: &apiextensionsv1.ServiceReference{
Namespace: r.Namespace,
Name: "capsule-webhook-service",
Path: pointer.StringPtr("/convert"),
Port: pointer.Int32Ptr(443),
},
CABundle: caBundle,
},
ConversionReviewVersions: []string{"v1alpha1", "v1beta1"},
},
}
return nil
})
return err
})
}
//nolint:dupl
func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
vw := &v1.ValidatingWebhookConfiguration{}
vw := &admissionregistrationv1.ValidatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-validating-webhook-configuration"}, vw)
if err != nil {
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
@@ -59,7 +96,7 @@ func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) erro
//nolint:dupl
func (r CAReconciler) UpdateMutatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
mw := &v1.MutatingWebhookConfiguration{}
mw := &admissionregistrationv1.MutatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-mutating-webhook-configuration"}, mw)
if err != nil {
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
@@ -127,6 +164,9 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
group.Go(func() error {
return r.UpdateValidatingWebhookConfiguration(crt.Bytes())
})
group.Go(func() error {
return r.UpdateCustomResourceDefinition(crt.Bytes())
})
if err = group.Wait(); err != nil {
return reconcile.Result{}, err

View File

@@ -20,7 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
type abstractServiceLabelsReconciler struct {
@@ -53,23 +53,23 @@ func (r *abstractServiceLabelsReconciler) Reconcile(ctx context.Context, request
}
_, err = controllerutil.CreateOrUpdate(ctx, r.client, r.obj, func() (err error) {
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServicesMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServicesMetadata.AdditionalAnnotations))
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations))
return nil
})
return reconcile.Result{}, err
}
func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*v1alpha1.Tenant, error) {
func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*capsulev1beta1.Tenant, error) {
ns := &corev1.Namespace{}
tenant := &v1alpha1.Tenant{}
tenant := &capsulev1beta1.Tenant{}
if err := client.Get(ctx, types.NamespacedName{Name: namespacedName.Namespace}, ns); err != nil {
return nil, err
}
capsuleLabel, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if _, ok := ns.GetLabels()[capsuleLabel]; !ok {
return nil, NewNonTenantObject(namespacedName.Name)
}
@@ -78,7 +78,7 @@ func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespa
return nil, err
}
if tenant.Spec.ServicesMetadata.AdditionalLabels == nil && tenant.Spec.ServicesMetadata.AdditionalAnnotations == nil {
if tenant.Spec.ServiceOptions == nil || tenant.Spec.ServiceOptions.AdditionalMetadata == nil {
return nil, NewNoServicesMetadata(namespacedName.Name)
}
@@ -118,7 +118,7 @@ func (r *abstractServiceLabelsReconciler) forOptionPerInstanceName() builder.For
}
func (r *abstractServiceLabelsReconciler) IsNamespaceInTenant(namespace string) bool {
tl := &v1alpha1.TenantList{}
tl := &capsulev1beta1.TenantList{}
if err := r.client.List(context.Background(), tl, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
}); err != nil {

View File

@@ -30,7 +30,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/controllers/rbac"
)
@@ -44,7 +44,7 @@ type TenantReconciler struct {
func (r *TenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1alpha1.Tenant{}).
For(&capsulev1beta1.Tenant{}).
Owns(&corev1.Namespace{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
@@ -57,7 +57,7 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1alpha1.Tenant{}
instance := &capsulev1beta1.Tenant{}
err = r.Get(ctx, request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
@@ -67,6 +67,11 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
r.Log.Error(err, "Error reading the object")
return
}
// Ensuring the Tenant Status
if err = r.updateTenantStatus(instance); err != nil {
r.Log.Error(err, "Cannot update Tenant status")
return
}
// Ensuring all namespaces are collected
r.Log.Info("Ensuring all Namespaces are collected")
@@ -81,22 +86,28 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
return
}
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
if err = r.syncNetworkPolicies(instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
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 Limit Ranges", "items", len(instance.Spec.LimitRanges))
if err = r.syncLimitRanges(instance); err != nil {
r.Log.Error(err, "Cannot sync LimitRange 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 Resource Quotas", "items", len(instance.Spec.ResourceQuota))
if err = r.syncResourceQuotas(instance); err != nil {
r.Log.Error(err, "Cannot sync ResourceQuota 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("Ensuring additional RoleBindings for owner")
@@ -124,7 +135,7 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
func (r *TenantReconciler) pruningResources(ns string, keys []string, obj client.Object) error {
capsuleLabel, err := capsulev1alpha1.GetTypeLabel(obj)
capsuleLabel, err := capsulev1beta1.GetTypeLabel(obj)
if err != nil {
return err
}
@@ -180,8 +191,8 @@ func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName
found.Annotations = make(map[string]string)
}
found.Labels = rq.Labels
found.Annotations[capsulev1alpha1.UsedQuotaFor(resourceName)] = actual.String()
found.Annotations[capsulev1alpha1.HardQuotaFor(resourceName)] = limit.String()
found.Annotations[capsulev1beta1.UsedQuotaFor(resourceName)] = actual.String()
found.Annotations[capsulev1beta1.HardQuotaFor(resourceName)] = limit.String()
// Updating the Resource according to the actual.Cmp result
found.Spec.Hard = rq.Spec.Hard
return nil
@@ -204,9 +215,9 @@ func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName
// Additional Role Bindings can be used in many ways: applying Pod Security Policies or giving
// access to CRDs or specific API groups.
func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err error) {
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hash := func(binding capsulev1alpha1.AdditionalRoleBindings) string {
hash := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(binding.ClusterRoleName))
@@ -224,11 +235,11 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
}
var tl, ll string
tl, err = capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return
}
ll, err = capsulev1alpha1.GetTypeLabel(&rbacv1.RoleBinding{})
ll, err = capsulev1beta1.GetTypeLabel(&rbacv1.RoleBinding{})
if err != nil {
return
}
@@ -284,19 +295,19 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
// .Status.Used value as the .Hard value.
// This will trigger a following reconciliation but that's ok: the mutateFn will re-use the same business logic, letting
// the mutateFn along with the CreateOrUpdate to don't perform the update since resources are identical.
func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1beta1.Tenant) error {
// getting requested ResourceQuota keys
keys := make([]string, 0, len(tenant.Spec.ResourceQuota))
for i := range tenant.Spec.ResourceQuota {
keys := make([]string, 0, len(tenant.Spec.ResourceQuota.Items))
for i := range tenant.Spec.ResourceQuota.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting ResourceQuota labels for the mutateFn
tenantLabel, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tenantLabel, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
typeLabel, err := capsulev1alpha1.GetTypeLabel(&corev1.ResourceQuota{})
typeLabel, err := capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{})
if err != nil {
return err
}
@@ -305,7 +316,7 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
if err := r.pruningResources(ns, keys, &corev1.ResourceQuota{}); err != nil {
return err
}
for i, q := range tenant.Spec.ResourceQuota {
for i, q := range tenant.Spec.ResourceQuota.Items {
target := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
@@ -407,19 +418,19 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
}
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
// getting requested LimitRange keys
keys := make([]string, 0, len(tenant.Spec.LimitRanges))
for i := range tenant.Spec.LimitRanges {
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items))
for i := range tenant.Spec.LimitRanges.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting LimitRange labels for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
ll, err := capsulev1alpha1.GetTypeLabel(&corev1.LimitRange{})
ll, err := capsulev1beta1.GetTypeLabel(&corev1.LimitRange{})
if err != nil {
return err
}
@@ -428,7 +439,7 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
if err := r.pruningResources(ns, keys, &corev1.LimitRange{}); err != nil {
return err
}
for i, spec := range tenant.Spec.LimitRanges {
for i, spec := range tenant.Spec.LimitRanges.Items {
t := &corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
@@ -456,7 +467,7 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
return nil
}
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Tenant) (err error) {
var res controllerutil.OperationResult
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
@@ -468,8 +479,10 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
a := make(map[string]string)
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations {
a[k] = v
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations {
a[k] = v
}
}
if tnt.Spec.NodeSelector != nil {
@@ -482,28 +495,28 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
if tnt.Spec.IngressClasses != nil {
if len(tnt.Spec.IngressClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
a[capsulev1beta1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
}
if len(tnt.Spec.IngressClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
a[capsulev1beta1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
a[capsulev1beta1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
a[capsulev1beta1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
a[capsulev1alpha1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
a[capsulev1beta1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
a[capsulev1alpha1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
a[capsulev1beta1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
@@ -511,12 +524,14 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
l := make(map[string]string)
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalLabels {
l[k] = v
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalLabels {
l[k] = v
}
}
l["name"] = namespace
capsuleLabel, _ := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
l[capsuleLabel] = tnt.GetName()
ns.SetLabels(l)
@@ -532,7 +547,7 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
}
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1beta1.Tenant) (err error) {
group := errgroup.Group{}
for _, item := range tenant.Status.Namespaces {
@@ -550,19 +565,19 @@ func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err e
}
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
// getting requested NetworkPolicy keys
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies))
for i := range tenant.Spec.NetworkPolicies {
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items))
for i := range tenant.Spec.NetworkPolicies.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting NetworkPolicy labels for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
nl, err := capsulev1alpha1.GetTypeLabel(&networkingv1.NetworkPolicy{})
nl, err := capsulev1beta1.GetTypeLabel(&networkingv1.NetworkPolicy{})
if err != nil {
return err
}
@@ -571,7 +586,7 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e
if err := r.pruningResources(ns, keys, &networkingv1.NetworkPolicy{}); err != nil {
return err
}
for i, spec := range tenant.Spec.NetworkPolicies {
for i, spec := range tenant.Spec.NetworkPolicies.Items {
t := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
@@ -604,19 +619,32 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e
// Since RBAC is based on deny all first, some specific actions like editing Capsule resources are going to be blocked
// via Dynamic Admission Webhooks.
// TODO(prometherion): we could create a capsule:admin role rather than hitting webhooks for each action
func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1beta1.Tenant) error {
// getting RoleBinding label for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
var subjects []rbacv1.Subject
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
l := map[string]string{tl: tenant.Name}
s := []rbacv1.Subject{
{
Kind: tenant.Spec.Owner.Kind.String(),
Name: tenant.Spec.Owner.Name,
},
for _, owner := range tenant.Spec.Owners {
if owner.Kind == "ServiceAccount" {
splitName := strings.Split(owner.Name, ":")
subjects = append(subjects, rbacv1.Subject{
Kind: owner.Kind.String(),
Name: splitName[len(splitName)-1],
Namespace: splitName[len(splitName)-2],
})
} else {
subjects = append(subjects, rbacv1.Subject{
APIGroup: "rbac.authorization.k8s.io",
Kind: owner.Kind.String(),
Name: owner.Name,
})
}
}
rbl := make(map[types.NamespacedName]rbacv1.RoleRef)
@@ -644,7 +672,7 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = l
target.Subjects = s
target.Subjects = subjects
target.RoleRef = rr
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
@@ -659,10 +687,10 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
return nil
}
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1alpha1.Tenant{}
found := &capsulev1beta1.Tenant{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
}
@@ -681,7 +709,7 @@ func (r *TenantReconciler) emitEvent(object runtime.Object, namespace string, re
r.Recorder.AnnotatedEventf(object, map[string]string{"OperationResult": string(res)}, eventType, namespace, msg)
}
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
nl := &corev1.NamespaceList{}
err = r.Client.List(context.TODO(), nl, client.MatchingFieldsSelector{
@@ -697,3 +725,15 @@ func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.Tenant) err
return
})
}
func (r *TenantReconciler) updateTenantStatus(tnt *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if tnt.IsCordoned() {
tnt.Status.State = capsulev1beta1.TenantStateCordoned
} else {
tnt.Status.State = capsulev1beta1.TenantStateActive
}
return r.Client.Status().Update(context.Background(), tnt)
})
}

View File

@@ -0,0 +1,149 @@
# Capsule with Amazon EKS
This is an example how to install Amazon EKS cluster and one user
manged by capsule.
It is based on [Using IAM Groups to manage Kubernetes access](https://www.eksworkshop.com/beginner/091_iam-groups/intro/)
Create EKS cluster:
```bash
export AWS_DEFAULT_REGION="eu-west-1"
export AWS_ACCESS_KEY_ID="xxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxx"
eksctl create cluster \
--name=test-k8s \
--managed \
--node-type=t3.small \
--node-volume-size=20 \
--kubeconfig=kubeconfig.conf
```
Create AWS User `alice` using CloudFormation, create AWS access files and
kubeconfig for such user:
```bash
cat > cf.yml << \EOF
Parameters:
ClusterName:
Type: String
Resources:
UserAlice:
Type: AWS::IAM::User
Properties:
UserName: !Sub "alice-${ClusterName}"
Policies:
- PolicyName: !Sub "alice-${ClusterName}-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowAssumeOrganizationAccountRole
Effect: Allow
Action: sts:AssumeRole
Resource: !GetAtt RoleAlice.Arn
AccessKeyAlice:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref UserAlice
RoleAlice:
Type: AWS::IAM::Role
Properties:
Description: !Sub "IAM role for the alice-${ClusterName} user"
RoleName: !Sub "alice-${ClusterName}"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: sts:AssumeRole
Outputs:
RoleAliceArn:
Description: The ARN of the Alice IAM Role
Value: !GetAtt RoleAlice.Arn
Export:
Name:
Fn::Sub: "${AWS::StackName}-RoleAliceArn"
AccessKeyAlice:
Description: The AccessKey for Alice user
Value: !Ref AccessKeyAlice
Export:
Name:
Fn::Sub: "${AWS::StackName}-AccessKeyAlice"
SecretAccessKeyAlice:
Description: The SecretAccessKey for Alice user
Value: !GetAtt AccessKeyAlice.SecretAccessKey
Export:
Name:
Fn::Sub: "${AWS::StackName}-SecretAccessKeyAlice"
EOF
eval aws cloudformation deploy --capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides "ClusterName=test-k8s" \
--stack-name "test-k8s-users" --template-file cf.yml
AWS_CLOUDFORMATION_DETAILS=$(aws cloudformation describe-stacks --stack-name "test-k8s-users")
ALICE_ROLE_ARN=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"RoleAliceArn\") .OutputValue")
ALICE_USER_ACCESSKEY=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"AccessKeyAlice\") .OutputValue")
ALICE_USER_SECRETACCESSKEY=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"SecretAccessKeyAlice\") .OutputValue")
eksctl create iamidentitymapping --cluster="test-k8s" --arn="${ALICE_ROLE_ARN}" --username alice --group capsule.clastix.io
cat > aws_config << EOF
[profile alice]
role_arn=${ALICE_ROLE_ARN}
source_profile=alice
EOF
cat > aws_credentials << EOF
[alice]
aws_access_key_id=${ALICE_USER_ACCESSKEY}
aws_secret_access_key=${ALICE_USER_SECRETACCESSKEY}
EOF
eksctl utils write-kubeconfig --cluster=test-k8s --kubeconfig="kubeconfig-alice.conf"
cat >> kubeconfig-alice.conf << EOF
- name: AWS_PROFILE
value: alice
- name: AWS_CONFIG_FILE
value: aws_config
- name: AWS_SHARED_CREDENTIALS_FILE
value: aws_credentials
EOF
```
----
Export "admin" kubeconfig to be able to install capsule:
```bash
export KUBECONFIG=kubeconfig.conf
```
Install capsule from helm chart:
```bash
helm repo add clastix https://clastix.github.io/charts
helm upgrade --install --version 0.0.19 --namespace capsule-system --create-namespace capsule clastix/capsule
```
Use the default Tenant example:
```bash
kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/samples/capsule_v1alpha1_tenant.yaml
```
Based on the tenant configuration above the user `alice` should be able
to create namespace...
Switch to new terminal tab and try to create namespace as user `alice`:
```bash
# Unset AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY if defined
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
kubectl create namespace test --kubeconfig="kubeconfig-alice.conf"
... do other commands allowed by Tenant configuration ...
```

View File

@@ -23,11 +23,11 @@ spec:
kind: User
```
> If you need to address specific use-case, the said annotation supports multiple values comme separated
>
> ```yaml
> capsule.clastix.io/allowed-image-pull-policy: Always,IfNotPresent
> ```
If you need to address specific use-case, the said annotation supports multiple values comma separated
```yaml
capsule.clastix.io/allowed-image-pull-policy: Always,IfNotPresent
```
# Whats next

View File

@@ -85,14 +85,14 @@ capsule-oil-0 <none> 42h
production-network-policy <none> 3m
```
an delete the namespace network-policies
And delete the namespace network policies
```
alice@caas# kubectl -n oil-production delete networkpolicy production-network-policy
```
However, the Capsule controller prevents Alice to delete the tenant network policy:
However, the Capsule controller prevents Alice from deleting the tenant network policy:
```
alice@caas# kubectl -n oil-production delete networkpolicy capsule-oil-0

View File

@@ -39,6 +39,7 @@ Bill, at Acme Corp. can use Capsule to address any of the following scenarios:
* [Taint Namespaces](./taint-namespaces.md)
* [Assign multiple Tenants to an owner](./multiple-tenants.md)
* [Cordoning a Tenant](./cordoning-tenant.md)
* [Velero Backup Restoration](./velero-backup-restoration.md)
> NB: as we improve Capsule, more use cases about multi-tenancy and cluster governance will be covered.

View File

@@ -1,7 +1,7 @@
# Assign permissions
Alice acts as the tenant admin. Other users can operate inside the tenant with different levels of permissions and authorizations. Alice is responsible for creating additional roles and assigning these roles to other users to work in the same tenant.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill has not to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill does not have to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
Capsule leaves Alice the freedom to create RBAC roles at the namespace level, or using the pre-defined cluster roles already available in Kubernetes, and assign them to other users in the tenant. Since roles and rolebindings are limited to a namespace scope, Alice can assign the roles to the other users accessing the same tenant only after the namespace is created. This gives Alice the power to administer the tenant without the intervention of the cluster admin.

View File

@@ -70,7 +70,7 @@ roleRef:
name: 'psp:privileged'
```
With the above example, Capsule is forbidding to any authenticated user in `oil-production` namespace to run privileged pods and let them to performs privilege escalation as declared by the Cluster Role `psp:privileged`.
With the above example, Capsule is forbidding any authenticated user in `oil-production` namespace to run privileged pods and to perform privilege escalation as declared by the Cluster Role `psp:privileged`.
# Whats next
See how Bill, the cluster admin, can assign to Alice the permissions to create custom resources in her tenant. [Create Custom Resources](./custom-resources.md).

View File

@@ -19,7 +19,7 @@ spec:
scopes:
- NotTerminating
- hard:
pods: "100"
pods: "10"
services: "50"
- hard:
requests.storage: 10Gi

View File

@@ -0,0 +1,27 @@
# Velero Backup Restoration
Velero is a backup system that perform disaster recovery, and migrate Kubernetes cluster resources and persistent volumes.
Using this in a Kubernetes cluster where Capsule is installed can lead to an incomplete restore of the cluster's Tenants. This is due to the fact that Velero omits the `ownerReferences` section from the tenant's namespace manifests when backup them.
To avoid this problem you can use the script `velero-restore.sh` under the `hack/` folder.
Below are some examples on how to use the script to avoid incomplete restorations.
## Getting Started
In case of a data loss, the right thing to do is to restore the cluster with **Velero** at first. Once Velero has finished, you can proceed using the script to complete the restoration.
```bash
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig restore
```
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
Additionally you can also specify a selected range of tenants to be restored:
```bash
./velero-restore.sh --tenant "gas oil" restore
```
In this way, only the tenants **gas** and **oil** will be restored.

View File

@@ -14,20 +14,22 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace with an additional Role Binding", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "additional-role-binding",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "dale",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "dale",
Kind: "User",
},
},
AdditionalRoleBindings: []v1alpha1.AdditionalRoleBindings{
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
@@ -55,13 +57,13 @@ var _ = Describe("creating a Namespace with an additional Role Binding", func()
It("should be assigned to each Namespace", func() {
for _, ns := range []string{"rb-1", "rb-2", "rb-3"} {
ns := NewNamespace(ns)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
var rb *rbacv1.RoleBinding
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
rb, err = cs.RbacV1().RoleBindings(ns.Name).Get(context.Background(), fmt.Sprintf("capsule-%s-0-%s", tnt.Name, "crds-rolebinding"), metav1.GetOptions{})
return err
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

View File

@@ -14,21 +14,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing an allowed set of Service external IPs", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-external-ip",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "google",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
ExternalServiceIPs: &v1alpha1.ExternalServiceIPs{
Allowed: []v1alpha1.AllowedIP{
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{
"10.20.0.0/16",
"192.168.1.2/32",
},
@@ -48,7 +50,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should fail creating an evil service", func() {
ns := NewNamespace("evil-service")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -73,7 +75,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())
@@ -81,7 +83,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should allow the first CIDR block", func() {
ns := NewNamespace("allowed-service-cidr")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -106,7 +108,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())
@@ -114,7 +116,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should allow the /32 CIDR block", func() {
ns := NewNamespace("allowed-service-strict")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -138,7 +140,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())

View File

@@ -14,20 +14,22 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing a Container Registry", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "container-registry",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "matt",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "matt",
Kind: "User",
},
},
ContainerRegistries: &v1alpha1.AllowedListSpec{
ContainerRegistries: &capsulev1beta1.AllowedListSpec{
Exact: []string{"docker.io", "docker.tld"},
Regex: `quay\.\w+`,
},
@@ -46,7 +48,7 @@ var _ = Describe("enforcing a Container Registry", func() {
It("should add labels to Namespace", func() {
ns := NewNamespace("registry-labels")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.Name}, ns)).Should(Succeed())
ok, _ = HaveKeyWithValue("capsule.clastix.io/allowed-registries", "docker.io,docker.tld").Match(ns.Annotations)
@@ -63,7 +65,7 @@ var _ = Describe("enforcing a Container Registry", func() {
It("should deny running a gcr.io container", func() {
ns := NewNamespace("registry-deny")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -78,14 +80,14 @@ var _ = Describe("enforcing a Container Registry", func() {
},
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
Expect(err).ShouldNot(Succeed())
})
It("should allow using an exact match", func() {
ns := NewNamespace("registry-list")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -101,7 +103,7 @@ var _ = Describe("enforcing a Container Registry", func() {
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err
@@ -110,7 +112,7 @@ var _ = Describe("enforcing a Container Registry", func() {
It("should allow using a regex match", func() {
ns := NewNamespace("registry-regex")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -126,7 +128,7 @@ var _ = Describe("enforcing a Container Registry", func() {
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err

View File

@@ -12,18 +12,22 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-group", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-assigned-custom-group",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",
},
},
},
}
@@ -39,33 +43,33 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
})
It("should fail using a User non matching the capsule-user-group flag", func() {
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"test"}
})
ns := NewNamespace("cg-namespace-fail")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
It("should succeed and be available in Tenant namespaces list with multiple groups", func() {
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"test", "alice"}
})
ns := NewNamespace("cg-namespace-1")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
})
It("should succeed and be available in Tenant namespaces list with default single group", func() {
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"capsule.clastix.io"}
})
ns := NewNamespace("cg-namespace-2")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
})
})

View File

@@ -0,0 +1,108 @@
//+build e2e
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating an ExternalName service when it is disabled for Tenant", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-external-service",
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
ExternalName: pointer.BoolPtr(false),
},
},
},
}
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 a service with ExternalService type", func() {
ns := NewNamespace("disable-external-service")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
EventuallyCreation(func() error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-ip",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Ports: []corev1.ServicePort{
{
Port: 8888,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 8888,
},
Protocol: corev1.ProtocolTCP,
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())
EventuallyCreation(func() error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-external-service",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeExternalName,
Ports: []corev1.ServicePort{
{
Port: 9999,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 9999,
},
Protocol: corev1.ProtocolTCP,
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())
})
})

View File

@@ -13,22 +13,27 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a nodePort service when it is disabled for Tenant", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-node-ports",
Annotations: map[string]string{
"capsule.clastix.io/enable-node-ports": "false",
},
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "google",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
},
},
},
}
@@ -45,7 +50,7 @@ var _ = Describe("creating a nodePort service when it is disabled for Tenant", f
It("should fail creating a service with NodePort type", func() {
ns := NewNamespace("disable-node-ports")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -67,7 +72,7 @@ var _ = Describe("creating a nodePort service when it is disabled for Tenant", f
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())

View File

@@ -14,18 +14,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a nodePort service when it is enabled for Tenant", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "enable-node-ports",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "google",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
},
}
@@ -42,7 +44,7 @@ var _ = Describe("creating a nodePort service when it is enabled for Tenant", fu
It("should allow creating a service with NodePort type", func() {
ns := NewNamespace("enable-node-ports")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -64,7 +66,7 @@ var _ = Describe("creating a nodePort service when it is enabled for Tenant", fu
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())

View File

@@ -12,29 +12,35 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace with Tenant name prefix enforcement", func() {
t1 := &v1alpha1.Tenant{
t1 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
t2 := &v1alpha1.Tenant{
t2 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome-tenant",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
@@ -49,7 +55,7 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", fun
return k8sClient.Create(context.TODO(), t2)
}).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.ForceTenantPrefix = true
})
})
@@ -57,27 +63,27 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", fun
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.ForceTenantPrefix = false
})
})
It("should fail when non using prefix", func() {
ns := NewNamespace("awesome")
NamespaceCreation(ns, t1, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
})
It("should succeed using prefix", func() {
ns := NewNamespace("awesome-namespace")
NamespaceCreation(ns, t1, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
})
It("should succeed and assigned according to closest match", func() {
ns1 := NewNamespace("awesome-tenant")
ns2 := NewNamespace("awesome-tenant-namespace")
NamespaceCreation(ns1, t1, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns2, t2, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns1, t1.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns2, t2.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(t1, defaultTimeoutInterval).Should(ContainElement(ns1.GetName()))
TenantNamespaceList(t2, defaultTimeoutInterval).Should(ContainElement(ns2.GetName()))

View File

@@ -13,22 +13,22 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing some defined ImagePullPolicy", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "image-pull-policies",
Annotations: map[string]string{
"capsule.clastix.io/allowed-image-pull-policy": "Always,IfNotPresent",
},
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "alex",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alex",
Kind: "User",
},
},
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"},
},
}
@@ -45,9 +45,9 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
It("should just allow the defined policies", func() {
ns := NewNamespace("allow-policy")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
By("allowing Always", func() {
pod := &corev1.Pod{
@@ -57,8 +57,8 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullAlways,
},
},
@@ -72,7 +72,6 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
}).Should(Succeed())
})
By("allowing IfNotPresent", func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -81,8 +80,8 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
@@ -104,8 +103,8 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullNever,
},
},

View File

@@ -13,22 +13,22 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing a defined ImagePullPolicy", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "image-pull-policy",
Annotations: map[string]string{
"capsule.clastix.io/allowed-image-pull-policy": "Always",
},
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "axel",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "axel",
Kind: "User",
},
},
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always"},
},
}
@@ -45,9 +45,9 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
It("should just allow the defined policy", func() {
ns := NewNamespace("allow-policies")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
By("allowing Always", func() {
pod := &corev1.Pod{
@@ -57,8 +57,8 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullAlways,
},
},
@@ -72,7 +72,6 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
}).Should(Succeed())
})
By("blocking IfNotPresent", func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -81,8 +80,8 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
@@ -104,8 +103,8 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
Name: "container",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullNever,
},
},

View File

@@ -15,20 +15,22 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when Tenant handles Ingress classes", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-class",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "ingress",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress",
Kind: "User",
},
},
IngressClasses: &v1alpha1.AllowedListSpec{
IngressClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{
"nginx",
"haproxy",
@@ -50,9 +52,9 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
It("should block a non allowed class", func() {
ns := NewNamespace("ingress-class-disallowed")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("non-specifying at all", func() {
@@ -114,9 +116,9 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
It("should allow enabled class using the deprecated annotation", func() {
ns := NewNamespace("ingress-class-allowed-annotation")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
for _, c := range tnt.Spec.IngressClasses.Exact {
@@ -143,14 +145,14 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
It("should allow enabled class using the ingressClassName field", func() {
ns := NewNamespace("ingress-class-allowed-annotation")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min < 18 {
Skip("Running test on Kubernetes " + v + ", doesn't provide .spec.ingressClassName")
}
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
for _, c := range tnt.Spec.IngressClasses.Exact {
@@ -175,10 +177,10 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
It("should allow enabled Ingress by regex using the deprecated annotation", func() {
ns := NewNamespace("ingress-class-allowed-annotation")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
ingressClass := "oil-ingress"
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
@@ -203,7 +205,7 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
It("should allow enabled Ingress by regex using the ingressClassName field", func() {
ns := NewNamespace("ingress-class-allowed-annotation")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
ingressClass := "oil-haproxy"
maj, min, v := GetKubernetesSemVer()
@@ -211,7 +213,7 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
Skip("Running test on Kubernetes " + v + ", doesn't provide .spec.ingressClassName")
}
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {

View File

@@ -14,18 +14,22 @@ import (
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when handling Ingress hostnames collision", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-hostnames-allowed-collision",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "ingress-allowed",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress-allowed",
Kind: "User",
},
},
},
}
@@ -68,54 +72,100 @@ var _ = Describe("when handling Ingress hostnames collision", func() {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowIngressHostnameCollision = true
})
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowIngressHostnameCollision = false
})
})
It("should not allow creating several Ingress with same hostname", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowIngressHostnameCollision = false
})
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("denied-collision")
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
}
if maj == 1 && min < 22 {
By("testing extensions", func() {
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}).ShouldNot(Succeed())
})
}
})
It("should allow creating several Ingress with same hostname", func() {
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("allowed-collision")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
Eventually(func() (err error) {
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-1", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
Eventually(func() (err error) {
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := networkingIngress("networking-2", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}).Should(Succeed())
})
}
if maj == 1 && min < 22 {
By("testing extensions", func() {
Eventually(func() (err error) {
obj := extensionsIngress("extensions-1", "kubernetes.io")
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-1", "cncf.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
Eventually(func() (err error) {
obj := extensionsIngress("extensions-2", "kubernetes.io")
}).Should(Succeed())
EventuallyCreation(func() (err error) {
obj := extensionsIngress("extensions-2", "cncf.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}).Should(Succeed())
})
}
})

View File

@@ -14,18 +14,22 @@ import (
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when handling Ingress hostnames collision", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-hostnames-denied-collision",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "ingress-denied",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress-denied",
Kind: "User",
},
},
},
}
@@ -67,14 +71,14 @@ var _ = Describe("when handling Ingress hostnames collision", func() {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowIngressHostnameCollision = true
})
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowIngressHostnameCollision = false
})
})
@@ -83,9 +87,9 @@ var _ = Describe("when handling Ingress hostnames collision", func() {
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("allowed-collision")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {

View File

@@ -16,21 +16,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when Tenant handles Ingress hostnames", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-hostnames",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "hostname",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "hostname",
Kind: "User",
},
},
IngressHostnames: &v1alpha1.AllowedListSpec{
Exact: []string{"sigs.k8s.io", "operator.sdk", "domain.tld"},
IngressHostnames: &capsulev1beta1.AllowedListSpec{
Exact: []string{"sigs.k8s.io", "operator.sdk", "domain.tld"},
Regex: `.*\.clastix\.io`,
},
},
@@ -113,15 +115,14 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should block a non allowed Hostname", func() {
maj, min, v := GetKubernetesSemVer()
if maj == 1 && min > 18 {
ns := NewNamespace("disallowed-hostname-networking")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
@@ -144,9 +145,9 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
if maj == 1 && min < 22 {
By("testing extensions", func() {
ns := NewNamespace("disallowed-hostname-extensions")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
@@ -167,9 +168,9 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
if maj == 1 && min > 18 {
ns := NewNamespace("allowed-hostname-list-networking")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
@@ -193,9 +194,9 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
if maj == 1 && min < 22 {
ns := NewNamespace("allowed-hostname-list-extensions")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing extensions", func() {
@@ -219,9 +220,9 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
if maj == 1 && min > 18 {
ns := NewNamespace("allowed-hostname-regex-networking")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("testing networking.k8s.io", func() {
@@ -246,9 +247,9 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
if maj == 1 && min < 22 {
By("testing extensions", func() {
ns := NewNamespace("allowed-hostname-regex-extensions")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
for _, h := range []string{"foo", "bar", "bizz"} {

View File

@@ -12,21 +12,23 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace creation with no Tenant assigned", func() {
It("should fail", func() {
tnt := &v1alpha1.Tenant{
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "missing",
Kind: "User",
tnt := &capsulev1beta1.Tenant{
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "missing",
Kind: "User",
},
},
},
}
ns := NewNamespace("no-namespace")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
Expect(err).ShouldNot(Succeed())
})

View File

@@ -14,18 +14,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating several Namespaces for a Tenant", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "capsule-labels",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "charlie",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "charlie",
Kind: "User",
},
},
},
}
@@ -47,7 +49,7 @@ var _ = Describe("creating several Namespaces for a Tenant", func() {
NewNamespace("third-capsule-ns"),
}
for _, ns := range namespaces {
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
ok, _ = HaveKeyWithValue("capsule.clastix.io/tenant", tnt.Name).Match(ns.Labels)

View File

@@ -13,20 +13,22 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "gatsby",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{
NamespacesMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
@@ -50,7 +52,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f
It("should contain additional Namespace metadata", func() {
ns := NewNamespace("namespace-metadata")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("checking additional labels", func() {

View File

@@ -12,24 +12,35 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace as Tenant owner", func() {
tnt := &v1alpha1.Tenant{
var _ = Describe("creating a Namespaces as different type of Tenant owners", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-assigned",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",
},
{
Name: "bob",
Kind: "Group",
},
{
Name: "system:serviceaccount:new-namespace-sa:default",
Kind: "ServiceAccount",
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
@@ -37,9 +48,28 @@ var _ = Describe("creating a Namespace as Tenant owner", func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should be available in Tenant namespaces list", func() {
ns := NewNamespace("new-namespace")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
It("should be available in Tenant namespaces list and rolebindigs should present when created as User", func() {
ns := NewNamespace("new-namespace-user")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
for _, a := range KindInTenantRoleBindingAssertions(ns, defaultTimeoutInterval) {
a.Should(ContainElements("User", "Group", "ServiceAccount"))
}
})
It("should be available in Tenant namespaces list and rolebindigs should present when created as Group", func() {
ns := NewNamespace("new-namespace-group")
NamespaceCreation(ns, tnt.Spec.Owners[1], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
for _, a := range KindInTenantRoleBindingAssertions(ns, defaultTimeoutInterval) {
a.Should(ContainElements("User", "Group", "ServiceAccount"))
}
})
It("should be available in Tenant namespaces list and rolebindigs should present when created as ServiceAccount", func() {
ns := NewNamespace("new-namespace-sa")
NamespaceCreation(ns, tnt.Spec.Owners[2], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
for _, a := range KindInTenantRoleBindingAssertions(ns, defaultTimeoutInterval) {
a.Should(ContainElements("User", "Group", "ServiceAccount"))
}
})
})

View File

@@ -13,18 +13,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace in over-quota of three", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "over-quota-tenant",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "bob",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "bob",
Kind: "User",
},
},
NamespaceQuota: pointer.Int32Ptr(3),
},
@@ -43,13 +45,13 @@ var _ = Describe("creating a Namespace in over-quota of three", func() {
By("creating three Namespaces", func() {
for _, name := range []string{"bob-dev", "bob-staging", "bob-production"} {
ns := NewNamespace(name)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
}
})
ns := NewNamespace("bob-fail")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
Expect(err).ShouldNot(Succeed())
})

View File

@@ -17,26 +17,28 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when Tenant owner interacts with the webhooks", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-owner",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "ruby",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ruby",
Kind: "User",
},
},
StorageClasses: &v1alpha1.AllowedListSpec{
StorageClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{
"cephfs",
"glusterfs",
},
},
LimitRanges: []corev1.LimitRangeSpec{
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
{
Limits: []corev1.LimitRangeItem{
{
@@ -53,7 +55,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
},
},
},
NetworkPolicies: []networkingv1.NetworkPolicySpec{
},
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{Items: []networkingv1.NetworkPolicySpec{
{
Egress: []networkingv1.NetworkPolicyEgressRule{
{
@@ -73,13 +76,15 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
},
},
},
ResourceQuota: []corev1.ResourceQuotaSpec{
},
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
{
Hard: map[corev1.ResourceName]resource.Quantity{
corev1.ResourcePods: resource.MustParse("10"),
},
},
},
},
},
}
JustBeforeEach(func() {
@@ -95,7 +100,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
It("should disallow deletions", func() {
By("blocking Capsule Limit ranges", func() {
ns := NewNamespace("limit-range-disallow")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
lr := &corev1.LimitRange{}
@@ -104,12 +109,12 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: n, Namespace: ns.GetName()}, lr)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
Expect(cs.CoreV1().LimitRanges(ns.GetName()).Delete(context.TODO(), lr.Name, metav1.DeleteOptions{})).ShouldNot(Succeed())
})
By("blocking Capsule Network Policy", func() {
ns := NewNamespace("network-policy-disallow")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
np := &networkingv1.NetworkPolicy{}
@@ -118,12 +123,12 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: n, Namespace: ns.GetName()}, np)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
Expect(cs.NetworkingV1().NetworkPolicies(ns.GetName()).Delete(context.TODO(), np.Name, metav1.DeleteOptions{})).ShouldNot(Succeed())
})
By("blocking Capsule Resource Quota", func() {
ns := NewNamespace("resource-quota-disallow")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
rq := &corev1.ResourceQuota{}
@@ -132,7 +137,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: n, Namespace: ns.GetName()}, rq)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
Expect(cs.NetworkingV1().NetworkPolicies(ns.GetName()).Delete(context.TODO(), rq.Name, metav1.DeleteOptions{})).ShouldNot(Succeed())
})
})
@@ -140,33 +145,33 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
It("should allow", func() {
By("listing Limit Range", func() {
ns := NewNamespace("limit-range-list")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err = cs.CoreV1().LimitRanges(ns.GetName()).List(context.TODO(), metav1.ListOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("listing Network Policy", func() {
ns := NewNamespace("network-policy-list")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err = cs.NetworkingV1().NetworkPolicies(ns.GetName()).List(context.TODO(), metav1.ListOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("listing Resource Quota", func() {
ns := NewNamespace("resource-quota-list")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err = cs.NetworkingV1().NetworkPolicies(ns.GetName()).List(context.TODO(), metav1.ListOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
@@ -175,15 +180,15 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
It("should allow all actions to Tenant owner Network Policy", func() {
ns := NewNamespace("network-policy-allow")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
np := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "custom-network-policy",
},
Spec: tnt.Spec.NetworkPolicies[0],
Spec: tnt.Spec.NetworkPolicies.Items[0],
}
By("creating", func() {
Eventually(func() (err error) {

View File

@@ -8,27 +8,30 @@ package e2e
import (
"context"
"github.com/clastix/capsule/api/v1alpha1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/scheduling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing a Priority Class", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "priority-class",
Annotations: map[string]string{
"priorityclass.capsule.clastix.io/allowed": "gold",
"priorityclass.capsule.clastix.io/allowed-regex": "pc\\-\\w+",
},
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "george",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "george",
Kind: "User",
},
},
PriorityClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{"gold"},
Regex: "pc\\-\\w+",
},
},
}
@@ -45,7 +48,7 @@ var _ = Describe("enforcing a Priority Class", func() {
It("should block non allowed Priority Class", func() {
ns := NewNamespace("system-node-critical")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -62,7 +65,7 @@ var _ = Describe("enforcing a Priority Class", func() {
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{})
return err
@@ -84,7 +87,7 @@ var _ = Describe("enforcing a Priority Class", func() {
}()
ns := NewNamespace("pc-exact-match")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -101,7 +104,7 @@ var _ = Describe("enforcing a Priority Class", func() {
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{})
return err
@@ -111,9 +114,9 @@ var _ = Describe("enforcing a Priority Class", func() {
It("should allow regex match", func() {
ns := NewNamespace("pc-regex-match")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
for i, pc := range []string{"pc-bronze", "pc-silver", "pc-gold"} {
for i, pc := range []string{"pc-bronze", "pc-silver", "pc-gold"} {
class := &v1.PriorityClass{
ObjectMeta: metav1.ObjectMeta{
Name: pc,
@@ -139,7 +142,7 @@ var _ = Describe("enforcing a Priority Class", func() {
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{})

View File

@@ -12,18 +12,22 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace with a protected Namespace regex enabled", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-protected-namespace",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",
},
},
},
}
@@ -39,21 +43,21 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled"
})
It("should succeed and be available in Tenant namespaces list", func() {
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.ProtectedNamespaceRegexpString = `^.*[-.]system$`
})
ns := NewNamespace("test-ok")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
})
It("should fail using a value non matching the regex", func() {
ns := NewNamespace("test-system")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.ProtectedNamespaceRegexpString = ""
})
})

View File

@@ -18,20 +18,22 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("exceeding a Tenant resource quota", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-resources-changes",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "bobby",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "bobby",
Kind: "User",
},
},
LimitRanges: []corev1.LimitRangeSpec{
LimitRanges: &capsulev1beta1.LimitRangesSpec{Items: []corev1.LimitRangeSpec{
{
Limits: []corev1.LimitRangeItem{
{
@@ -76,7 +78,8 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
},
},
},
ResourceQuota: []corev1.ResourceQuotaSpec{
},
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{Items: []corev1.ResourceQuotaSpec{
{
Hard: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceLimitsCPU: resource.MustParse("8"),
@@ -99,6 +102,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
},
},
},
},
},
}
@@ -111,7 +115,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
By("creating the Namespaces", func() {
for _, i := range nsl {
ns := NewNamespace(i)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
}
})
@@ -121,7 +125,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
})
It("should block new Pods", func() {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
for _, namespace := range nsl {
Eventually(func() (err error) {
d := &appsv1.Deployment{
@@ -184,7 +188,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
},
},
}
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns).Create(context.Background(), pod, metav1.CreateOptions{})
return err

View File

@@ -13,18 +13,20 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace trying to select a third Tenant", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-non-owned",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "undefined",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "undefined",
Kind: "User",
},
},
},
}
@@ -42,7 +44,7 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func()
var ns *corev1.Namespace
By("assigning to the Namespace the Capsule Tenant label", func() {
l, err := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
l, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
Expect(err).ToNot(HaveOccurred())
ns := NewNamespace("tenant-non-owned-ns")
@@ -51,7 +53,7 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func()
})
})
cs := ownerClient(&v1alpha1.Tenant{Spec: v1alpha1.TenantSpec{Owner: v1alpha1.OwnerSpec{Name: "dale", Kind: "User"}}})
cs := ownerClient(capsulev1beta1.OwnerSpec{Name: "dale", Kind: "User"})
_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
Expect(err).To(HaveOccurred())
})

View File

@@ -12,51 +12,59 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace without a Tenant selector when user owns multiple Tenants", func() {
t1 := &v1alpha1.Tenant{
t1 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-one",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
t2 := &v1alpha1.Tenant{
t2 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-two",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
t3 := &v1alpha1.Tenant{
t3 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-three",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "Group",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "Group",
},
},
},
}
t4 := &v1alpha1.Tenant{
t4 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-four",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "Group",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "Group",
},
},
},
}
@@ -64,31 +72,31 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
It("should fail", func() {
ns := NewNamespace("fail-ns")
By("user owns 2 tenants", func() {
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t1)}).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t2)}).Should(Succeed())
NamespaceCreation(ns, t1, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t2, defaultTimeoutInterval).ShouldNot(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t1) }).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t2) }).Should(Succeed())
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
})
By("group owns 2 tenants", func() {
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t3)}).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t4)}).Should(Succeed())
NamespaceCreation(ns, t3, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t4, defaultTimeoutInterval).ShouldNot(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t3) }).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t4) }).Should(Succeed())
NamespaceCreation(ns, t3.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t4.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t4)).Should(Succeed())
})
By("user and group owns 4 tenants", func() {
t1.ResourceVersion, t2.ResourceVersion, t3.ResourceVersion, t4.ResourceVersion = "", "", "", ""
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t1)}).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t2)}).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t3)}).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t4)}).Should(Succeed())
NamespaceCreation(ns, t1, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t2, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t3, defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t4, defaultTimeoutInterval).ShouldNot(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t1) }).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t2) }).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t3) }).Should(Succeed())
EventuallyCreation(func() error { return k8sClient.Create(context.TODO(), t4) }).Should(Succeed())
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t3.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
NamespaceCreation(ns, t4.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed())

View File

@@ -12,29 +12,33 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace with Tenant selector when user owns multiple tenants", func() {
t1 := &v1alpha1.Tenant{
t1 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-one",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
t2 := &v1alpha1.Tenant{
t2 := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-two",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
},
},
},
}
@@ -55,13 +59,13 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi
It("should be assigned to the selected Tenant", func() {
ns := NewNamespace("tenant-2-ns")
By("assigning to the Namespace the Capsule Tenant label", func() {
l, err := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
l, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
Expect(err).ToNot(HaveOccurred())
ns.Labels = map[string]string{
l: t2.Name,
}
})
NamespaceCreation(ns, t2, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(t2, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
})
})

View File

@@ -18,30 +18,34 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("adding metadata to Service objects", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "service-metadata",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "gatsby",
Kind: "User",
},
ServicesMetadata: v1alpha1.AdditionalMetadata{
AdditionalLabels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
AdditionalAnnotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
AdditionalRoleBindings: []v1alpha1.AdditionalRoleBindings{
ServiceOptions: &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
AdditionalAnnotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
},
},
},
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "system:controller:endpointslice-controller",
Subjects: []rbacv1.Subject{
@@ -67,7 +71,7 @@ var _ = Describe("adding metadata to Service objects", func() {
It("should apply them to Service", func() {
ns := NewNamespace("service-metadata")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
svc := &corev1.Service{
@@ -96,7 +100,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: svc.GetName(), Namespace: ns.GetName()}, svc)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(svc.Labels)
if !ok {
return false
@@ -108,7 +112,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: svc.GetName(), Namespace: ns.GetName()}, svc)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(svc.Annotations)
if !ok {
return false
@@ -121,7 +125,7 @@ var _ = Describe("adding metadata to Service objects", func() {
It("should apply them to Endpoints", func() {
ns := NewNamespace("endpoints-metadata")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
ep := &corev1.Endpoints{
@@ -151,7 +155,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ep.GetName(), Namespace: ns.GetName()}, ep)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(ep.Labels)
if !ok {
return false
@@ -163,7 +167,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ep.GetName(), Namespace: ns.GetName()}, ep)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(ep.Annotations)
if !ok {
return false
@@ -181,7 +185,7 @@ var _ = Describe("adding metadata to Service objects", func() {
}
ns := NewNamespace("endpointslice-metadata")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
eps := &discoveryv1beta1.EndpointSlice{
@@ -210,7 +214,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations EndpointSlice", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: eps.GetName(), Namespace: ns.GetName()}, eps)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(eps.Annotations)
if !ok {
return false
@@ -222,7 +226,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels on EndpointSlice", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: eps.GetName(), Namespace: ns.GetName()}, eps)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(eps.Labels)
if !ok {
return false

View File

@@ -15,20 +15,22 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when Tenant handles Storage classes", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "storage-class",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "storage",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "storage",
Kind: "User",
},
},
StorageClasses: &v1alpha1.AllowedListSpec{
StorageClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{
"cephfs",
"glusterfs",
@@ -50,12 +52,12 @@ var _ = Describe("when Tenant handles Storage classes", func() {
It("should fails", func() {
ns := NewNamespace("storage-class-disallowed")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("non-specifying it", func() {
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
p := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "denied-pvc",
@@ -75,7 +77,7 @@ var _ = Describe("when Tenant handles Storage classes", func() {
})
By("specifying a forbidden one", func() {
Eventually(func() (err error) {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
p := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "mighty-storage",
@@ -97,9 +99,9 @@ var _ = Describe("when Tenant handles Storage classes", func() {
It("should allow", func() {
ns := NewNamespace("storage-class-allowed")
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("using exact matches", func() {
for _, c := range tnt.Spec.StorageClasses.Exact {

View File

@@ -21,7 +21,8 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
capsulev1alpha "github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
@@ -34,12 +35,6 @@ var (
tenantRoleBindingNames = []string{"namespace:admin", "namespace-deleter"}
)
const (
capsuleDeploymentName = "capsule-controller-manager"
capsuleNamespace = "capsule-system"
capsuleManagerContainerName = "manager"
)
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
@@ -64,7 +59,10 @@ var _ = BeforeSuite(func(done Done) {
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = capsulev1alpha.AddToScheme(scheme.Scheme)
err = capsulev1beta1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = capsulev1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
@@ -79,11 +77,11 @@ var _ = AfterSuite(func() {
Expect(testEnv.Stop()).ToNot(HaveOccurred())
})
func ownerClient(tenant *capsulev1alpha.Tenant) (cs kubernetes.Interface) {
func ownerClient(owner capsulev1beta1.OwnerSpec) (cs kubernetes.Interface) {
c, err := config.GetConfig()
Expect(err).ToNot(HaveOccurred())
c.Impersonate.Groups = []string{capsulev1alpha.GroupVersion.Group, tenant.Spec.Owner.Name}
c.Impersonate.UserName = tenant.Spec.Owner.Name
c.Impersonate.Groups = []string{capsulev1beta1.GroupVersion.Group, owner.Name}
c.Impersonate.UserName = owner.Name
cs, err = kubernetes.NewForConfig(c)
Expect(err).ToNot(HaveOccurred())
return

View File

@@ -9,23 +9,26 @@ import (
"context"
"time"
"github.com/clastix/capsule/api/v1alpha1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("cordoning a Tenant", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-cordoning",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "jim",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "jim",
Kind: "User",
},
},
},
}
@@ -41,7 +44,7 @@ var _ = Describe("cordoning a Tenant", func() {
})
It("should block or allow operations", func() {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
ns := NewNamespace("cordoned-namespace")
@@ -60,7 +63,7 @@ var _ = Describe("cordoning a Tenant", func() {
}
By("creating a Namespace", func() {
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})

View File

@@ -13,20 +13,23 @@ import (
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("when a second Tenant contains an already declared allowed Ingress hostname", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-collision-ingress-hostnames",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "first-user",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "first-user",
Kind: "User",
},
},
IngressHostnames: &v1alpha1.AllowedListSpec{
IngressHostnames: &capsulev1beta1.AllowedListSpec{
Exact: []string{"capsule.clastix.io", "docs.capsule.k8s", "42.clatix.io"},
},
},
@@ -37,39 +40,91 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowTenantIngressHostnamesCollision = true
})
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
ModifyCapsuleConfigurationOpts(func(configuration *v1alpha1.CapsuleConfiguration) {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowTenantIngressHostnamesCollision = false
})
})
It("should not block creation if contains collided Ingress hostnames", func() {
It("should block creation if contains collided Ingress hostnames", func() {
var cleanupFuncs []func()
for i, h := range tnt.Spec.IngressHostnames.Exact {
tnt2 := &v1alpha1.Tenant{
duplicated := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", tnt.GetName(), i),
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "second-user",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "second-user",
Kind: "User",
},
},
IngressHostnames: &v1alpha1.AllowedListSpec{
IngressHostnames: &capsulev1beta1.AllowedListSpec{
Exact: []string{h},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt2)
return k8sClient.Create(context.TODO(), duplicated)
}).ShouldNot(Succeed())
cleanupFuncs = append(cleanupFuncs, func() {
duplicatedTnt := *duplicated
_ = k8sClient.Delete(context.TODO(), &duplicatedTnt)
})
}
for _, fn := range cleanupFuncs {
fn()
}
})
It("should not block creation if contains collided Ingress hostnames", func() {
var cleanupFuncs []func()
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1alpha1.CapsuleConfiguration) {
configuration.Spec.AllowTenantIngressHostnamesCollision = true
})
for i, h := range tnt.Spec.IngressHostnames.Exact {
duplicated := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", tnt.GetName(), i),
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "second-user",
Kind: "User",
},
},
IngressHostnames: &capsulev1beta1.AllowedListSpec{
Exact: []string{h},
},
},
}
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), duplicated)
}).Should(Succeed())
_ = k8sClient.Delete(context.TODO(), tnt2)
cleanupFuncs = append(cleanupFuncs, func() {
duplicatedTnt := *duplicated
_ = k8sClient.Delete(context.TODO(), &duplicatedTnt)
})
}
for _, fn := range cleanupFuncs {
fn()
}
})
})

Some files were not shown because too many files have changed in this diff Show More