Compare commits

...

156 Commits

Author SHA1 Message Date
Oliver Bähler
5c7804e1bf fix: add rolebinding validation against rfc-1123 dns for sa subjects
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2021-11-12 11:22:26 +01:00
Oliver Bähler
c4481f26f7 docs: additions to dev-guide
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2021-11-12 11:22:26 +01:00
Maksim Fedotov
ec715d2e8f fix: do not register tenant controller\webhook\indexer until CA is created 2021-11-06 16:34:22 +01:00
Luca Spezzano
0aeaf89cb7 fix(docs): broken links and style, deleted command code from MD file 2021-11-06 16:30:34 +01:00
Dario Tranchitella
3d31ddb4e3 docs: instructions on how to develop the docs website 2021-11-06 16:30:34 +01:00
Luca Spezzano
e83f344cdc feat(docs): removed meta robots and added meta og:url 2021-11-06 16:30:34 +01:00
Luca Spezzano
da83a8711a style(docs): added blockquote style 2021-11-06 16:30:34 +01:00
Luca Spezzano
43a944ace0 feat(docs): created 404 default page 2021-11-06 16:30:34 +01:00
Luca Spezzano
0acc2d2ef1 feat(docs): setup Gridsome for the website 2021-11-06 16:30:34 +01:00
Maxim Fedotov
14f9686bbb Forbidden node labels and annotations (#464)
* feat: forbidden node labels and annotations

* test(e2e): forbidden node labels and annotations

* build(kustomize): forbidden node labels and annotations

* build(helm): forbidden node labels and annotations

* build(installer): forbidden node labels and annotations

* chore(make): forbidden node labels and annotations

* docs: forbidden node labels and annotations

* test(e2e): forbidden node labels and annotations. Use EventuallyCreation func

* feat: forbidden node labels and annotations. Check kubernetes version

* test(e2e): forbidden node labels and annotations. Check kubernetes version

* docs: forbidden node labels and annotations. Version restrictions

* feat: forbidden node labels and annotations. Do not update deepcopy functions

* docs: forbidden node labels and annotations. Use blockquotes for notes

Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
2021-11-02 20:01:53 +03:00
Dario Tranchitella
6ba9826c51 chore(linters): no more need of duplicate check 2021-11-02 17:13:23 +01:00
Dario Tranchitella
bd58084ded docs!: container registry enforcement required fqci 2021-11-02 17:13:23 +01:00
Dario Tranchitella
3a5e50886d test: fqci is required for containar registry enforcement 2021-11-02 17:13:23 +01:00
Dario Tranchitella
e2768dad83 fix!: forcing to use fqci and container registries with no repositories 2021-11-02 17:13:23 +01:00
Vivek Singh
b97c23176d fix: duplicate release for helm chart
this commit remote helm release workflow trigger on create which triggers duplicate event as push

fixes: #459
2021-11-02 17:13:10 +01:00
Dario Tranchitella
fa8e805842 build(ci): triggering e2e also for nested files 2021-10-28 17:53:17 +02:00
Dario Tranchitella
8df66fc232 test: resources are no more pointers 2021-10-28 17:53:17 +02:00
Dario Tranchitella
c2218912eb fix: pointer doesn't trigger resources pruning 2021-10-28 17:53:17 +02:00
Tom OBrien
e361e2d424 fix: allowing regex underscore for container registry enforcement
While not best practice, underscore can be used and so should be allowed.
2021-10-27 20:55:39 +02:00
Dario Tranchitella
260b60d263 build(helm): bumping up to new Helm version 2021-10-24 17:04:58 +02:00
maxgio
e0d5e6feb2 Refactor helper script to create a Capsule user (#454)
* chore(hack/create-user.sh): let pick bash interpreter from path

bash interpreter binary could be put at different paths than /bin/bash.

Signed-off-by: maxgio92 <massimiliano.giovagnoli.1992@gmail.com>

* refactor(hack/create-user.sh): add helper function to apply dry

add helper function to check commands existence.

Signed-off-by: maxgio92 <massimiliano.giovagnoli.1992@gmail.com>
2021-10-22 20:55:52 +02:00
Adriano Pezzuto
0784dc7177 docs: add service account group to Capsule group (#450) 2021-10-15 14:57:55 +02:00
Vivek Kumar Singh
b17c6c4636 fix(helm): do not hardcode namespace forwebhook configs 2021-10-07 16:14:22 +02:00
Bright Zheng
52cf597041 docs: use one patch for each webhook 2021-10-02 17:13:20 +02:00
Bright Zheng
b8dcded882 docs: add dev env diagram 2021-10-02 17:13:20 +02:00
Bright Zheng
6a175e9017 docs: explicitly add the contribution section 2021-10-02 17:13:20 +02:00
Bright Zheng
3c609f84db docs: tune the dev setup process 2021-10-02 17:13:20 +02:00
Bright Zheng
7c3a59c4e4 feat: ignore vscode 2021-10-02 17:13:20 +02:00
Bright Zheng
d3e3b8a881 docs: review and enhance dev guide 2021-09-30 21:26:31 +02:00
Bright Zheng
7a8148bd58 docs: add dev guide 2021-09-30 21:26:31 +02:00
Bright Zheng
405d3ac52d docs: move and refactor contributing.md 2021-09-30 21:26:31 +02:00
Bright Zheng
f92acf9a9d fix: correct the make run issue 2021-09-30 21:26:31 +02:00
Pietro Terrizzi
bbb7b850d6 fix: avoid CRD reinstall 2021-09-30 21:16:04 +02:00
Maksim Fedotov
0f7284d190 fix(helm): remove matchExpressions selector from ingresses webhook 2021-09-29 09:59:12 +02:00
Alessio Greggi
7db263b2b6 fix(documentation): add link to use case velero backup restoration 2021-09-23 18:34:46 +02:00
Alessio Greggi
0a8f50f761 docs(operator): add documentation for deny wildcard hostnames 2021-09-23 18:34:46 +02:00
Gonzalo Gabriel Jiménez Fuentes
7a66e8ea93 ci: limit e2e tests to specific paths 2021-09-23 17:57:25 +02:00
Gonzalo Gabriel Jiménez Fuentes
b5eb03ea76 chore: adding auto-generated code 2021-09-23 17:57:25 +02:00
Gonzalo Gabriel Jiménez Fuentes
681b514516 ci: allowing tag creation as trigger to push helm chart 2021-09-23 17:57:25 +02:00
Maksim Fedotov
b28b98a7bc feat: namespace labeling for tenant owners. fix linting issues 2021-09-23 14:10:24 +02:00
Maksim Fedotov
f6bf0ca446 build(installer): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
1081bad7cb docs: namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
79372c7332 build(helm): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
4e8faaf845 build(kustomize): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
d1b008972c test(e2e): namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Maksim Fedotov
a14c7609df feat: namespace labeling for tenant owners 2021-09-23 14:10:24 +02:00
Gonzalo Gabriel Jiménez Fuentes
03456c0b54 fix(ci): allowing tag creation as trigger to push helm chart 2021-09-23 14:01:57 +02:00
Maksim Fedotov
ddfe2219a0 build(helm): update chart version 2021-09-23 11:39:43 +02:00
Maksim Fedotov
6b68363a46 build(helm): additional webhook configuration in chart 2021-09-23 11:39:43 +02:00
alegrey91
357834c5b9 refactor(test): switch from kubernetes version control to NoKindMatchError 2021-09-21 19:14:49 +02:00
Dario Tranchitella
085d9f6503 test(e2e): disabled Ingress wildcard annotation 2021-09-21 19:14:49 +02:00
alegrey91
196e3c910d feat: add deny-wildcard annotation 2021-09-21 19:14:49 +02:00
Bright Zheng
0039c91c23 docs: fix doc minor issues (#425) 2021-09-20 14:35:33 +02:00
Dario Tranchitella
26965a5ea2 fix: skipping indexer if error is a NoKindMatch 2021-09-17 15:43:42 +02:00
Maksim Fedotov
422b6598ba fix: check if user is a member of capsuleUserGroup instead of tenantOwner when cordoning a tenant 2021-09-15 11:14:39 +02:00
Gonzalo Gabriel Jiménez Fuentes
61e6ab4088 fix(hack): jq installation checking 2021-09-13 12:04:49 +02:00
Dario Tranchitella
94c6a64fcb fix: validating Tenant owner name when is a ServiceAccount 2021-09-04 14:17:06 +02:00
Dario Tranchitella
75ebb571e4 fix(chore): ignoring Helm tags 2021-09-01 18:18:07 +02:00
Dario Tranchitella
8f3b3eac29 fix: deleting Pods upon TLS update for HA installations 2021-09-01 18:18:07 +02:00
Dario Tranchitella
7979c256d9 chore: ready for v0.1.0 release 2021-08-23 17:09:36 +02:00
bsctl
bdafbcf90a docs: fix minor issues 2021-08-23 16:38:17 +02:00
Dario Tranchitella
d0530bbbe3 docs: updating capsule-proxy (#406) 2021-08-23 12:00:47 +02:00
Adriano Pezzuto
1035afc7fe fix(grafana): change webhook metric used in dashboard (#404) 2021-08-20 17:39:00 +02:00
Dario Tranchitella
67046c5b54 fix(hack): supporting older versions of Kubernetes for certificates 2021-08-19 18:12:02 +02:00
Pietro Terrizzi
564c4db81a docs(monitor): capsule dashboard install and steps 2021-08-19 15:11:36 +02:00
Pietro Terrizzi
30c3ab078d docs(helm): added further servicemonitor values 2021-08-19 15:11:36 +02:00
Pietro Terrizzi
e9b803b9cd docs(monitoring): added screenshots 2021-08-19 15:11:36 +02:00
bsctl
cb8e504832 docs: add general contributions lineguides for capsule-proxy 2021-08-19 13:03:10 +02:00
bsctl
713867d916 docs: documenting required new-line at the end of the file 2021-08-19 13:03:10 +02:00
bsctl
23e55c685c docs: documenting the Conventional git Commit Messages 2021-08-19 13:03:10 +02:00
Adriano Pezzuto
6393541818 build(helm): update chart and app version (#395)
* build(helm): update chart and app version

* fix(docs): helm charts values descriptions

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2021-08-18 23:38:35 +02:00
Dario Tranchitella
c140ab076e ci(gh): adding git semantic commit message check 2021-08-18 22:08:53 +02:00
Maxim Fedotov
6b629777b7 build(helm): add customLabels and customAnnotations params (#391)
Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
2021-08-17 23:24:37 +03:00
Pietro Terrizzi
5554ed5f32 feat(helm): additional labels,annotations and matchlabels 2021-08-17 18:01:19 +02:00
Pietro Terrizzi
00ef9a2f67 chore(helm): added quotes to servicemonitor ns 2021-08-17 18:01:19 +02:00
Dario Tranchitella
46c2f0e997 build(helm): enforcement of LoadBalancer service kind 2021-08-17 17:21:59 +02:00
Dario Tranchitella
0c0a90a934 build(kustomize): enforcement of LoadBalancer service kind 2021-08-17 17:21:59 +02:00
Dario Tranchitella
9d65013a22 docs: enforcement of LoadBalancer service kind 2021-08-17 17:21:59 +02:00
Dario Tranchitella
60ab33337d feat: enforcement of LoadBalancer service kind 2021-08-17 17:21:59 +02:00
Adriano Pezzuto
225d671301 Fix PriorityClasses description in CRD (#389)
* fix(kustomize): update the PriorityClasses description in CRD

* fix(helm): update the PriorityClasses description in CRD

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2021-08-17 15:19:10 +02:00
bsctl
7538926bae docs: update README for v1beta1 2021-08-17 14:58:56 +02:00
bsctl
0de0eca72a fix(gh): upgrade release version 2021-08-17 11:25:08 +02:00
bsctl
d5a702ceae fix(hack): add signerName to CSR 2021-08-17 11:25:08 +02:00
Dario Tranchitella
a2fda44110 fix: NewIngressHostnameCollision is returning pointer for error parsing 2021-08-12 19:30:27 +02:00
Dario Tranchitella
06330cf992 fix: example was wrong due to missing porting of NamespaceOptions 2021-08-12 19:30:27 +02:00
Dario Tranchitella
1ec9936158 docs: hostname collision is now managed at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
694b519af8 build(helm): hostname collision is now managed at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
0b34f04291 build(helm): removing deprecated collision values 2021-08-12 19:30:27 +02:00
Dario Tranchitella
a702ef2af2 docs(helm): deprecating hostname collision 2021-08-12 19:30:27 +02:00
Dario Tranchitella
04d91af9f5 build(kustomize): hostname collision is now managed at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
8949be7497 test(e2e): scoped Ingress hostname and path collision 2021-08-12 19:30:27 +02:00
Dario Tranchitella
df08c9e63e refactor: hostname collision is now managed at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
07daffd669 build(helm): Ingress hostname collision scope at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
3a42b90221 build(kustomize): Ingress hostname collision scope at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
09277e9f3d feat: Ingress hostname collision scope at Tenant level 2021-08-12 19:30:27 +02:00
Dario Tranchitella
47794c0cf8 style: no need of nolint here 2021-08-12 19:30:27 +02:00
Dario Tranchitella
e24394f329 refactor: avoiding init functions for direct registration 2021-08-12 19:30:27 +02:00
Dario Tranchitella
01053d5deb refactor: renaming struct field names for allowed hostnames and classes 2021-08-12 19:30:27 +02:00
Dario Tranchitella
b749e34547 refactor: grouping Ingress options into defined struct 2021-08-12 19:30:27 +02:00
bsctl
82480f3afd docs: fix minor issues 2021-08-11 09:38:41 +02:00
bsctl
88a9c242a4 docs: update links in documentation 2021-08-11 09:38:41 +02:00
bsctl
651c62ff4a docs: add further test cases 2021-08-11 09:38:41 +02:00
bsctl
dcb8b784d5 docs: additional test cases 2021-08-11 09:38:41 +02:00
bsctl
7a698633d7 docs: additional test cases 2021-08-11 09:38:41 +02:00
bsctl
894ea5016b docs: add few test cases 2021-08-11 09:38:41 +02:00
Dario Tranchitella
e4e3283b90 build(helm): Tenant status enums must be capitalized 2021-08-11 07:28:53 +02:00
Dario Tranchitella
007f0083c2 build(kustomize): Tenant status enums must be capitalized 2021-08-11 07:28:53 +02:00
Dario Tranchitella
bc6fc920d3 fix: Tenant status enums must be capitalized 2021-08-11 07:28:53 +02:00
Dario Tranchitella
01b511b509 test(e2e): fixing flakiness for Service and EP metadata 2021-08-10 15:39:43 +02:00
Dario Tranchitella
6223b1c297 chore(github): forcing Go 1.16 and removing caching 2021-08-10 15:39:43 +02:00
Dario Tranchitella
d5158f06be chore(github): updating Kubernetes supported matrix 2021-08-10 15:39:43 +02:00
Dario Tranchitella
047f4a0ff7 build(helm): aligning descriptions for v1.22.0 2021-08-10 15:39:43 +02:00
Dario Tranchitella
71cdb45925 build(kustomize): aligning descriptions for v1.22.0 2021-08-10 15:39:43 +02:00
Dario Tranchitella
9182895811 refactor:EndpointSlice v1beta1 deprecated for v1 2021-08-10 15:39:43 +02:00
Dario Tranchitella
2eceb0935a chore(gomod): updating Kubernetes deps to 1.22 2021-08-10 15:39:43 +02:00
Dario Tranchitella
8ead555743 docs: reference to admissionregistration.k8s.io/v1 for local debugging
Starting from Kubernetes v1.22+, admissionregistration.k8s.io/v1beta1 is
deprecated and an alignement of the docs referring to outdated APIs has
been put in place.
2021-08-10 15:39:43 +02:00
Dario Tranchitella
57bf3d1c1b feat: skipping Ingress indexer setup for deprecated APIs
Starting from Kubernetes v1.22+, networking.k8s.io/v1beta1 and
extensions/v1beta1 are no more supported and indexers must not be
started, otherwise the manager would panic.
2021-08-10 15:39:43 +02:00
Dario Tranchitella
bb58e90f5d test(e2e): skipping ingress class tests if running on Kubernetes 1.22 2021-08-10 15:39:43 +02:00
Dario Tranchitella
f8fa87a998 chore(hack)!: upgrading to certificates.k8s.io/v1
Kubernetes 1.22 dropped support for certificates.k8s.io/v1beta1, v1
version has been provided since 1.19 and this must be considered as a
breaking change for users using this hack script for older versions.
2021-08-10 15:39:43 +02:00
Maxim Fedotov
b3658b7bfc refactor AdditionalMetadataSpec struct. Remove Additional prefix from labels and annotations fields (#379)
* refactor: remove 'Additional' prefix from Labels and Annotations fields in AdditionalMetadataSpec

* test(e2e): aligning tests to use updated AdditionalMetadataSpec structure

* build(kustomize): CRD update for updated v1beta1 AdditionalMetadataSpec

* build(helm): CRD update for updated v1beta1 AdditionalMetadataSpec

* build(installer): CRD update for updated v1beta1 AdditionalMetadataSpec

Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
2021-08-10 12:11:16 +03:00
Maksim Fedotov
54d0201161 test(e2e): fix linting issues for NamespaceOptions tests 2021-08-09 20:25:03 +02:00
Maksim Fedotov
44ffe0ddf5 build(installer): CRD update for v1beta1 NamespaceOptions 2021-08-09 20:25:03 +02:00
Maksim Fedotov
491ab71842 build(helm): CRD update for v1beta1 NamespaceOptions 2021-08-09 20:25:03 +02:00
Maksim Fedotov
4e9dbf8690 build(kustomize): CRD update for v1beta1 NamespaceOptions 2021-08-09 20:25:03 +02:00
Maksim Fedotov
34614015a0 test(e2e): aligning tests to use new NamespaceOptions structure 2021-08-09 20:25:03 +02:00
Maksim Fedotov
737fb26e39 refactor: use NamespaceOptions struct to store namespace-related tenant configurations 2021-08-09 20:25:03 +02:00
Pietro Terrizzi
b56015922f chore(gh): using build-args 2021-08-09 11:53:43 +02:00
Maxim Fedotov
ddb9ffd79e refactor: split tenant controller to separate files
Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
2021-08-07 21:37:48 +02:00
Maksim Fedotov
cae65c9f84 fix: capsuleconfiguration controller package name should be config instead of rbac 2021-08-07 20:40:08 +02:00
Dario Tranchitella
befcf65bdd feat: adding webhook and rest client latency per endpoint 2021-08-03 09:51:33 +02:00
Dario Tranchitella
e1d98334a2 chore(gh): updating e2e workflow 2021-07-28 17:34:24 +02:00
Dario Tranchitella
848c6d99c2 refactor: using goroutines per Namespace for each resource Kind reconciliation 2021-07-28 17:34:24 +02:00
Dario Tranchitella
bd12068397 fix: handling multiple resources for hard ResourceQuota resources 2021-07-24 14:36:57 +02:00
Dario Tranchitella
4604e44c37 build(helm): Tenant or Namespace scope for resource quota budgets 2021-07-24 14:36:57 +02:00
Dario Tranchitella
31863b53af build(kustomize): Tenant or Namespace scope for resource quota budgets 2021-07-24 14:36:57 +02:00
Dario Tranchitella
7a055fcb9f fix(test): matching upon reconciliation, not retrieval 2021-07-24 14:36:57 +02:00
Dario Tranchitella
29ab5ca64a test: Tenant or Namespace scope for resource quota budgets 2021-07-24 14:36:57 +02:00
Dario Tranchitella
c52f7844db feat: Tenant or Namespace scope for resource quota budgets 2021-07-24 14:36:57 +02:00
spagno
9244122d42 docs (helm): added namespace creation 2021-07-23 19:38:10 +02:00
Dario Tranchitella
f883e7b662 fix: wrong description of Service external IPs 2021-07-23 08:28:20 +02:00
Dario Tranchitella
2f5f31b678 test(e2e): allowed external IPs is grouped in ServiceOptions 2021-07-23 08:28:20 +02:00
Dario Tranchitella
e7ef9642ad build(helm): allowed external IPs is grouped in ServiceOptions 2021-07-23 08:28:20 +02:00
Dario Tranchitella
34f73af5c4 build(kustomize): allowed external IPs is grouped in ServiceOptions 2021-07-23 08:28:20 +02:00
Dario Tranchitella
18912a002b feat: allowed external IPs is grouped in ServiceOptions 2021-07-23 08:28:20 +02:00
Dario Tranchitella
d43ad2f9f8 build(kustomize): updating to v0.1.0-rc5 2021-07-23 08:28:20 +02:00
Vivek Kumar Singh
9a595877ce docs: update capsule-proxy docs
Signed-off-by: Vivek Singh <vivekkmr45@yahoo.in>
2021-07-22 12:40:03 +02:00
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
286 changed files with 38656 additions and 6208 deletions

View File

@@ -7,6 +7,15 @@ on:
branches: [ "*" ]
jobs:
commit_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v2
with:
firstParent: true
golangci:
name: lint
runs-on: ubuntu-latest
@@ -25,18 +34,9 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Go modules
uses: actions/cache@v1
env:
cache-name: go-mod
- uses: actions/setup-go@v2
with:
path: |
~/go/pkg/mod
/home/runner/work/capsule/capsule
key: ${{ runner.os }}-build-${{ env.cache-name }}
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
go-version: '^1.16'
- 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

View File

@@ -10,12 +10,27 @@ jobs:
runs-on: ubuntu-20.04
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v2
-
name: Docker meta
- name: Generate build-args
id: build-args
run: |
# Declare vars for internal use
VERSION=$(git describe --abbrev=0 --tags)
GIT_HEAD_COMMIT=$(git rev-parse --short HEAD)
GIT_TAG_COMMIT=$(git rev-parse --short $VERSION)
GIT_MODIFIED_1=$(git diff $GIT_HEAD_COMMIT $GIT_TAG_COMMIT --quiet && echo "" || echo ".dev")
GIT_MODIFIED_2=$(git diff --quiet && echo "" || echo ".dirty")
# Export to GH_ENV
echo "GIT_LAST_TAG=$VERSION" >> $GITHUB_ENV
echo "GIT_HEAD_COMMIT=$GIT_HEAD_COMMIT" >> $GITHUB_ENV
echo "GIT_TAG_COMMIT=$GIT_TAG_COMMIT" >> $GITHUB_ENV
echo "GIT_MODIFIED=$(echo "$GIT_MODIFIED_1""$GIT_MODIFIED_2")" >> $GITHUB_ENV
echo "GIT_REPO=$(git config --get remote.origin.url)" >> $GITHUB_ENV
echo "BUILD_DATE=$(git log -1 --format="%at" | xargs -I{} date -d @{} +%Y-%m-%dT%H:%M:%S)" >> $GITHUB_ENV
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
@@ -26,22 +41,19 @@ jobs:
flavor: |
latest=false
-
name: Set up QEMU
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
platforms: arm64,arm
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
install: true
uses: docker/setup-buildx-action@v1
-
name: Inspect builder
- name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
@@ -49,16 +61,14 @@ jobs:
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
-
name: Login to quay.io Container Registry
- name: Login to quay.io Container Registry
uses: docker/login-action@v1
with:
registry: quay.io
username: ${{ github.repository_owner }}+github
password: ${{ secrets.BOT_QUAY_IO }}
-
name: Build and push
- name: Build and push
id: build-release
uses: docker/build-push-action@v2
with:
@@ -67,7 +77,13 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
GIT_HEAD_COMMIT=${{ env.GIT_HEAD_COMMIT }}
GIT_TAG_COMMIT=${{ env.GIT_TAG_COMMIT }}
GIT_REPO=${{ env.GIT_REPO }}
GIT_LAST_TAG=${{ env.GIT_LAST_TAG }}
GIT_MODIFIED=${{ env.GIT_MODIFIED }}
BUILD_DATE=${{ env.BUILD_DATE }}
-
name: Image digest
- name: Image digest
run: echo ${{ steps.build-release.outputs.digest }}

View File

@@ -3,33 +3,41 @@ name: e2e
on:
push:
branches: [ "*" ]
paths:
- '.github/workflows/e2e.yml'
- 'api/**'
- 'controllers/**'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
- 'main.go'
- 'Makefile'
pull_request:
branches: [ "*" ]
paths:
- '.github/workflows/e2e.yml'
- 'api/**'
- 'controllers/**'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
- 'main.go'
- 'Makefile'
jobs:
kind:
name: Kubernetes
strategy:
matrix:
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.0']
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.0']
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Go modules and Docker images
uses: actions/cache@v1
env:
cache-name: gomod-docker
- uses: actions/setup-go@v2
with:
path: |
~/go/pkg/mod
/var/lib/docker
/home/runner/work/capsule/capsule
key: ${{ matrix.k8s-version }}-build-${{ env.cache-name }}
restore-keys: |
${{ matrix.k8s-version }}-build-
${{ matrix.k8s-version }}-
go-version: '^1.16'
- run: make manifests
- name: Checking if manifests are disaligned
run: test -z "$(git diff 2> /dev/null)"
@@ -39,10 +47,11 @@ jobs:
run: go get github.com/onsi/ginkgo/ginkgo
- uses: actions/setup-go@v2
with:
go-version: '^1.13.8'
go-version: '^1.16'
- uses: engineerd/setup-kind@v0.5.0
with:
skipClusterCreation: true
version: v0.11.1
- uses: azure/setup-helm@v1
with:
version: 3.3.4

View File

@@ -3,6 +3,7 @@ name: Helm Chart
on:
push:
branches: [ "*" ]
tags: [ "helm-v*" ]
pull_request:
branches: [ "*" ]

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ bin
*.swp
*.swo
*~
.vscode
**/*.kubeconfig
**/*.crt

View File

@@ -1,5 +1,5 @@
# Current Operator version
VERSION ?= $$(git describe --abbrev=0 --tags)
VERSION ?= $$(git describe --abbrev=0 --tags --match "v*")
# Default bundle image tag
BUNDLE_IMG ?= quay.io/clastix/capsule:$(VERSION)-bundle
@@ -45,7 +45,7 @@ manager: generate fmt vet
# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate manifests
go run ./main.go
go run .
# Creates the single file to install Capsule without any external dependency
installer: manifests kustomize
@@ -78,6 +78,58 @@ manifests: controller-gen
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
# Setup development env
# Usage:
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
# For example:
# LAPTOP_HOST_IP=192.168.10.101 make dev-setup
define TLS_CNF
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = req_ext
[ req_distinguished_name ]
countryName = SG
stateOrProvinceName = SG
localityName = SG
organizationName = CAPSULE
commonName = CAPSULE
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
IP.1 = $(LAPTOP_HOST_IP)
endef
export TLS_CNF
dev-setup:
kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
mkdir -p /tmp/k8s-webhook-server/serving-certs
echo "$${TLS_CNF}" > _tls.cnf
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
-extensions req_ext \
-config _tls.cnf \
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
rm -f _tls.cnf
export WEBHOOK_URL="https://$${LAPTOP_HOST_IP}:9443"; \
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
--type='json' -p="[\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
]" && \
kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
--type='json' -p="[\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/cordoning\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/ingresses\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespaces\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/networkpolicies\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}}\
]";
# Build the docker image
docker-build: test
docker build . -t ${IMG} --build-arg GIT_HEAD_COMMIT=$(GIT_HEAD_COMMIT) \

View File

@@ -13,7 +13,7 @@
---
# Kubernetes multi-tenancy made simple
# Kubernetes multi-tenancy made easy
**Capsule** helps to implement a multi-tenancy and policy-based environment in your Kubernetes cluster. It is not intended to be yet another _PaaS_, instead, it has been designed as a micro-services-based ecosystem with the minimalist approach, leveraging only on upstream Kubernetes.
# What's the problem with the current status?
@@ -71,36 +71,24 @@ Clone this repository and move to the repo folder:
```
$ 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`.
## How to create Tenants
Use the scaffold [Tenant](config/samples/capsule_v1alpha1_tenant.yaml) and simply apply as cluster admin.
Use the scaffold [Tenant](config/samples/capsule_v1beta1_tenant.yaml) and simply apply as cluster admin.
```
$ kubectl apply -f config/samples/capsule_v1alpha1_tenant.yaml
tenant.capsule.clastix.io/oil created
$ kubectl apply -f config/samples/capsule_v1beta1_tenant.yaml
tenant.capsule.clastix.io/gas created
```
You can check the tenant just created as
```
$ kubectl get tenants
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND NODE SELECTOR AGE
oil 3 0 alice User 1m
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
gas Active 3 0 {"kubernetes.io/os":"linux"} 25s
```
## Tenant owners
@@ -112,52 +100,46 @@ Assignment to a group depends on the authentication strategy in your cluster.
For example, if you are using `capsule.clastix.io`, users authenticated through a _X.509_ certificate must have `capsule.clastix.io` as _Organization_: `-subj "/CN=${USER}/O=capsule.clastix.io"`
Users authenticated through an _OIDC token_ must have
Users authenticated through an _OIDC token_ must have in their token:
```json
...
"users_groups": [
"capsule.clastix.io",
"other_group"
"capsule.clastix.io",
"other_group"
]
```
in their token.
The [hack/create-user.sh](hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `alice` user acting as owner of a tenant called `oil`
The [hack/create-user.sh](hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `bob` user acting as owner of a tenant called `gas`
```bash
./hack/create-user.sh alice oil
creating certs in TMPDIR /tmp/tmp.4CLgpuime3
Generating RSA private key, 2048 bit long modulus (2 primes)
............+++++
........................+++++
e is 65537 (0x010001)
certificatesigningrequest.certificates.k8s.io/alice-oil created
certificatesigningrequest.certificates.k8s.io/alice-oil approved
kubeconfig file is: alice-oil.kubeconfig
to use it as alice export KUBECONFIG=alice-oil.kubeconfig
./hack/create-user.sh bob gas
...
certificatesigningrequest.certificates.k8s.io/bob-gas created
certificatesigningrequest.certificates.k8s.io/bob-gas approved
kubeconfig file is: bob-gas.kubeconfig
to use it as bob export KUBECONFIG=bob-gas.kubeconfig
```
## Working with Tenants
Log in to the Kubernetes cluster as `alice` tenant owner
Log in to the Kubernetes cluster as `bob` tenant owner
```
$ export KUBECONFIG=alice-oil.kubeconfig
$ export KUBECONFIG=bob-gas.kubeconfig
```
and create a couple of new namespaces
```
$ kubectl create namespace oil-production
$ kubectl create namespace oil-development
$ kubectl create namespace gas-production
$ kubectl create namespace gas-development
```
As user `alice` you can operate with fully admin permissions:
As user `bob` you can operate with fully admin permissions:
```
$ kubectl -n oil-development run nginx --image=docker.io/nginx
$ kubectl -n oil-development get pods
$ kubectl -n gas-development run nginx --image=docker.io/nginx
$ kubectl -n gas-development get pods
```
but limited to only your own namespaces:
@@ -165,12 +147,9 @@ but limited to only your own namespaces:
```
$ kubectl -n kube-system get pods
Error from server (Forbidden): pods is forbidden:
User "alice" cannot list resource "pods" in API group "" in the namespace "kube-system"
User "bob" cannot list resource "pods" in API group "" in the namespace "kube-system"
```
# Documentation
Please, check the project [documentation](./docs/index.md) for more cool things you can do with Capsule.
# Removal
Similar to `deploy`, you can get rid of Capsule using the `remove` target.
@@ -178,15 +157,21 @@ Similar to `deploy`, you can get rid of Capsule using the `remove` target.
$ make remove
```
# Documentation
Please, check the project [documentation](./docs/index.md) for more cool things you can do with Capsule.
# Contribution
Capsule is Open Source with Apache 2 license and any contribution is welcome.
Please refer to the corresponding docs:
- [contributing.md](./docs/contributing.md) for the general guide; and
- [dev-guide.md](./docs/dev-guide.md) for how to set up the development env to get started.
# FAQ
- Q. How to pronounce Capsule?
A. It should be pronounced as `/ˈkæpsjuːl/`.
- Q. Can I contribute?
A. Absolutely! Capsule is Open Source with Apache 2 license and any contribution is welcome. Please refer to the corresponding [section](./docs/operator/contributing.md) in the documentation.
- Q. Is it production grade?
A. Although under frequent development and improvements, Capsule is ready to be used in production environments as currently, people are using it in public and private deployments. Check out the [release](https://github.com/clastix/capsule/releases) page for a detailed list of available versions.

View File

@@ -0,0 +1,8 @@
package v1alpha1
const (
ForbiddenNodeLabelsAnnotation = "capsule.clastix.io/forbidden-node-labels"
ForbiddenNodeLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-node-labels-regexp"
ForbiddenNodeAnnotationsAnnotation = "capsule.clastix.io/forbidden-node-annotations"
ForbiddenNodeAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-node-annotations-regexp"
)

View File

@@ -8,7 +8,6 @@ import (
)
// CapsuleConfigurationSpec defines the Capsule configuration
// nolint:maligned
type CapsuleConfigurationSpec struct {
// Names of the groups for Capsule users.
// +kubebuilder:default={capsule.clastix.io}
@@ -19,15 +18,6 @@ type CapsuleConfigurationSpec struct {
ForceTenantPrefix bool `json:"forceTenantPrefix,omitempty"`
// Disallow creation of namespaces, whose name matches this regexp
ProtectedNamespaceRegexpString string `json:"protectedNamespaceRegex,omitempty"`
// When defining the exact match for allowed Ingress hostnames at Tenant level, a collision is not allowed.
// Toggling this, Capsule will not check if a hostname collision is in place, allowing the creation of
// two or more Tenant resources although sharing the same allowed hostname(s).
//
// The JSON path of the resource is: /spec/ingressHostnames/allowed
AllowTenantIngressHostnamesCollision bool `json:"allowTenantIngressHostnamesCollision,omitempty"`
// Allow the collision of Ingress resource hostnames across all the Tenants.
// +kubebuilder:default=true
AllowIngressHostnameCollision bool `json:"allowIngressHostnameCollision,omitempty"`
}
// +kubebuilder:object:root=true

View File

@@ -17,44 +17,57 @@ import (
)
const (
resourceQuotaScopeAnnotation = "capsule.clastix.io/resource-quota-scope"
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"
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableExternalNameAnnotation = "capsule.clastix.io/enable-external-name"
enableLoadBalancerAnnotation = "capsule.clastix.io/enable-loadbalancer-service"
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"
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"
ingressHostnameCollisionScope = "ingress.capsule.clastix.io/hostname-collision-scope"
)
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.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,
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,
@@ -124,21 +137,34 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
dst.ObjectMeta = t.ObjectMeta
// Spec
dst.Spec.NamespaceQuota = t.Spec.NamespaceQuota
if t.Spec.NamespaceQuota != nil {
if dst.Spec.NamespaceOptions == nil {
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
}
dst.Spec.NamespaceOptions.Quota = 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 dst.Spec.NamespaceOptions == nil {
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
}
dst.Spec.NamespaceOptions.AdditionalMetadata = &capsulev1beta1.AdditionalMetadataSpec{
Labels: t.Spec.NamespacesMetadata.AdditionalLabels,
Annotations: t.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if t.Spec.ServicesMetadata != nil {
dst.Spec.ServicesMetadata = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
Labels: t.Spec.ServicesMetadata.AdditionalLabels,
Annotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
},
}
}
}
if t.Spec.StorageClasses != nil {
@@ -147,14 +173,22 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
Regex: t.Spec.StorageClasses.Regex,
}
}
if v, ok := t.Annotations[ingressHostnameCollisionScope]; ok {
switch v {
case string(capsulev1beta1.HostnameCollisionScopeCluster), string(capsulev1beta1.HostnameCollisionScopeTenant), string(capsulev1beta1.HostnameCollisionScopeNamespace):
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScope(v)
default:
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScopeDisabled
}
}
if t.Spec.IngressClasses != nil {
dst.Spec.IngressClasses = &capsulev1beta1.AllowedListSpec{
dst.Spec.IngressOptions.AllowedClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressClasses.Exact,
Regex: t.Spec.IngressClasses.Regex,
}
}
if t.Spec.IngressHostnames != nil {
dst.Spec.IngressHostnames = &capsulev1beta1.AllowedListSpec{
dst.Spec.IngressOptions.AllowedHostnames = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressHostnames.Exact,
Regex: t.Spec.IngressHostnames.Regex,
}
@@ -166,17 +200,28 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
}
}
if len(t.Spec.NetworkPolicies) > 0 {
dst.Spec.NetworkPolicies = &capsulev1beta1.NetworkPolicySpec{
dst.Spec.NetworkPolicies = capsulev1beta1.NetworkPolicySpec{
Items: t.Spec.NetworkPolicies,
}
}
if len(t.Spec.LimitRanges) > 0 {
dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{
dst.Spec.LimitRanges = capsulev1beta1.LimitRangesSpec{
Items: t.Spec.LimitRanges,
}
}
if len(t.Spec.ResourceQuota) > 0 {
dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{
dst.Spec.ResourceQuota = capsulev1beta1.ResourceQuotaSpec{
Scope: func() capsulev1beta1.ResourceQuotaScope {
if v, ok := t.GetAnnotations()[resourceQuotaScopeAnnotation]; ok {
switch v {
case string(capsulev1beta1.ResourceQuotaScopeNamespace):
return capsulev1beta1.ResourceQuotaScopeNamespace
case string(capsulev1beta1.ResourceQuotaScopeTenant):
return capsulev1beta1.ResourceQuotaScopeTenant
}
}
return capsulev1beta1.ResourceQuotaScopeTenant
}(),
Items: t.Spec.ResourceQuota,
}
}
@@ -189,12 +234,15 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
}
}
if t.Spec.ExternalServiceIPs != nil {
dst.Spec.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
dst.Spec.ServiceOptions.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)
dst.Spec.ServiceOptions.ExternalServiceIPs.Allowed[i] = capsulev1beta1.AllowedIP(IP)
}
}
@@ -226,7 +274,43 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName()))
}
dst.Spec.EnableNodePorts = pointer.BoolPtr(val)
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)
}
loadBalancerService, ok := annotations[enableLoadBalancerAnnotation]
if ok {
val, err := strconv.ParseBool(loadBalancerService)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableLoadBalancerAnnotation, 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.LoadBalancer = pointer.BoolPtr(val)
}
// Status
@@ -240,6 +324,8 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
delete(dst.ObjectMeta.Annotations, podPriorityAllowedAnnotation)
delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation)
delete(dst.ObjectMeta.Annotations, enableExternalNameAnnotation)
delete(dst.ObjectMeta.Annotations, enableLoadBalancerAnnotation)
delete(dst.ObjectMeta.Annotations, ownerGroupsAnnotation)
delete(dst.ObjectMeta.Annotations, ownerUsersAnnotation)
delete(dst.ObjectMeta.Annotations, ownerServiceAccountAnnotation)
@@ -252,6 +338,11 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
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)
delete(dst.ObjectMeta.Annotations, resourceQuotaScopeAnnotation)
delete(dst.ObjectMeta.Annotations, ingressHostnameCollisionScope)
return nil
}
@@ -304,6 +395,17 @@ func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
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 {
@@ -349,7 +451,10 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
t.ObjectMeta = src.ObjectMeta
// Spec
t.Spec.NamespaceQuota = src.Spec.NamespaceQuota
if src.Spec.NamespaceOptions != nil && src.Spec.NamespaceOptions.Quota != nil {
t.Spec.NamespaceQuota = src.Spec.NamespaceOptions.Quota
}
t.Spec.NodeSelector = src.Spec.NodeSelector
if t.Annotations == nil {
@@ -358,16 +463,16 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
t.convertV1Beta1OwnerToV1Alpha1(src)
if src.Spec.NamespacesMetadata != nil {
if src.Spec.NamespaceOptions != nil && src.Spec.NamespaceOptions.AdditionalMetadata != nil {
t.Spec.NamespacesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.NamespacesMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.NamespacesMetadata.AdditionalAnnotations,
AdditionalLabels: src.Spec.NamespaceOptions.AdditionalMetadata.Labels,
AdditionalAnnotations: src.Spec.NamespaceOptions.AdditionalMetadata.Annotations,
}
}
if src.Spec.ServicesMetadata != nil {
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.ServicesMetadata.AdditionalAnnotations,
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.Labels,
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.Annotations,
}
}
if src.Spec.StorageClasses != nil {
@@ -376,16 +481,17 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
Regex: src.Spec.StorageClasses.Regex,
}
}
if src.Spec.IngressClasses != nil {
t.Annotations[ingressHostnameCollisionScope] = string(src.Spec.IngressOptions.HostnameCollisionScope)
if src.Spec.IngressOptions.AllowedClasses != nil {
t.Spec.IngressClasses = &AllowedListSpec{
Exact: src.Spec.IngressClasses.Exact,
Regex: src.Spec.IngressClasses.Regex,
Exact: src.Spec.IngressOptions.AllowedClasses.Exact,
Regex: src.Spec.IngressOptions.AllowedClasses.Regex,
}
}
if src.Spec.IngressHostnames != nil {
if src.Spec.IngressOptions.AllowedHostnames != nil {
t.Spec.IngressHostnames = &AllowedListSpec{
Exact: src.Spec.IngressHostnames.Exact,
Regex: src.Spec.IngressHostnames.Regex,
Exact: src.Spec.IngressOptions.AllowedHostnames.Exact,
Regex: src.Spec.IngressOptions.AllowedHostnames.Regex,
}
}
if src.Spec.ContainerRegistries != nil {
@@ -394,13 +500,14 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
Regex: src.Spec.ContainerRegistries.Regex,
}
}
if src.Spec.NetworkPolicies != nil {
if len(src.Spec.NetworkPolicies.Items) > 0 {
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
}
if src.Spec.LimitRanges != nil {
if len(src.Spec.LimitRanges.Items) > 0 {
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
}
if src.Spec.ResourceQuota != nil {
if len(src.Spec.ResourceQuota.Items) > 0 {
t.Annotations[resourceQuotaScopeAnnotation] = string(src.Spec.ResourceQuota.Scope)
t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
}
if len(src.Spec.AdditionalRoleBindings) > 0 {
@@ -411,12 +518,12 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
})
}
}
if src.Spec.ExternalServiceIPs != nil {
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.ExternalServiceIPs != nil {
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: make([]AllowedIP, len(src.Spec.ExternalServiceIPs.Allowed)),
Allowed: make([]AllowedIP, len(src.Spec.ServiceOptions.ExternalServiceIPs.Allowed)),
}
for i, IP := range src.Spec.ExternalServiceIPs.Allowed {
for i, IP := range src.Spec.ServiceOptions.ExternalServiceIPs.Allowed {
t.Spec.ExternalServiceIPs.Allowed[i] = AllowedIP(IP)
}
}
@@ -437,7 +544,17 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
}
}
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.EnableNodePorts)
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AllowedServices != nil {
if src.Spec.ServiceOptions.AllowedServices.NodePort != nil {
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
}
if src.Spec.ServiceOptions.AllowedServices.ExternalName != nil {
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
}
if src.Spec.ServiceOptions.AllowedServices.LoadBalancer != nil {
t.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
}
}
// Status
t.Status = TenantStatus{

View File

@@ -36,13 +36,28 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Regex: "^foo*",
}
var v1beta1AdditionalMetadataSpec = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
Labels: map[string]string{
"foo": "bar",
},
AdditionalAnnotations: map[string]string{
Annotations: map[string]string{
"foo": "bar",
},
}
var v1beta1NamespaceOptions = &capsulev1beta1.NamespaceOptions{
Quota: &namespaceQuota,
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
}
var v1beta1ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
LoadBalancer: pointer.BoolPtr(false),
},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
},
}
var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
@@ -164,6 +179,10 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "PriorityClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
@@ -211,21 +230,24 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
},
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1beta1AdditionalMetadataSpec,
ServicesMetadata: v1beta1AdditionalMetadataSpec,
StorageClasses: v1beta1AllowedListSpec,
IngressClasses: v1beta1AllowedListSpec,
IngressHostnames: v1beta1AllowedListSpec,
NamespaceOptions: v1beta1NamespaceOptions,
ServiceOptions: v1beta1ServiceOptions,
StorageClasses: v1beta1AllowedListSpec,
IngressOptions: capsulev1beta1.IngressOptions{
HostnameCollisionScope: capsulev1beta1.HostnameCollisionScopeDisabled,
AllowedClasses: v1beta1AllowedListSpec,
AllowedHostnames: v1beta1AllowedListSpec,
},
ContainerRegistries: v1beta1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{
Items: networkPolicies,
},
LimitRanges: &capsulev1beta1.LimitRangesSpec{
LimitRanges: capsulev1beta1.LimitRangesSpec{
Items: limitRanges,
},
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{
Scope: capsulev1beta1.ResourceQuotaScopeNamespace,
Items: resourceQuotas,
},
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
@@ -240,15 +262,11 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
},
},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
},
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"},
PriorityClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{"default"},
Regex: "^tier-.*$",
},
EnableNodePorts: pointer.BoolPtr(false),
},
Status: capsulev1beta1.TenantStatus{
Size: 1,
@@ -266,7 +284,9 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Annotations: map[string]string{
"foo": "bar",
podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent",
enableExternalNameAnnotation: "false",
enableNodePortsAnnotation: "false",
enableLoadBalancerAnnotation: "false",
podPriorityAllowedAnnotation: "default",
podPriorityAllowedRegexAnnotation: "^tier-.*$",
ownerGroupsAnnotation: "owner-foo,owner-bar",
@@ -280,6 +300,9 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
enableIngressClassListingAnnotation: "alice,owner-foo,owner-bar",
enableIngressClassUpdateAnnotation: "alice,bob",
enableIngressClassDeletionAnnotation: "alice,jack",
enablePriorityClassListingAnnotation: "jack",
resourceQuotaScopeAnnotation: "Namespace",
ingressHostnameCollisionScope: "Disabled",
},
},
Spec: TenantSpec{

View File

@@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Copyright 2020-2021 Clastix Labs

View File

@@ -4,6 +4,6 @@
package v1beta1
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1
import (
"regexp"
"sort"
"strings"
)
type ForbiddenListSpec struct {
Exact []string `json:"denied,omitempty"`
Regex string `json:"deniedRegex,omitempty"`
}
func (in *ForbiddenListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
})
i := sort.SearchStrings(in.Exact, value)
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
}
func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}

View File

@@ -0,0 +1,67 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
//nolint:dupl
package v1beta1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestForbiddenListSpec_ExactMatch(t *testing.T) {
type tc struct {
In []string
True []string
False []string
}
for _, tc := range []tc{
{
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"bing", "bong"},
},
{
[]string{"one", "two", "three"},
[]string{"one", "two", "three"},
[]string{"a", "b", "c"},
},
{
nil,
nil,
[]string{"any", "value"},
},
} {
a := ForbiddenListSpec{
Exact: tc.In,
}
for _, ok := range tc.True {
assert.True(t, a.ExactMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.ExactMatch(ko))
}
}
}
func TestForbiddenListSpec_RegexMatch(t *testing.T) {
type tc struct {
Regex string
True []string
False []string
}
for _, tc := range []tc{
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
{``, nil, []string{"any", "value"}},
} {
a := ForbiddenListSpec{
Regex: tc.Regex,
}
for _, ok := range tc.True {
assert.True(t, a.RegexMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.RegexMatch(ko))
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
const (
HostnameCollisionScopeCluster HostnameCollisionScope = "Cluster"
HostnameCollisionScopeTenant HostnameCollisionScope = "Tenant"
HostnameCollisionScopeNamespace HostnameCollisionScope = "Namespace"
HostnameCollisionScopeDisabled HostnameCollisionScope = "Disabled"
)
// +kubebuilder:validation:Enum=Cluster;Tenant;Namespace;Disabled
type HostnameCollisionScope string

View File

@@ -0,0 +1,24 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type IngressOptions struct {
// 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.
AllowedClasses *AllowedListSpec `json:"allowedClasses,omitempty"`
// Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames.
//
//
// - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule.
//
// - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant.
//
// - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace.
//
//
// Optional.
// +kubebuilder:default=Disabled
HostnameCollisionScope HostnameCollisionScope `json:"hostnameCollisionScope,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.
AllowedHostnames *AllowedListSpec `json:"allowedHostnames,omitempty"`
}

View File

@@ -0,0 +1,51 @@
package v1beta1
import "strings"
type NamespaceOptions struct {
//+kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
}
func (t *Tenant) hasForbiddenNamespaceLabelsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceLabelsAnnotation]; ok {
return true
}
if _, ok := t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
return true
}
return false
}
func (t *Tenant) hasForbiddenNamespaceAnnotationsAnnotations() bool {
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsAnnotation]; ok {
return true
}
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
return true
}
return false
}
func (t *Tenant) ForbiddenUserNamespaceLabels() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceLabelsAnnotations() {
return nil
}
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceLabelsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation],
}
}
func (t *Tenant) ForbiddenUserNamespaceAnnotations() *ForbiddenListSpec {
if !t.hasForbiddenNamespaceAnnotationsAnnotations() {
return nil
}
return &ForbiddenListSpec{
Exact: strings.Split(t.Annotations[ForbiddenNamespaceAnnotationsAnnotation], ","),
Regex: t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation],
}
}

View File

@@ -31,7 +31,7 @@ func (p ProxyOperation) String() string {
return string(p)
}
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses;PriorityClasses
type ProxyServiceKind string
func (p ProxyServiceKind) String() string {
@@ -39,9 +39,10 @@ func (p ProxyServiceKind) String() string {
}
const (
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
PriorityClassesProxy ProxyServiceKind = "PriorityClasses"
ListOperation ProxyOperation = "List"
UpdateOperation ProxyOperation = "Update"

View File

@@ -5,6 +5,17 @@ package v1beta1
import corev1 "k8s.io/api/core/v1"
// +kubebuilder:validation:Enum=Tenant;Namespace
type ResourceQuotaScope string
const (
ResourceQuotaScopeTenant ResourceQuotaScope = "Tenant"
ResourceQuotaScopeNamespace ResourceQuotaScope = "Namespace"
)
type ResourceQuotaSpec struct {
// +kubebuilder:default=Tenant
// Define if the Resource Budget should compute resource across all Namespaces in the Tenant or individually per cluster. Default is Tenant
Scope ResourceQuotaScope `json:"scope,omitempty"`
Items []corev1.ResourceQuotaSpec `json:"items,omitempty"`
}

View File

@@ -0,0 +1,16 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
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"`
//+kubebuilder:default=true
// Specifies if LoadBalancer service type resources are allowed for the Tenant. Default is true. Optional.
LoadBalancer *bool `json:"loadBalancer,omitempty"`
}

View File

@@ -0,0 +1,13 @@
// 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"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
}

View File

@@ -8,12 +8,16 @@ import (
)
const (
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
ForbiddenNamespaceLabelsAnnotation = "capsule.clastix.io/forbidden-namespace-labels"
ForbiddenNamespaceLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-labels-regexp"
ForbiddenNamespaceAnnotationsAnnotation = "capsule.clastix.io/forbidden-namespace-annotations"
ForbiddenNamespaceAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-annotations-regexp"
)
func UsedQuotaFor(resource fmt.Stringer) string {

View File

@@ -18,10 +18,10 @@ func (t *Tenant) IsCordoned() bool {
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if t.Spec.NamespaceQuota == nil {
if t.Spec.NamespaceOptions == nil || t.Spec.NamespaceOptions.Quota == nil {
return false
}
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceOptions.Quota)
}
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {

View File

@@ -3,18 +3,18 @@
package v1beta1
// +kubebuilder:validation:Enum=cordoned;active
// +kubebuilder:validation:Enum=Cordoned;Active
type tenantState string
const (
TenantStateActive tenantState = "active"
TenantStateCordoned tenantState = "cordoned"
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".
//+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"`

View File

@@ -11,42 +11,30 @@ import (
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 additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
ServicesMetadata *AdditionalMetadataSpec `json:"servicesMetadata,omitempty"`
// Specifies options for the Namespaces, such as additional metadata or 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.
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,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 options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
IngressOptions IngressOptions `json:"ingressOptions,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"`
NetworkPolicies NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
LimitRanges *LimitRangesSpec `json:"limitRanges,omitempty"`
LimitRanges LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota *ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
ResourceQuota ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// 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.
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
//+kubebuilder:default=true
// Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
EnableNodePorts *bool `json:"enableNodePorts,omitempty"`
}
//+kubebuilder:object:root=true
@@ -54,7 +42,7 @@ type TenantSpec struct {
//+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 quota",type="integer",JSONPath=".spec.namespaceOptions.quota",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"

View File

@@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Copyright 2020-2021 Clastix Labs
@@ -17,15 +18,15 @@ import (
// 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
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*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
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
@@ -83,6 +84,36 @@ func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
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
}
if in.LoadBalancer != nil {
in, out := &in.LoadBalancer, &out.LoadBalancer
*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) {
{
@@ -124,6 +155,51 @@ func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ForbiddenListSpec) DeepCopyInto(out *ForbiddenListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForbiddenListSpec.
func (in *ForbiddenListSpec) DeepCopy() *ForbiddenListSpec {
if in == nil {
return nil
}
out := new(ForbiddenListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressOptions) DeepCopyInto(out *IngressOptions) {
*out = *in
if in.AllowedClasses != nil {
in, out := &in.AllowedClasses, &out.AllowedClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedHostnames != nil {
in, out := &in.AllowedHostnames, &out.AllowedHostnames
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressOptions.
func (in *IngressOptions) DeepCopy() *IngressOptions {
if in == nil {
return nil
}
out := new(IngressOptions)
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
@@ -146,6 +222,31 @@ func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceOptions) DeepCopyInto(out *NamespaceOptions) {
*out = *in
if in.Quota != nil {
in, out := &in.Quota, &out.Quota
*out = new(int32)
**out = **in
}
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceOptions.
func (in *NamespaceOptions) DeepCopy() *NamespaceOptions {
if in == nil {
return nil
}
out := new(NamespaceOptions)
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
@@ -253,6 +354,36 @@ func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec {
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)
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPsSpec)
(*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
@@ -322,19 +453,14 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*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)
if in.NamespaceOptions != nil {
in, out := &in.NamespaceOptions, &out.NamespaceOptions
*out = new(NamespaceOptions)
(*in).DeepCopyInto(*out)
}
if in.ServicesMetadata != nil {
in, out := &in.ServicesMetadata, &out.ServicesMetadata
*out = new(AdditionalMetadataSpec)
if in.ServiceOptions != nil {
in, out := &in.ServiceOptions, &out.ServiceOptions
*out = new(ServiceOptions)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
@@ -342,16 +468,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*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)
}
in.IngressOptions.DeepCopyInto(&out.IngressOptions)
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(AllowedListSpec)
@@ -364,21 +481,9 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*out)[key] = val
}
}
if in.NetworkPolicies != nil {
in, out := &in.NetworkPolicies, &out.NetworkPolicies
*out = new(NetworkPolicySpec)
(*in).DeepCopyInto(*out)
}
if in.LimitRanges != nil {
in, out := &in.LimitRanges, &out.LimitRanges
*out = new(LimitRangesSpec)
(*in).DeepCopyInto(*out)
}
if in.ResourceQuota != nil {
in, out := &in.ResourceQuota, &out.ResourceQuota
*out = new(ResourceQuotaSpec)
(*in).DeepCopyInto(*out)
}
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
in.LimitRanges.DeepCopyInto(&out.LimitRanges)
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindingsSpec, len(*in))
@@ -386,11 +491,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*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))
@@ -401,11 +501,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.EnableNodePorts != nil {
in, out := &in.EnableNodePorts, &out.EnableNodePorts
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.

View File

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

View File

@@ -1,6 +1,6 @@
# Deploying the Capsule Operator
Use the Capsule Operator for easily implementing, managing, and maintaining mutitenancy and access control in Kubernetes.
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
## Requirements
@@ -24,19 +24,23 @@ The Capsule Operator Chart can be used to instantly deploy the Capsule Operator
$ helm repo add clastix https://clastix.github.io/charts
2. Install the Chart:
2. Create the Namespace:
$ kubectl create namespace capsule-system
3. Install the Chart:
$ helm install capsule clastix/capsule -n capsule-system
3. Show the status:
4. Show the status:
$ helm status capsule -n capsule-system
4. Upgrade the Chart
5. Upgrade the Chart
$ helm upgrade capsule clastix/capsule -n capsule-system
5. Uninstall the Chart
6. Uninstall the Chart
$ helm uninstall capsule -n capsule-system
@@ -63,10 +67,8 @@ 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`
`manager.options.capsuleUserGroups` | Override the Capsule user groups | `[capsule.clastix.io]`
`manager.options.protectedNamespaceRegex` | If specified, disallows creation of namespaces matching the passed regexp | `null`
`manager.options.allowIngressHostnameCollision` | Allow the Ingress hostname collision at Ingress resource level across all the Tenants | `true`
`manager.options.allowTenantIngressHostnamesCollision` | Skip the validation check at Tenant level for colliding Ingress hostnames | `false`
`manager.image.repository` | Set the image repository of the controller. | `quay.io/clastix/capsule`
`manager.image.tag` | Overrides the image tag whose default is the chart. `appVersion` | `null`
`manager.image.pullPolicy` | Set the image pull policy. | `IfNotPresent`
@@ -78,6 +80,7 @@ Parameter | Description | Default
`manager.resources.limits/cpu` | Set the memory limits assigned to the controller. | `128Mi`
`mutatingWebhooksTimeoutSeconds` | Timeout in seconds for mutating webhooks. | `30`
`validatingWebhooksTimeoutSeconds` | Timeout in seconds for validating webhooks. | `30`
`webhooks` | Additional configuration for capsule webhooks. |
`imagePullSecrets` | Configuration for `imagePullSecrets` so that you can use a private images registry. | `[]`
`serviceAccount.create` | Specifies whether a service account should be created. | `true`
`serviceAccount.annotations` | Annotations to add to the service account. | `{}`
@@ -89,19 +92,24 @@ Parameter | Description | Default
`replicaCount` | Set the replica count for Capsule pod. | `1`
`affinity` | Set affinity rules for the Capsule pod. | `{}`
`podSecurityPolicy.enabled` | Specify if a Pod Security Policy must be created. | `false`
`serviceMonitor.enabled` | Specify if a Service Monitor must be created. | `false`
`serviceMonitor.serviceAccount.name` | Specify Service Account name for metrics scrape. | `capsule`
`serviceMonitor.serviceAccount.namespace` | Specify Service Account namespace for metrics scrape. | `capsule-system`
`serviceMonitor.enabled` | Specifies if a service monitor must be created. | `false`
`serviceMonitor.labels` | Additional labels which will be added to service monitor. | `{}`
`serviceMonitor.annotations` | Additional annotations which will be added to service monitor. | `{}`
`serviceMonitor.matchLabels` | Additional matchLabels which will be added to service monitor. | `{}`
`serviceMonitor.serviceAccount.name` | Specifies service account name for metrics scrape. | `capsule`
`serviceMonitor.serviceAccount.namespace` | Specifies service account namespace for metrics scrape. | `capsule-system`
`customLabels` | Additional labels which will be added to all resources created by Capsule helm chart . | `{}`
`customAnnotations` | Additional annotations which will be added to all resources created by Capsule helm chart . | `{}`
## Created resources
This Helm Chart cretes the following Kubernetes resources in the release namespace:
This Helm Chart creates the following Kubernetes resources in the release namespace:
* Capsule Namespace
* Capsule Operator Deployment
* Capsule Service
* CA Secret
* Certfificate Secret
* Certificate Secret
* Tenant Custom Resource Definition
* MutatingWebHookConfiguration
* ValidatingWebHookConfiguration

View File

@@ -30,14 +30,8 @@ spec:
spec:
description: CapsuleConfigurationSpec defines the Capsule configuration
properties:
allowIngressHostnameCollision:
default: true
description: Allow the collision of Ingress resource hostnames across all the Tenants.
type: boolean
allowTenantIngressHostnamesCollision:
description: "When defining the exact match for allowed Ingress hostnames at Tenant level, a collision is not allowed. Toggling this, Capsule will not check if a hostname collision is in place, allowing the creation of two or more Tenant resources although sharing the same allowed hostname(s). \n The JSON path of the resource is: /spec/ingressHostnames/allowed"
type: boolean
forceTenantPrefix:
default: false
description: Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
type: boolean
protectedNamespaceRegex:

View File

@@ -222,11 +222,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -408,11 +412,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -453,9 +461,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -572,7 +580,7 @@ spec:
name: State
type: string
- description: The max amount of Namespaces can be created
jsonPath: .spec.namespaceQuota
jsonPath: .spec.namespaceOptions.quota
name: Namespace quota
type: integer
- description: The total amount of Namespaces in use
@@ -646,21 +654,6 @@ spec:
allowedRegex:
type: string
type: object
enableNodePorts:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
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:
@@ -670,24 +663,37 @@ spec:
- 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.
ingressOptions:
description: Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. 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:
allowedClasses:
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
allowedHostnames:
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
hostnameCollisionScope:
default: Disabled
description: "Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames. \n - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule. \n - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant. \n - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace. \n Optional."
enum:
- Cluster
- Tenant
- Namespace
- Disabled
type: string
type: object
limitRanges:
@@ -759,22 +765,26 @@ spec:
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.
namespaceOptions:
description: Specifies options for the Namespaces, such as additional metadata or 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.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
quota:
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
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
@@ -793,11 +803,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -979,11 +993,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -1024,9 +1042,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -1062,6 +1080,7 @@ spec:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
@@ -1082,7 +1101,7 @@ spec:
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.
description: Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
properties:
allowed:
items:
@@ -1140,17 +1159,55 @@ spec:
type: array
type: object
type: array
scope:
default: Tenant
description: Define if the Resource Budget should compute resource across all Namespaces in the Tenant or individually per cluster. Default is Tenant
enum:
- Tenant
- Namespace
type: string
type: object
servicesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
additionalLabels:
additionalProperties:
type: string
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
loadBalancer:
default: true
description: Specifies if LoadBalancer 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
externalIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no 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
type: object
storageClasses:
@@ -1178,11 +1235,11 @@ spec:
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".
default: Active
description: The operational state of the Tenant. Possible values are "Active", "Cordoned".
enum:
- cordoned
- active
- Cordoned
- Active
type: string
required:
- size

View File

@@ -40,6 +40,9 @@ helm.sh/chart: {{ include "capsule.chart" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- if .Values.customLabels }}
{{ toYaml .Values.customLabels }}
{{- end }}
{{- end }}
{{/*
@@ -50,6 +53,19 @@ app.kubernetes.io/name: {{ include "capsule.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
ServiceAccount annotations
*/}}
{{- define "capsule.serviceAccountAnnotations" -}}
{{- if .Values.serviceAccount.annotations }}
{{- toYaml .Values.serviceAccount.annotations }}
{{- end }}
{{- if .Values.customAnnotations }}
{{ toYaml .Values.customAnnotations }}
{{- end }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}

View File

@@ -3,5 +3,9 @@ kind: Secret
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "capsule.secretCaName" . }}
data:

View File

@@ -3,5 +3,9 @@ kind: Secret
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "capsule.secretTlsName" . }}
data:

View File

@@ -2,6 +2,12 @@ apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
forceTenantPrefix: {{ .Values.manager.options.forceTenantPrefix }}
userGroups:
@@ -9,5 +15,3 @@ spec:
- {{ . }}
{{- end}}
protectedNamespaceRegex: {{ .Values.manager.options.protectedNamespaceRegex | quote }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.deploymentName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
@@ -11,12 +15,12 @@ spec:
{{- include "capsule.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
labels:
{{- include "capsule.selectorLabels" . | nindent 8 }}
{{- include "capsule.labels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:

View File

@@ -4,9 +4,13 @@ kind: Role
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.labels }}
{{- if .Values.serviceMonitor.labels }}
{{- toYaml .Values.serviceMonitor.labels | nindent 4 }}
{{- end }}
{{- end }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "capsule.fullname" . }}-metrics-role
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
rules:

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-controller-manager-metrics-service
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ports:
- port: 8080

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:
- admissionReviewVersions:
- v1
@@ -15,7 +19,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /namespace-owner-reference
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.namespaceOwnerReference.failurePolicy }}
matchPolicy: Equivalent
name: owner.namespace.capsule.clastix.io
namespaceSelector: {}
@@ -28,6 +32,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
scope: '*'

View File

@@ -5,6 +5,10 @@ metadata:
name: {{ include "capsule.fullname" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
fsGroup:
rule: RunAsAny

View File

@@ -6,16 +6,16 @@ kind: Job
metadata:
name: "{{ .Release.Name }}-waiting-certs"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- include "capsule.labels" . | nindent 4 }}
annotations:
# This is what defines this resource as a hook. Without this line, the
# job is considered part of the release.
"helm.sh/hook": post-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
template:
metadata:

View File

@@ -7,16 +7,16 @@ kind: Job
metadata:
name: "{{ .Release.Name }}-rbac-cleaner"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- include "capsule.labels" . | nindent 4 }}
annotations:
# This is what defines this resource as a hook. Without this line, the
# job is considered part of the release.
"helm.sh/hook": pre-delete
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
template:
metadata:

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-proxy-role
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- authentication.k8s.io
@@ -24,6 +28,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-metrics-reader
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- nonResourceURLs:
- /metrics
@@ -36,6 +44,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-proxy-rolebinding
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
@@ -51,6 +63,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-manager-rolebinding
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole

View File

@@ -5,8 +5,8 @@ metadata:
name: {{ include "capsule.serviceAccountName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
{{- if or (.Values.serviceAccount.annotations) (.Values.customAnnotations) }}
annotations:
{{- toYaml . | nindent 4 }}
{{- include "capsule.serviceAccountAnnotations" . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -6,9 +6,13 @@ metadata:
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.labels }}
{{- toYaml .Values.serviceMonitor.labels | nindent 4 }}
{{- with .Values.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
endpoints:
- interval: 15s
@@ -16,7 +20,11 @@ spec:
path: /metrics
jobLabel: app.kubernetes.io/name
selector:
matchLabels: {{ include "capsule.labels" . | nindent 6 }}
matchLabels:
{{- include "capsule.labels" . | nindent 6 }}
{{- with .Values.serviceMonitor.matchLabels }}
{{- toYaml . | nindent 6 }}
{{- end }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:
- admissionReviewVersions:
- v1
@@ -15,13 +19,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /cordoning
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.cordoning.failurePolicy }}
matchPolicy: Equivalent
name: cordoning.tenant.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.cordoning.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -47,13 +49,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /ingresses
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.ingresses.failurePolicy }}
matchPolicy: Equivalent
name: ingress.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.ingresses.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -80,7 +80,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /namespaces
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.namespaces.failurePolicy }}
matchPolicy: Equivalent
name: namespaces.capsule.clastix.io
namespaceSelector: {}
@@ -109,13 +109,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /networkpolicies
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.networkpolicies.failurePolicy }}
matchPolicy: Equivalent
name: networkpolicies.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.networkpolicies.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -140,13 +138,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /pods
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.pods.failurePolicy }}
matchPolicy: Exact
name: pods.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.pods.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -167,14 +163,12 @@ webhooks:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: capsule-system
namespace: {{ .Release.Namespace }}
path: /persistentvolumeclaims
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.persistentvolumeclaims.failurePolicy }}
name: pvc.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.persistentvolumeclaims.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -198,13 +192,11 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /services
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.services.failurePolicy }}
matchPolicy: Exact
name: services.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
{{- toYaml .Values.webhooks.services.namespaceSelector | nindent 4}}
objectSelector: {}
rules:
- apiGroups:
@@ -229,7 +221,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /tenants
port: 443
failurePolicy: Fail
failurePolicy: {{ .Values.webhooks.tenants.failurePolicy }}
matchPolicy: Exact
name: tenants.capsule.clastix.io
namespaceSelector: {}
@@ -248,3 +240,29 @@ webhooks:
scope: '*'
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /nodes
port: 443
failurePolicy: {{ .Values.webhooks.nodes.failurePolicy }}
name: nodes.capsule.clastix.io
matchPolicy: Exact
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
resources:
- nodes
sideEffects: None
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "capsule.fullname" . }}-webhook-service
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ports:
- port: 443

View File

@@ -21,8 +21,6 @@ manager:
forceTenantPrefix: false
capsuleUserGroups: ["capsule.clastix.io"]
protectedNamespaceRegex: ""
allowIngressHostnameCollision: true
allowTenantIngressHostnamesCollision: false
livenessProbe:
httpGet:
path: /healthz
@@ -44,8 +42,6 @@ jobs:
repository: quay.io/clastix/kubectl
pullPolicy: IfNotPresent
tag: "v1.20.7"
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30
imagePullSecrets: []
serviceAccount:
create: true
@@ -68,7 +64,7 @@ podSecurityPolicy:
serviceMonitor:
enabled: false
# Install the ServiceMonitor into a different Namespace, as the monitoring stack one (default: the release one)
namespace:
namespace: ''
# Assign additional labels according to Prometheus' serviceMonitorSelector matching labels
labels: {}
annotations: {}
@@ -76,3 +72,58 @@ serviceMonitor:
serviceAccount:
name: capsule
namespace: capsule-system
# Additional labels
customLabels: {}
# Additional annotations
customAnnotations: {}
# Webhooks configurations
webhooks:
namespaceOwnerReference:
failurePolicy: Fail
cordoning:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
ingresses:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
namespaces:
failurePolicy: Fail
networkpolicies:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
pods:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
persistentvolumeclaims:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
tenants:
failurePolicy: Fail
services:
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
nodes:
failurePolicy: Fail
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30

View File

@@ -30,15 +30,8 @@ spec:
metadata:
type: object
spec:
description: CapsuleConfigurationSpec defines the Capsule configuration nolint:maligned
description: CapsuleConfigurationSpec defines the Capsule configuration
properties:
allowIngressHostnameCollision:
default: true
description: Allow the collision of Ingress resource hostnames across all the Tenants.
type: boolean
allowTenantIngressHostnamesCollision:
description: "When defining the exact match for allowed Ingress hostnames at Tenant level, a collision is not allowed. Toggling this, Capsule will not check if a hostname collision is in place, allowing the creation of two or more Tenant resources although sharing the same allowed hostname(s). \n The JSON path of the resource is: /spec/ingressHostnames/allowed"
type: boolean
forceTenantPrefix:
default: false
description: Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.

View File

@@ -222,11 +222,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -408,11 +412,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -453,9 +461,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -572,7 +580,7 @@ spec:
name: State
type: string
- description: The max amount of Namespaces can be created
jsonPath: .spec.namespaceQuota
jsonPath: .spec.namespaceOptions.quota
name: Namespace quota
type: integer
- description: The total amount of Namespaces in use
@@ -646,21 +654,6 @@ spec:
allowedRegex:
type: string
type: object
enableNodePorts:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
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:
@@ -670,24 +663,37 @@ spec:
- 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.
ingressOptions:
description: Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. 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:
allowedClasses:
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
allowedHostnames:
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
hostnameCollisionScope:
default: Disabled
description: "Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames. \n - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule. \n - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant. \n - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace. \n Optional."
enum:
- Cluster
- Tenant
- Namespace
- Disabled
type: string
type: object
limitRanges:
@@ -759,22 +765,26 @@ spec:
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.
namespaceOptions:
description: Specifies options for the Namespaces, such as additional metadata or 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.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
quota:
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
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
@@ -793,11 +803,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -979,11 +993,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -1024,9 +1042,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -1062,6 +1080,7 @@ spec:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
@@ -1082,7 +1101,7 @@ spec:
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.
description: Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
properties:
allowed:
items:
@@ -1140,17 +1159,55 @@ spec:
type: array
type: object
type: array
scope:
default: Tenant
description: Define if the Resource Budget should compute resource across all Namespaces in the Tenant or individually per cluster. Default is Tenant
enum:
- Tenant
- Namespace
type: string
type: object
servicesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
additionalLabels:
additionalProperties:
type: string
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
loadBalancer:
default: true
description: Specifies if LoadBalancer 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
externalIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no 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
type: object
storageClasses:
@@ -1178,11 +1235,11 @@ spec:
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".
default: Active
description: The operational state of the Tenant. Possible values are "Active", "Cordoned".
enum:
- cordoned
- active
- Cordoned
- Active
type: string
required:
- size

File diff suppressed because it is too large Load Diff

View File

@@ -35,15 +35,8 @@ spec:
metadata:
type: object
spec:
description: CapsuleConfigurationSpec defines the Capsule configuration nolint:maligned
description: CapsuleConfigurationSpec defines the Capsule configuration
properties:
allowIngressHostnameCollision:
default: true
description: Allow the collision of Ingress resource hostnames across all the Tenants.
type: boolean
allowTenantIngressHostnamesCollision:
description: "When defining the exact match for allowed Ingress hostnames at Tenant level, a collision is not allowed. Toggling this, Capsule will not check if a hostname collision is in place, allowing the creation of two or more Tenant resources although sharing the same allowed hostname(s). \n The JSON path of the resource is: /spec/ingressHostnames/allowed"
type: boolean
forceTenantPrefix:
default: false
description: Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
@@ -301,11 +294,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -487,11 +484,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -532,9 +533,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -651,7 +652,7 @@ spec:
name: State
type: string
- description: The max amount of Namespaces can be created
jsonPath: .spec.namespaceQuota
jsonPath: .spec.namespaceOptions.quota
name: Namespace quota
type: integer
- description: The total amount of Namespaces in use
@@ -725,21 +726,6 @@ spec:
allowedRegex:
type: string
type: object
enableNodePorts:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
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:
@@ -749,24 +735,37 @@ spec:
- 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.
ingressOptions:
description: Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. 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:
allowedClasses:
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
allowedHostnames:
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
hostnameCollisionScope:
default: Disabled
description: "Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames. \n - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule. \n - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant. \n - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace. \n Optional."
enum:
- Cluster
- Tenant
- Namespace
- Disabled
type: string
type: object
limitRanges:
@@ -838,22 +837,26 @@ spec:
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.
namespaceOptions:
description: Specifies options for the Namespaces, such as additional metadata or 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.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
quota:
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
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
@@ -872,11 +875,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -1058,11 +1065,15 @@ spec:
items:
description: NetworkPolicyPort describes a port to allow traffic on
properties:
endPort:
description: If set, indicates that the range of ports from port to endPort, inclusive, should be allowed by the policy. This field cannot be defined if the port field is not defined or if the port field is defined as a named (string) port. The endPort must be equal or greater than port. This feature is in Beta state and is enabled by default. It can be disabled using the Feature Gate "NetworkPolicyEndPort".
format: int32
type: integer
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.
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. If present, only traffic on the specified protocol AND port will be matched.
x-kubernetes-int-or-string: true
protocol:
default: TCP
@@ -1103,9 +1114,9 @@ spec:
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
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
description: PolicyType string describes the NetworkPolicy type This type is beta-level in 1.8
type: string
type: array
required:
@@ -1141,6 +1152,7 @@ spec:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
@@ -1161,7 +1173,7 @@ spec:
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.
description: Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
properties:
allowed:
items:
@@ -1219,17 +1231,55 @@ spec:
type: array
type: object
type: array
scope:
default: Tenant
description: Define if the Resource Budget should compute resource across all Namespaces in the Tenant or individually per cluster. Default is Tenant
enum:
- Tenant
- Namespace
type: string
type: object
servicesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
additionalLabels:
additionalProperties:
type: string
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
loadBalancer:
default: true
description: Specifies if LoadBalancer 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
externalIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no 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
type: object
storageClasses:
@@ -1257,11 +1307,11 @@ spec:
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".
default: Active
description: The operational state of the Tenant. Possible values are "Active", "Cordoned".
enum:
- cordoned
- active
- Cordoned
- Active
type: string
required:
- size
@@ -1361,7 +1411,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/clastix/capsule:v0.1.0-rc3
image: quay.io/clastix/capsule:v0.1.1-rc0
imagePullPolicy: IfNotPresent
name: manager
ports:
@@ -1395,8 +1445,6 @@ metadata:
name: capsule-default
namespace: capsule-system
spec:
allowIngressHostnameCollision: false
allowTenantIngressHostnamesCollision: false
forceTenantPrefix: false
protectedNamespaceRegex: ""
userGroups:
@@ -1424,6 +1472,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None
@@ -1539,14 +1588,33 @@ webhooks:
service:
name: capsule-webhook-service
namespace: capsule-system
path: /pods
path: /nodes
failurePolicy: Fail
name: pods.capsule.clastix.io
name: nodes.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
resources:
- nodes
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: capsule-webhook-service
namespace: capsule-system
path: /pods
failurePolicy: Fail
name: pods.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:

View File

@@ -6,5 +6,3 @@ spec:
userGroups: ["capsule.clastix.io"]
forceTenantPrefix: false
protectedNamespaceRegex: ""
allowTenantIngressHostnamesCollision: false
allowIngressHostnameCollision: false

View File

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

View File

@@ -7,5 +7,3 @@ spec:
userGroups: ["capsule.clastix.io"]
forceTenantPrefix: false
protectedNamespaceRegex: ""
allowTenantIngressHostnamesCollision: false
allowIngressHostnameCollision: false

View File

@@ -16,21 +16,31 @@ spec:
- docker.io
- quay.io
allowedRegex: ^\w+.gcr.io$
enableNodePorts: false
externalServiceIPs:
allowed:
- 10.20.0.0/16
- "10.96.42.42"
serviceOptions:
additionalMetadata:
annotations:
capsule.clastix.io/bgp: "true"
labels:
capsule.clastix.io/pool: gas
allowedServices:
nodePort: false
externalName: false
externalIPs:
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$
ingressOptions:
hostnameCollisionScope: Cluster
allowedClasses:
allowed:
- default
allowedRegex: ^\w+-lb$
allowedHostnames:
allowed:
- gas.acmecorp.com
allowedRegex: ^.*acmecorp.com$
limitRanges:
items:
-
@@ -63,12 +73,13 @@ spec:
min:
storage: 1Gi
type: PersistentVolumeClaim
namespaceQuota: 3
namespacesMetadata:
additionalAnnotations:
capsule.clastix.io/backup: "false"
additionalLabels:
capsule.clastix.io/tenant: gas
namespaceOptions:
quota: 3
additionalMetadata:
annotations:
capsule.clastix.io/backup: "false"
labels:
capsule.clastix.io/tenant: gas
networkPolicies:
items:
-
@@ -122,11 +133,6 @@ spec:
-
hard:
requests.storage: 100Gi
servicesMetadata:
additionalAnnotations:
capsule.clastix.io/bgp: "true"
additionalLabels:
capsule.clastix.io/pool: gas
storageClasses:
allowed:
- default

View File

@@ -22,6 +22,7 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None
@@ -117,6 +118,25 @@ webhooks:
resources:
- networkpolicies
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /nodes
failurePolicy: Fail
name: nodes.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
resources:
- nodes
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:

View File

@@ -23,13 +23,13 @@
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/5/namespaceSelector
path: /webhooks/6/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/6/namespaceSelector
path: /webhooks/7/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
@@ -43,12 +43,12 @@
- 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
- op: add
path: /webhooks/7/rules/0/scope
value: Namespaced

View File

@@ -1,7 +1,7 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package rbac
package config
import (
"context"
@@ -25,8 +25,8 @@ type Manager struct {
}
// InjectClient injects the Client interface, required by the Runnable interface
func (r *Manager) InjectClient(c client.Client) error {
r.Client = c
func (c *Manager) InjectClient(client client.Client) error {
c.Client = client
return nil
}
@@ -52,22 +52,22 @@ func forOptionPerInstanceName(instanceName string) builder.ForOption {
})
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
func (c *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
Complete(r)
Complete(c)
}
func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
r.Log.Info("CapsuleConfiguration reconciliation started", "request.name", request.Name)
func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
c.Log.Info("CapsuleConfiguration reconciliation started", "request.name", request.Name)
cfg := configuration.NewCapsuleConfiguration(r.Client, request.Name)
cfg := configuration.NewCapsuleConfiguration(c.Client, request.Name)
// Validating the Capsule Configuration options
if _, err = cfg.ProtectedNamespaceRegexp(); err != nil {
panic(errors.Wrap(err, "Invalid configuration for protected Namespace regex"))
}
r.Log.Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
c.Log.Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
return
}

View File

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

View File

@@ -35,7 +35,7 @@ type CAReconciler struct {
func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, forOptionPerInstanceName(caSecretName)).
For(&corev1.Secret{}, forOptionPerInstanceName(CASecretName)).
Complete(r)
}
@@ -157,7 +157,7 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
privateKeySecretKey: key.Bytes(),
}
group := errgroup.Group{}
group := new(errgroup.Group)
group.Go(func() error {
return r.UpdateMutatingWebhookConfiguration(crt.Bytes())
})

View File

@@ -7,6 +7,6 @@ const (
certSecretKey = "tls.crt"
privateKeySecretKey = "tls.key"
caSecretName = "capsule-ca"
CASecretName = "capsule-ca"
tlsSecretName = "capsule-tls"
)

View File

@@ -22,10 +22,10 @@ func getCertificateAuthority(client client.Client, namespace string) (ca cert.CA
err = client.Get(context.TODO(), types.NamespacedName{
Namespace: namespace,
Name: caSecretName,
Name: CASecretName,
}, instance)
if err != nil {
return nil, fmt.Errorf("missing secret %s, cannot reconcile", caSecretName)
return nil, fmt.Errorf("missing secret %s, cannot reconcile", CASecretName)
}
if instance.Data == nil {

View File

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

View File

@@ -53,8 +53,8 @@ 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.Labels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServiceOptions.AdditionalMetadata.Annotations))
return nil
})
@@ -78,7 +78,7 @@ func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespa
return nil, err
}
if tenant.Spec.ServicesMetadata == nil {
if tenant.Spec.ServiceOptions == nil || tenant.Spec.ServiceOptions.AdditionalMetadata == nil {
return nil, NewNoServicesMetadata(namespacedName.Name)
}

View File

@@ -5,6 +5,7 @@ package servicelabels
import (
"github.com/go-logr/logr"
discoveryv1 "k8s.io/api/discovery/v1"
discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
)
@@ -13,8 +14,8 @@ type EndpointSlicesLabelsReconciler struct {
abstractServiceLabelsReconciler
Log logr.Logger
VersionMinor int
VersionMajor int
VersionMinor uint
VersionMajor uint
}
func (r *EndpointSlicesLabelsReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -24,12 +25,16 @@ func (r *EndpointSlicesLabelsReconciler) SetupWithManager(mgr ctrl.Manager) erro
log: r.Log,
}
if r.VersionMajor == 1 && r.VersionMinor <= 16 {
switch {
case r.VersionMajor == 1 && r.VersionMinor <= 16:
r.Log.Info("Skipping controller setup, as EndpointSlices are not supported on current kubernetes version", "VersionMajor", r.VersionMajor, "VersionMinor", r.VersionMinor)
return nil
case r.VersionMajor == 1 && r.VersionMinor >= 21:
r.abstractServiceLabelsReconciler.obj = &discoveryv1.EndpointSlice{}
default:
r.abstractServiceLabelsReconciler.obj = &discoveryv1beta1.EndpointSlice{}
}
r.abstractServiceLabelsReconciler.obj = &discoveryv1beta1.EndpointSlice{}
return ctrl.NewControllerManagedBy(mgr).
For(r.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName()).
Complete(r)

View File

@@ -0,0 +1,80 @@
package tenant
import (
"context"
"fmt"
"strconv"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *Manager) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
// getting requested LimitRange keys
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items))
for i := range tenant.Spec.LimitRanges.Items {
keys = append(keys, strconv.Itoa(i))
}
group := new(errgroup.Group)
for _, ns := range tenant.Status.Namespaces {
namespace := ns
group.Go(func() error {
return r.syncLimitRange(tenant, namespace, keys)
})
}
return group.Wait()
}
func (r *Manager) syncLimitRange(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
// getting LimitRange labels for the mutateFn
var tenantLabel, limitRangeLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return
}
if limitRangeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.LimitRange{}); err != nil {
return
}
if err = r.pruningResources(namespace, keys, &corev1.LimitRange{}); err != nil {
return
}
for i, spec := range tenant.Spec.LimitRanges.Items {
target := &corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: namespace,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = map[string]string{
tenantLabel: tenant.Name,
limitRangeLabel: strconv.Itoa(i),
}
target.Spec = spec
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", target.GetName()), err)
r.Log.Info("LimitRange sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,121 @@
package tenant
import (
"context"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
type Manager struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1beta1.Tenant{}).
Owns(&corev1.Namespace{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
Owns(&corev1.ResourceQuota{}).
Owns(&rbacv1.RoleBinding{}).
Complete(r)
}
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1beta1.Tenant{}
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
if errors.IsNotFound(err) {
r.Log.Info("Request object not found, could have been deleted after reconcile request")
return reconcile.Result{}, nil
}
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")
if err = r.collectNamespaces(instance); err != nil {
r.Log.Error(err, "Cannot collect Namespace resources")
return
}
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
if err = r.syncNamespaces(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace items")
return
}
r.Log.Info("Starting processing of Network Policies")
if err = r.syncNetworkPolicies(instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
return
}
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.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")
if err = r.syncAdditionalRoleBindings(instance); err != nil {
r.Log.Error(err, "Cannot sync additional RoleBindings items")
return
}
r.Log.Info("Ensuring RoleBinding for owner")
if err = r.ownerRoleBinding(instance); err != nil {
r.Log.Error(err, "Cannot sync owner RoleBinding")
return
}
r.Log.Info("Ensuring Namespace count")
if err = r.ensureNamespaceCount(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace count")
return
}
r.Log.Info("Tenant reconciling completed")
return ctrl.Result{}, err
}
func (r *Manager) 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,183 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tenant
import (
"context"
"fmt"
"strings"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *Manager) syncNamespaces(tenant *capsulev1beta1.Tenant) (err error) {
group := new(errgroup.Group)
for _, item := range tenant.Status.Namespaces {
namespace := item
group.Go(func() error {
return r.syncNamespaceMetadata(namespace, tenant)
})
}
if err = group.Wait(); err != nil {
r.Log.Error(err, "Cannot sync Namespaces")
err = fmt.Errorf("cannot sync Namespaces: %s", err.Error())
}
return
}
func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Tenant) (err error) {
var res controllerutil.OperationResult
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
ns := &corev1.Namespace{}
if conflictErr = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
return
}
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
annotations := make(map[string]string)
labels := map[string]string{
"name": namespace,
capsuleLabel: tnt.GetName(),
}
if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations {
annotations[k] = v
}
}
if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels {
labels[k] = v
}
}
if tnt.Spec.NodeSelector != nil {
var selector []string
for k, v := range tnt.Spec.NodeSelector {
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
}
annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
}
if tnt.Spec.IngressOptions.AllowedClasses != nil {
if len(tnt.Spec.IngressOptions.AllowedClasses.Exact) > 0 {
annotations[capsulev1beta1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressOptions.AllowedClasses.Exact, ",")
}
if len(tnt.Spec.IngressOptions.AllowedClasses.Regex) > 0 {
annotations[capsulev1beta1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressOptions.AllowedClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
annotations[capsulev1beta1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
annotations[capsulev1beta1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
annotations[capsulev1beta1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
annotations[capsulev1beta1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsAnnotation] = value
}
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceLabelsRegexpAnnotation] = value
}
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsAnnotation] = value
}
if value, ok := tnt.Annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
annotations[capsulev1beta1.ForbiddenNamespaceAnnotationsRegexpAnnotation] = value
}
if ns.Annotations == nil {
ns.SetAnnotations(annotations)
} else {
for k, v := range annotations {
ns.Annotations[k] = v
}
}
if ns.Labels == nil {
ns.SetLabels(labels)
} else {
for k, v := range labels {
ns.Labels[k] = v
}
}
return nil
})
return
})
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
return
}
func (r *Manager) ensureNamespaceCount(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1beta1.Tenant{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
}
found.Status.Size = tenant.Status.Size
return r.Client.Status().Update(context.TODO(), found, &client.UpdateOptions{})
})
}
func (r *Manager) collectNamespaces(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
list := &corev1.NamespaceList{}
err = r.Client.List(context.TODO(), list, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
})
if err != nil {
return
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, tenant.DeepCopy(), func() error {
tenant.AssignNamespaces(list.Items)
return r.Client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
})
return
})
}

View File

@@ -0,0 +1,82 @@
package tenant
import (
"context"
"fmt"
"strconv"
"golang.org/x/sync/errgroup"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *Manager) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
// getting requested NetworkPolicy keys
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items))
for i := range tenant.Spec.NetworkPolicies.Items {
keys = append(keys, strconv.Itoa(i))
}
group := new(errgroup.Group)
for _, ns := range tenant.Status.Namespaces {
namespace := ns
group.Go(func() error {
return r.syncNetworkPolicy(tenant, namespace, keys)
})
}
return group.Wait()
}
func (r *Manager) syncNetworkPolicy(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
if err = r.pruningResources(namespace, keys, &networkingv1.NetworkPolicy{}); err != nil {
return
}
// getting NetworkPolicy labels for the mutateFn
var tenantLabel, networkPolicyLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return
}
if networkPolicyLabel, err = capsulev1beta1.GetTypeLabel(&networkingv1.NetworkPolicy{}); err != nil {
return
}
for i, spec := range tenant.Spec.NetworkPolicies.Items {
target := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: namespace,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.SetLabels(map[string]string{
tenantLabel: tenant.Name,
networkPolicyLabel: strconv.Itoa(i),
})
target.Spec = spec
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", target.GetName()), err)
r.Log.Info("Network Policy sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,251 @@
package tenant
import (
"context"
"fmt"
"strconv"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// When the Resource Budget assigned to a Tenant is Tenant-scoped we have to rely on the ResourceQuota resources to
// represent the resource quota for the single Tenant rather than the single Namespace,
// so abusing of this API although its Namespaced scope.
//
// Since a Namespace could take-up all the available resource quota, the Namespace ResourceQuota will be a 1:1 mapping
// to the Tenant one: in first time Capsule is going to sum all the analogous ResourceQuota resources on other Tenant
// namespaces to check if the Tenant quota has been exceeded or not, reusing the native Kubernetes policy putting the
// .Status.Used value as the .Hard value.
// This will trigger following reconciliations 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.
//
// In case of Namespace-scoped Resource Budget, we're just replicating the resources across all registered Namespaces.
func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error) {
// getting ResourceQuota labels for the mutateFn
var tenantLabel, typeLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return err
}
if typeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{}); err != nil {
return err
}
if tenant.Spec.ResourceQuota.Scope == capsulev1beta1.ResourceQuotaScopeTenant {
group := new(errgroup.Group)
for i, q := range tenant.Spec.ResourceQuota.Items {
index := i
resourceQuota := q
group.Go(func() (scopeErr error) {
// Calculating the Resource Budget at Tenant scope just if this is put in place.
// Requirement to list ResourceQuota of the current Tenant
var tntRequirement *labels.Requirement
if tntRequirement, scopeErr = labels.NewRequirement(tenantLabel, selection.Equals, []string{tenant.Name}); scopeErr != nil {
r.Log.Error(scopeErr, "Cannot build ResourceQuota Tenant requirement")
}
// Requirement to list ResourceQuota for the current index
var indexRequirement *labels.Requirement
if indexRequirement, scopeErr = labels.NewRequirement(typeLabel, selection.Equals, []string{strconv.Itoa(index)}); scopeErr != nil {
r.Log.Error(scopeErr, "Cannot build ResourceQuota index requirement")
}
// Listing all the ResourceQuota according to the said requirements.
// These are required since Capsule is going to sum all the used quota to
// sum them and get the Tenant one.
list := &corev1.ResourceQuotaList{}
if scopeErr = r.List(context.TODO(), list, &client.ListOptions{LabelSelector: labels.NewSelector().Add(*tntRequirement).Add(*indexRequirement)}); scopeErr != nil {
r.Log.Error(scopeErr, "Cannot list ResourceQuota", "tenantFilter", tntRequirement.String(), "indexFilter", indexRequirement.String())
return
}
// Iterating over all the options declared for the ResourceQuota,
// summing all the used quota across different Namespaces to determinate
// if we're hitting a Hard quota at Tenant level.
// For this case, we're going to block the Quota setting the Hard as the
// used one.
for name, hardQuota := range resourceQuota.Hard {
r.Log.Info("Desired hard " + name.String() + " quota is " + hardQuota.String())
// Getting the whole usage across all the Tenant Namespaces
var quantity resource.Quantity
for _, item := range list.Items {
quantity.Add(item.Status.Used[name])
}
r.Log.Info("Computed " + name.String() + " quota for the whole Tenant is " + quantity.String())
switch quantity.Cmp(resourceQuota.Hard[name]) {
case 0:
// The Tenant is matching exactly the Quota:
// falling through next case since we have to block further
// resource allocations.
fallthrough
case 1:
// The Tenant is OverQuota:
// updating all the related ResourceQuota with the current
// used Quota to block further creations.
for item := range list.Items {
if _, ok := list.Items[item].Status.Used[name]; ok {
list.Items[item].Spec.Hard[name] = list.Items[item].Status.Used[name]
} else {
um := make(map[corev1.ResourceName]resource.Quantity)
um[name] = resource.Quantity{}
list.Items[item].Spec.Hard = um
}
}
default:
// The Tenant is respecting the Hard quota:
// restoring the default one for all the elements,
// also for the reconciled one.
for item := range list.Items {
if list.Items[item].Spec.Hard == nil {
list.Items[item].Spec.Hard = map[corev1.ResourceName]resource.Quantity{}
}
list.Items[item].Spec.Hard[name] = resourceQuota.Hard[name]
}
}
if scopeErr = r.resourceQuotasUpdate(name, quantity, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
r.Log.Error(scopeErr, "cannot proceed with outer ResourceQuota")
return
}
}
return
})
}
// Waiting the update of all ResourceQuotas
if err = group.Wait(); err != nil {
return
}
}
// getting requested ResourceQuota keys
keys := make([]string, 0, len(tenant.Spec.ResourceQuota.Items))
for i := range tenant.Spec.ResourceQuota.Items {
keys = append(keys, strconv.Itoa(i))
}
group := new(errgroup.Group)
for _, ns := range tenant.Status.Namespaces {
namespace := ns
group.Go(func() error {
return r.syncResourceQuota(tenant, namespace, keys)
})
}
return group.Wait()
}
func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
// getting ResourceQuota labels for the mutateFn
var tenantLabel, typeLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return err
}
if typeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{}); err != nil {
return err
}
// Pruning resource of non-requested resources
if err = r.pruningResources(namespace, keys, &corev1.ResourceQuota{}); err != nil {
return err
}
for index, resQuota := range tenant.Spec.ResourceQuota.Items {
target := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, index),
Namespace: namespace,
},
}
var res controllerutil.OperationResult
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
res, retryErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.SetLabels(map[string]string{
tenantLabel: tenant.Name,
typeLabel: strconv.Itoa(index),
})
target.Spec.Scopes = resQuota.Scopes
target.Spec.ScopeSelector = resQuota.ScopeSelector
// In case of Namespace scope for the ResourceQuota we can easily apply the bare specification
if tenant.Spec.ResourceQuota.Scope == capsulev1beta1.ResourceQuotaScopeNamespace {
target.Spec.Hard = resQuota.Hard
}
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
return retryErr
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err)
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
}
}
return nil
}
// Serial ResourceQuota processing is expensive: using Go routines we can speed it up.
// In case of multiple errors these are logged properly, returning a generic error since we have to repush back the
// reconciliation loop.
func (r *Manager) resourceQuotasUpdate(resourceName corev1.ResourceName, actual, limit resource.Quantity, list ...corev1.ResourceQuota) (err error) {
group := new(errgroup.Group)
for _, item := range list {
rq := item
group.Go(func() (err error) {
found := &corev1.ResourceQuota{}
if err = r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
return
}
return retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
_, retryErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, found, func() error {
// Ensuring annotation map is there to avoid uninitialized map error and
// assigning the overall usage
if found.Annotations == nil {
found.Annotations = make(map[string]string)
}
found.Labels = rq.Labels
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
})
return retryErr
})
})
}
if err = group.Wait(); err != nil {
// We had an error and we mark the whole transaction as failed
// to process it another time according to the Tenant controller back-off factor.
r.Log.Error(err, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
err = fmt.Errorf("update of outer ResourceQuota items has failed: %s", err.Error())
}
return err
}

View File

@@ -0,0 +1,179 @@
package tenant
import (
"context"
"fmt"
"hash/fnv"
"strings"
"golang.org/x/sync/errgroup"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/controllers/rbac"
)
// Additional Role Bindings can be used in many ways: applying Pod Security Policies or giving
// access to CRDs or specific API groups.
func (r *Manager) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err error) {
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hashFn := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(binding.ClusterRoleName))
for _, sub := range binding.Subjects {
_, _ = h.Write([]byte(sub.Kind + sub.Name))
}
return fmt.Sprintf("%x", h.Sum64())
}
// getting requested Role Binding keys
var keys []string
for _, i := range tenant.Spec.AdditionalRoleBindings {
keys = append(keys, hashFn(i))
}
group := new(errgroup.Group)
for _, ns := range tenant.Status.Namespaces {
namespace := ns
group.Go(func() error {
return r.syncAdditionalRoleBinding(tenant, namespace, keys, hashFn)
})
}
return group.Wait()
}
func (r *Manager) syncAdditionalRoleBinding(tenant *capsulev1beta1.Tenant, ns string, keys []string, hashFn func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string) (err error) {
var tenantLabel, roleBindingLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return
}
if roleBindingLabel, err = capsulev1beta1.GetTypeLabel(&rbacv1.RoleBinding{}); err != nil {
return
}
if err = r.pruningResources(ns, keys, &rbacv1.RoleBinding{}); err != nil {
return
}
for i, roleBinding := range tenant.Spec.AdditionalRoleBindings {
roleBindingHashLabel := hashFn(roleBinding)
target := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, roleBinding.ClusterRoleName),
Namespace: ns,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() error {
target.ObjectMeta.Labels = map[string]string{
tenantLabel: tenant.Name,
roleBindingLabel: roleBindingHashLabel,
}
target.RoleRef = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: roleBinding.ClusterRoleName,
}
target.Subjects = roleBinding.Subjects
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", target.GetName()), err)
if err != nil {
r.Log.Error(err, "Cannot sync Additional RoleBinding")
}
r.Log.Info(fmt.Sprintf("Additional RoleBindings sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
}
}
return nil
}
// Each Tenant owner needs the admin Role attached to each Namespace, otherwise no actions on it can be performed.
// 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 *Manager) ownerRoleBinding(tenant *capsulev1beta1.Tenant) error {
// getting RoleBinding label for the mutateFn
var subjects []rbacv1.Subject
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
newLabels := map[string]string{tl: tenant.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,
})
}
}
list := make(map[types.NamespacedName]rbacv1.RoleRef)
for _, i := range tenant.Status.Namespaces {
list[types.NamespacedName{Namespace: i, Name: "namespace:admin"}] = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: "admin",
}
list[types.NamespacedName{Namespace: i, Name: "namespace-deleter"}] = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: rbac.DeleterRoleName,
}
}
for namespacedName, roleRef := range list {
target := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: namespacedName.Name,
Namespace: namespacedName.Namespace,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = newLabels
target.Subjects = subjects
target.RoleRef = roleRef
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err)
r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,63 @@
package tenant
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// 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 *Manager) pruningResources(ns string, keys []string, obj client.Object) (err error) {
var capsuleLabel string
if capsuleLabel, err = capsulev1beta1.GetTypeLabel(obj); err != nil {
return
}
selector := labels.NewSelector()
var exists *labels.Requirement
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
return
}
selector = selector.Add(*exists)
if len(keys) > 0 {
var notIn *labels.Requirement
if notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys); err != nil {
return err
}
selector = selector.Add(*notIn)
}
r.Log.Info("Pruning objects with label selector " + selector.String())
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
ListOptions: client.ListOptions{
LabelSelector: selector,
Namespace: ns,
},
DeleteOptions: client.DeleteOptions{},
})
})
}
func (r *Manager) emitEvent(object runtime.Object, namespace string, res controllerutil.OperationResult, msg string, err error) {
var eventType = corev1.EventTypeNormal
if err != nil {
eventType = corev1.EventTypeWarning
res = "Error"
}
r.Recorder.AnnotatedEventf(object, map[string]string{"OperationResult": string(res)}, eventType, namespace, msg)
}

View File

@@ -1,739 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
"fmt"
"hash/fnv"
"strconv"
"strings"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/controllers/rbac"
)
// TenantReconciler reconciles a Tenant object
type TenantReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *TenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1beta1.Tenant{}).
Owns(&corev1.Namespace{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
Owns(&corev1.ResourceQuota{}).
Owns(&rbacv1.RoleBinding{}).
Complete(r)
}
func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1beta1.Tenant{}
err = r.Get(ctx, request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
r.Log.Info("Request object not found, could have been deleted after reconcile request")
return reconcile.Result{}, nil
}
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")
if err = r.collectNamespaces(instance); err != nil {
r.Log.Error(err, "Cannot collect Namespace resources")
return
}
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
if err = r.syncNamespaces(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace 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
}
}
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
}
}
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")
if err = r.syncAdditionalRoleBindings(instance); err != nil {
r.Log.Error(err, "Cannot sync additional RoleBindings items")
return
}
r.Log.Info("Ensuring RoleBinding for owner")
if err = r.ownerRoleBinding(instance); err != nil {
r.Log.Error(err, "Cannot sync owner RoleBinding")
return
}
r.Log.Info("Ensuring Namespace count")
if err = r.ensureNamespaceCount(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace count")
return
}
r.Log.Info("Tenant reconciling completed")
return ctrl.Result{}, err
}
// 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 := capsulev1beta1.GetTypeLabel(obj)
if err != nil {
return err
}
s := labels.NewSelector()
exists, err := labels.NewRequirement(capsuleLabel, selection.Exists, []string{})
if err != nil {
return err
}
s = s.Add(*exists)
if len(keys) > 0 {
var notIn *labels.Requirement
notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys)
if err != nil {
return err
}
s = s.Add(*notIn)
}
r.Log.Info("Pruning objects with label selector " + s.String())
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
ListOptions: client.ListOptions{
LabelSelector: s,
Namespace: ns,
},
DeleteOptions: client.DeleteOptions{},
})
})
}
// Serial ResourceQuota processing is expensive: using Go routines we can speed it up.
// In case of multiple errors these are logged properly, returning a generic error since we have to repush back the
// reconciliation loop.
func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName, actual, limit resource.Quantity, list ...corev1.ResourceQuota) error {
g := errgroup.Group{}
for _, item := range list {
rq := item
g.Go(func() error {
found := &corev1.ResourceQuota{}
if err := r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
return err
}
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
_, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, found, func() error {
// Ensuring annotation map is there to avoid uninitialized map error and
// assigning the overall usage
if found.Annotations == nil {
found.Annotations = make(map[string]string)
}
found.Labels = rq.Labels
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
})
return err
})
})
}
var err error
if err = g.Wait(); err != nil {
// We had an error and we mark the whole transaction as failed
// to process it another time according to the Tenant controller back-off factor.
r.Log.Error(err, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
err = fmt.Errorf("update of outer ResourceQuota items has failed: %s", err.Error())
}
return err
}
// 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 *capsulev1beta1.Tenant) (err error) {
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hash := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(binding.ClusterRoleName))
for _, sub := range binding.Subjects {
_, _ = h.Write([]byte(sub.Kind + sub.Name))
}
return fmt.Sprintf("%x", h.Sum64())
}
// getting requested Role Binding keys
var keys []string
for _, i := range tenant.Spec.AdditionalRoleBindings {
keys = append(keys, hash(i))
}
var tl, ll string
tl, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return
}
ll, err = capsulev1beta1.GetTypeLabel(&rbacv1.RoleBinding{})
if err != nil {
return
}
for _, ns := range tenant.Status.Namespaces {
if err = r.pruningResources(ns, keys, &rbacv1.RoleBinding{}); err != nil {
return err
}
for i, roleBinding := range tenant.Spec.AdditionalRoleBindings {
lv := hash(roleBinding)
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, roleBinding.ClusterRoleName),
Namespace: ns,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, rb, func() error {
rb.ObjectMeta.Labels = map[string]string{
tl: tenant.Name,
ll: lv,
}
rb.RoleRef = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: roleBinding.ClusterRoleName,
}
rb.Subjects = roleBinding.Subjects
return controllerutil.SetControllerReference(tenant, rb, r.Scheme)
})
r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", rb.GetName()), err)
if err != nil {
r.Log.Error(err, "Cannot sync Additional RoleBinding")
}
r.Log.Info(fmt.Sprintf("Additional RoleBindings sync result: %s", string(res)), "name", rb.Name, "namespace", rb.Namespace)
if err != nil {
return
}
}
}
return nil
}
// We're relying on the ResourceQuota resource to represent the resource quota for the single Tenant rather than the
// single Namespace, so abusing of this API although its Namespaced scope.
// Since a Namespace could take-up all the available resource quota, the Namespace ResourceQuota will be a 1:1 mapping
// to the Tenant one: in a second time Capsule is going to sum all the analogous ResourceQuota resources on other Tenant
// namespaces to check if the Tenant quota has been exceeded or not, reusing the native Kubernetes policy putting the
// .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 *capsulev1beta1.Tenant) error {
// getting requested ResourceQuota keys
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 := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
typeLabel, err := capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{})
if err != nil {
return err
}
for _, ns := range tenant.Status.Namespaces {
if err := r.pruningResources(ns, keys, &corev1.ResourceQuota{}); err != nil {
return err
}
for i, q := range tenant.Spec.ResourceQuota.Items {
target := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.SetLabels(map[string]string{
tenantLabel: tenant.Name,
typeLabel: strconv.Itoa(i),
})
// Requirement to list ResourceQuota of the current Tenant
tr, err := labels.NewRequirement(tenantLabel, selection.Equals, []string{tenant.Name})
if err != nil {
r.Log.Error(err, "Cannot build ResourceQuota Tenant requirement")
}
// Requirement to list ResourceQuota for the current index
ir, err := labels.NewRequirement(typeLabel, selection.Equals, []string{strconv.Itoa(i)})
if err != nil {
r.Log.Error(err, "Cannot build ResourceQuota index requirement")
}
// Listing all the ResourceQuota according to the said requirements.
// These are required since Capsule is going to sum all the used quota to
// sum them and get the Tenant one.
rql := &corev1.ResourceQuotaList{}
err = r.List(context.TODO(), rql, &client.ListOptions{
LabelSelector: labels.NewSelector().Add(*tr).Add(*ir),
})
if err != nil {
r.Log.Error(err, "Cannot list ResourceQuota", "tenantFilter", tr.String(), "indexFilter", ir.String())
return err
}
// Iterating over all the options declared for the ResourceQuota,
// summing all the used quota across different Namespaces to determinate
// if we're hitting a Hard quota at Tenant level.
// For this case, we're going to block the Quota setting the Hard as the
// used one.
for rn, rq := range q.Hard {
r.Log.Info("Desired hard " + rn.String() + " quota is " + rq.String())
// Getting the whole usage across all the Tenant Namespaces
var qt resource.Quantity
for _, rq := range rql.Items {
qt.Add(rq.Status.Used[rn])
}
r.Log.Info("Computed " + rn.String() + " quota for the whole Tenant is " + qt.String())
switch qt.Cmp(q.Hard[rn]) {
case 0:
// The Tenant is matching exactly the Quota:
// falling through next case since we have to block further
// resource allocations.
fallthrough
case 1:
// The Tenant is OverQuota:
// updating all the related ResourceQuota with the current
// used Quota to block further creations.
for i := range rql.Items {
if _, ok := rql.Items[i].Status.Used[rn]; ok {
rql.Items[i].Spec.Hard[rn] = rql.Items[i].Status.Used[rn]
} else {
um := make(map[corev1.ResourceName]resource.Quantity)
um[rn] = resource.Quantity{}
rql.Items[i].Spec.Hard = um
}
}
default:
// The Tenant is respecting the Hard quota:
// restoring the default one for all the elements,
// also for the reconciled one.
for i := range rql.Items {
if rql.Items[i].Spec.Hard == nil {
rql.Items[i].Spec.Hard = map[corev1.ResourceName]resource.Quantity{}
}
rql.Items[i].Spec.Hard[rn] = q.Hard[rn]
}
target.Spec = q
}
if err := r.resourceQuotasUpdate(rn, qt, q.Hard[rn], rql.Items...); err != nil {
r.Log.Error(err, "cannot proceed with outer ResourceQuota")
return err
}
}
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err)
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
}
}
}
return nil
}
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
// getting requested LimitRange keys
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 := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
ll, err := capsulev1beta1.GetTypeLabel(&corev1.LimitRange{})
if err != nil {
return err
}
for _, ns := range tenant.Status.Namespaces {
if err := r.pruningResources(ns, keys, &corev1.LimitRange{}); err != nil {
return err
}
for i, spec := range tenant.Spec.LimitRanges.Items {
t := &corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() (err error) {
t.ObjectMeta.Labels = map[string]string{
tl: tenant.Name,
ll: strconv.Itoa(i),
}
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", t.GetName()), err)
r.Log.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
}
}
}
return nil
}
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Tenant) (err error) {
var res controllerutil.OperationResult
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
ns := &corev1.Namespace{}
if conflictErr = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
return
}
res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
a := make(map[string]string)
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations {
a[k] = v
}
}
if tnt.Spec.NodeSelector != nil {
var selector []string
for k, v := range tnt.Spec.NodeSelector {
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
}
a["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
}
if tnt.Spec.IngressClasses != nil {
if len(tnt.Spec.IngressClasses.Exact) > 0 {
a[capsulev1beta1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
}
if len(tnt.Spec.IngressClasses.Regex) > 0 {
a[capsulev1beta1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
a[capsulev1beta1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
a[capsulev1beta1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
a[capsulev1beta1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
a[capsulev1beta1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
ns.SetAnnotations(a)
l := make(map[string]string)
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalLabels {
l[k] = v
}
}
l["name"] = namespace
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
l[capsuleLabel] = tnt.GetName()
ns.SetLabels(l)
return nil
})
return
})
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
return
}
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1beta1.Tenant) (err error) {
group := errgroup.Group{}
for _, item := range tenant.Status.Namespaces {
namespace := item
group.Go(func() error {
return r.syncNamespaceMetadata(namespace, tenant)
})
}
if err = group.Wait(); err != nil {
r.Log.Error(err, "Cannot sync Namespaces")
err = fmt.Errorf("cannot sync Namespaces: %s", err.Error())
}
return
}
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
// getting requested NetworkPolicy keys
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 := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
nl, err := capsulev1beta1.GetTypeLabel(&networkingv1.NetworkPolicy{})
if err != nil {
return err
}
for _, ns := range tenant.Status.Namespaces {
if err := r.pruningResources(ns, keys, &networkingv1.NetworkPolicy{}); err != nil {
return err
}
for i, spec := range tenant.Spec.NetworkPolicies.Items {
t := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() (err error) {
t.SetLabels(map[string]string{
tl: tenant.Name,
nl: strconv.Itoa(i),
})
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", t.GetName()), err)
r.Log.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
}
}
}
return nil
}
// Each Tenant owner needs the admin Role attached to each Namespace, otherwise no actions on it can be performed.
// 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 *capsulev1beta1.Tenant) error {
// getting RoleBinding label for the mutateFn
var subjects []rbacv1.Subject
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
l := map[string]string{tl: tenant.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)
for _, i := range tenant.Status.Namespaces {
rbl[types.NamespacedName{Namespace: i, Name: "namespace:admin"}] = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: "admin",
}
rbl[types.NamespacedName{Namespace: i, Name: "namespace-deleter"}] = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: rbac.DeleterRoleName,
}
}
for nn, rr := range rbl {
target := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Name,
Namespace: nn.Namespace,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = l
target.Subjects = subjects
target.RoleRef = rr
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err)
r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
}
}
return nil
}
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1beta1.Tenant{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
}
found.Status.Size = tenant.Status.Size
return r.Client.Status().Update(context.TODO(), found, &client.UpdateOptions{})
})
}
func (r *TenantReconciler) emitEvent(object runtime.Object, namespace string, res controllerutil.OperationResult, msg string, err error) {
var eventType = corev1.EventTypeNormal
if err != nil {
eventType = corev1.EventTypeWarning
res = "Error"
}
r.Recorder.AnnotatedEventf(object, map[string]string{"OperationResult": string(res)}, eventType, namespace, msg)
}
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{
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
})
if err != nil {
return
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, tenant.DeepCopy(), func() error {
tenant.AssignNamespaces(nl.Items)
return r.Client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
})
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)
})
}

8
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
*.log
.cache
.DS_Store
src/.temp
node_modules
dist
.env
.env.*

12
docs/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Capsule Documentation
1. Ensure to have [`yarn`](https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable) installed in your path.
2. `yarn install`
## Local development
```shell
yarn develop
```
This will create a local webserver listening on `localhost:8080` with hot-reload of your local changes.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/content/assets/workqueue.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

369
docs/content/dev-guide.md Normal file
View File

@@ -0,0 +1,369 @@
# Capsule Development Guide
## Prerequisites
### Tools
Make sure you have these tools installed:
- [Go 1.16+](https://golang.org/dl/)
- [Operator SDK 1.7.2+](https://github.com/operator-framework/operator-sdk), or [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
- [KinD](https://github.com/kubernetes-sigs/kind) or [k3d](https://k3d.io/), with `kubectl`
- [ngrok](https://ngrok.com/) (if you want to run locally with remote Kubernetes)
- [golangci-lint](https://github.com/golangci/golangci-lint)
- OpenSSL
### Kubernetes Cluster
A lightweight Kubernetes within your laptop can be very handy for Kubernetes-native development like Capsule.
#### By `k3d`
```shell
# Install K3d cli by brew in Mac, or your preferred way
$ brew install k3d
# Export your laptop's IP, e.g. retrieving it by: ifconfig
# Do change this IP to yours
$ export LAPTOP_HOST_IP=192.168.10.101
# Spin up a bare minimum cluster
# Refer to here for more options: https://k3d.io/v4.4.8/usage/commands/k3d_cluster_create/
$ k3d cluster create k3s-capsule --servers 1 --agents 1 --no-lb --k3s-server-arg --tls-san=${LAPTOP_HOST_IP}
# Get Kubeconfig
$ k3d kubeconfig get k3s-capsule > /tmp/k3s-capsule && export KUBECONFIG="/tmp/k3s-capsule"
# This will create a cluster with 1 server and 1 worker node
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3d-k3s-capsule-server-0 Ready control-plane,master 2m13s v1.21.2+k3s1
k3d-k3s-capsule-agent-0 Ready <none> 2m3s v1.21.2+k3s1
# Or 2 Docker containers if you view it from Docker perspective
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5c26ad840c62 rancher/k3s:v1.21.2-k3s1 "/bin/k3s agent" 53 seconds ago Up 45 seconds k3d-k3s-capsule-agent-0
753998879b28 rancher/k3s:v1.21.2-k3s1 "/bin/k3s server --t…" 53 seconds ago Up 51 seconds 0.0.0.0:49708->6443/tcp k3d-k3s-capsule-server-0
```
#### By `kind`
```shell
# # Install kind cli by brew in Mac, or your preferred way
$ brew install kind
# Prepare a kind config file with necessary customization
$ cat > kind.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "0.0.0.0"
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
metadata:
name: config
apiServer:
certSANs:
- localhost
- 127.0.0.1
- kubernetes
- kubernetes.default.svc
- kubernetes.default.svc.cluster.local
- kind
- 0.0.0.0
- ${LAPTOP_HOST_IP}
- role: worker
EOF
# Spin up a bare minimum cluster with 1 master 1 worker node
$ kind create cluster --name kind-capsule --config kind.yaml
# This will create a cluster with 1 server and 1 worker node
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-capsule-control-plane Ready control-plane,master 84s v1.21.1
kind-capsule-worker Ready <none> 56s v1.21.1
# Or 2 Docker containers if you view it from Docker perspective
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7b329fd3a838 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute 0.0.0.0:54894->6443/tcp kind-capsule-control-plane
7d50f1633555 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute kind-capsule-worker
```
## Fork & clone the repository
The `fork-clone-contribute-pr` flow is common for contributing to OSS projects like Kubernetes, Capsule.
Let's assume you've forked it into your GitHub namespace, say `myuser`, and then you can clone it with Git protocol.
Do remember to change the `myuser` to yours.
```shell
$ git clone git@github.com:myuser/capsule.git && cd capsule
```
It's a good practice to add the upsteam as the remote too so we can easily fetch and merge the upstream to our fork:
```shell
$ git remote add upstream https://github.com/clastix/capsule.git
$ git remote -vv
origin git@github.com:myuser/capsule.git (fetch)
origin git@github.com:myuser/capsule.git (push)
upstream https://github.com/clastix/capsule.git (fetch)
upstream https://github.com/clastix/capsule.git (push)
```
## Build & deploy Capsule
```shell
# Download the project dependencies
$ go mod download
# Build the Capsule image
$ make docker-build
# Retrieve the built image version
$ export CAPSULE_IMAGE_VESION=`docker images --format '{{.Tag}}' quay.io/clastix/capsule`
# If k3s, load the image into cluster by
$ k3d image import --cluster k3s-capsule capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
# If Kind, load the image into cluster by
$ kind load docker-image --name kind-capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
# deploy all the required manifests
# Note: 1) please retry if you saw errors; 2) if you want to clean it up first, run: make remove
$ make deploy
# Make sure the controller is running
$ kubectl get pod -n capsule-system
NAME READY STATUS RESTARTS AGE
capsule-controller-manager-5c6b8445cf-566dc 1/1 Running 0 23s
# Check the logs if needed
$ kubectl -n capsule-system logs --all-containers -l control-plane=controller-manager
# You may have a try to deploy a Tenant too to make sure it works end to end
$ kubectl apply -f - <<EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owners:
- name: alice
kind: User
- name: system:serviceaccount:capsule-system:default
kind: ServiceAccount
EOF
# There shouldn't be any errors and you should see the newly created tenant
$ kubectl get tenants
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
oil Active 0 14s
```
If you want to test namespace creation or such stuff, make sure to use impersonation:
```sh
$ kubectl ... --as system:serviceaccount:capsule-system:default --as-group capsule.clastix.io
```
As of now, a complete Capsule environment has been set up in `kind`- or `k3d`-powered cluster, and the `capsule-controller-manager` is running as a deployment serving as:
- The reconcilers for CRDs and;
- A series of webhooks
## Set up development env
During development, we prefer that the code is running within our IDE locally, instead of running as the normal Pod(s) within the Kubernetes cluster.
Such a setup can be illustrated as below diagram:
![Development Env](./assets/dev-env.png)
To achieve that, there are some necessary steps we need to walk through, which have been made as a `make` target within our `Makefile`.
So the TL;DR answer is:
```shell
# If you haven't installed or run `make deploy` before, do it first
# Note: please retry if you saw errors
$ make deploy
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
$ LAPTOP_HOST_IP="<YOUR_LAPTOP_IP>" make dev-setup
```
This is a very common setup for typical Kubernetes Operator development so we'd better walk them through with more details here.
1. Scaling down the deployed Pod(s) to 0
We need to scale the existing replicas of `capsule-controller-manager` to 0 to avoid reconciliation competition between the Pod(s) and the code running outside of the cluster, in our preferred IDE for example.
```shell
$ kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
deployment.apps/capsule-controller-manager scaled
```
2. Preparing TLS certificate for the webhooks
Running webhooks requires TLS, we can prepare the TLS key pair in our development env to handle HTTPS requests.
```shell
# Prepare a simple OpenSSL config file
# Do remember to export LAPTOP_HOST_IP before running this command
$ cat > _tls.cnf <<EOF
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = req_ext
[ req_distinguished_name ]
countryName = SG
stateOrProvinceName = SG
localityName = SG
organizationName = CAPSULE
commonName = CAPSULE
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
IP.1 = ${LAPTOP_HOST_IP}
EOF
# Create this dir to mimic the Pod mount point
$ mkdir -p /tmp/k8s-webhook-server/serving-certs
# Generate the TLS cert/key under /tmp/k8s-webhook-server/serving-certs
$ openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
-extensions req_ext \
-config _tls.cnf \
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
# Clean it up
$ rm -f _tls.cnf
```
3. Patching the Webhooks
By default, the webhooks will be registered with the services, which will route to the Pods, inside the cluster.
We need to _delegate_ the controllers' and webbooks' services to the code running in our IDE by patching the `MutatingWebhookConfiguration` and `ValidatingWebhookConfiguration`.
```shell
# Export your laptop's IP with the 9443 port exposed by controllers/webhooks' services
$ export WEBHOOK_URL="https://${LAPTOP_HOST_IP}:9443"
# Export the cert we just generated as the CA bundle for webhook TLS
$ export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`
# Patch the MutatingWebhookConfiguration webhook
$ kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
--type='json' -p="[\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"${CA_BUNDLE}\"}}\
]"
# Verify it if you want
$ kubectl get MutatingWebhookConfiguration capsule-mutating-webhook-configuration -o yaml
# Patch the ValidatingWebhookConfiguration webhooks
# Note: there is a list of validating webhook endpoints, not just one
$ kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
--type='json' -p="[\
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/cordoning\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/ingresses\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/namespaces\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/networkpolicies\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/pods\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/services\",'caBundle':\"${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/tenants\",'caBundle':\"${CA_BUNDLE}\"}}\
]"
# Verify it if you want
$ kubectl get ValidatingWebhookConfiguration capsule-validating-webhook-configuration -o yaml
```
## Run Capsule outside the cluster
Now we can run Capsule controllers with webhooks outside of the Kubernetes cluster:
```shell
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/
$ go run .
```
To verify that, we can open a new console and create a new Tenant:
```shell
$ kubectl apply -f - <<EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: gas
spec:
owners:
- name: alice
kind: User
EOF
```
We should see output like:
```log
tenant.capsule.clastix.io/gas created
```
And could see logs in the `make run` console like:
```log
...
{"level":"info","ts":"2021-09-28T21:10:30.520+0800","logger":"controllers.Tenant","msg":"Ensuring all Namespaces are collected","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Starting processing of Namespaces","Request.Name":"gas","items":0}
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring additional RoleBindings for owner","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring RoleBinding for owner","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.527+0800","logger":"controllers.Tenant","msg":"Ensuring Namespace count","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.533+0800","logger":"controllers.Tenant","msg":"Tenant reconciling completed","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.540+0800","logger":"controllers.Tenant","msg":"Ensuring all Namespaces are collected","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Starting processing of Namespaces","Request.Name":"gas","items":0}
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring additional RoleBindings for owner","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring RoleBinding for owner","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.547+0800","logger":"controllers.Tenant","msg":"Ensuring Namespace count","Request.Name":"gas"}
{"level":"info","ts":"2021-09-28T21:10:30.554+0800","logger":"controllers.Tenant","msg":"Tenant reconciling completed","Request.Name":"gas"}
```
## Work in your preferred IDE
Now it's time to work through our familiar inner loop for development in our preferred IDE.
For example, if you're using [Visual Studio Code](https://code.visualstudio.com), this `launch.json` file can be a good start.
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": [
"--zap-encoder=console",
"--zap-log-level=debug",
"--configuration-name=capsule-default"
],
"env": {
"NAMESPACE": "capsule-system",
"TMPDIR": "/tmp/"
}
}
]
}
```
Please refer to [contributing](/docs/contributing) for more details while contributing.

8
docs/content/index.md Normal file
View File

@@ -0,0 +1,8 @@
# Capsule Documentation
**Capsule** helps to implement a multi-tenancy and policy-based environment in your Kubernetes cluster. It has been designed as a micro-services based ecosystem with the minimalist approach, leveraging only on upstream Kubernetes.
Currently, the Capsule ecosystem comprises the following:
* [Capsule Operator](/docs/operator/overview)
* [Capsule Proxy](/docs/proxy/overview)
* [Capsule Lens extension](/docs/lens-extension/overview)

View File

@@ -0,0 +1,11 @@
# Capsule extension for Lens
With Capsule extension for [Lens](https://github.com/lensapp/lens), a cluster administrator can easily manage from a single pane of glass all resources of a Kubernetes cluster, including all the Tenants created through the Capsule Operator.
## Features
Capsule extension for Lens provides these capabilities:
- List all tenants
- See tenant details and change through the embedded Lens editor
- Check Resources Quota and Budget at both the tenant and namespace level
Please, see the [README](https://github.com/clastix/capsule-lens-extension) for details about the installation of the Capsule Lens Extension.

View File

@@ -0,0 +1,63 @@
# How to contribute to Capsule
First, thanks for your interest in Capsule, any contribution is welcome!
## Development environment setup
The first step is to set up your local development environment.
Please follow the [Capsule Development Guide](/docs/dev-guide) for details.
## Code convention
The changes must follow the Pull Request method where a _GitHub Action_ will
check the `golangci-lint`, so ensure your changes respect the coding standard.
### golint
You can easily check them issuing the _Make_ recipe `golint`.
```
# make golint
golangci-lint run -c .golangci.yml
```
> Enabled linters and related options are defined in the [.golanci.yml file](https://github.com/clastix/capsule/blob/master/.golangci.yml)
### goimports
Also, the Go import statements must be sorted following the best practice:
```
<STANDARD LIBRARY>
<EXTERNAL PACKAGES>
<LOCAL PACKAGES>
```
To help you out you can use the _Make_ recipe `goimports`
```
# make goimports
goimports -w -l -local "github.com/clastix/capsule" .
```
### Commits
All the Pull Requests must refer to an already open issue: this is the first phase to contribute also for informing maintainers about the issue.
Commit's first line should not exceed 50 columns.
A commit description is welcomed to explain more the changes: just ensure
to put a blank line and an arbitrary number of maximum 72 characters long
lines, at most one blank line between them.
Please, split changes into several and documented small commits: this will help us to perform a better review. Commits must follow the Conventional Commits Specification, a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with Semantic Versioning, by describing the features, fixes, and breaking changes made in commit messages. See [Conventional Commits Specification](https://www.conventionalcommits.org) to learn about Conventional Commits.
> In case of errors or need of changes to previous commits,
> fix them squashing to make changes atomic.
### Miscellanea
Please, add a new single line at end of any file as the current coding style.

View File

@@ -6,54 +6,47 @@ Make sure you have access to a Kubernetes cluster as administrator.
There are two ways to install Capsule:
* Use the Helm Chart available [here](https://github.com/clastix/capsule/tree/master/charts/capsule)
* Use [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
* Use the [single YAML file installer](https://raw.githubusercontent.com/clastix/capsule/master/config/install.yaml)
* Use the [Capsule Helm Chart](https://github.com/clastix/capsule/blob/master/charts/capsule/README.md)
### Install with kustomize
Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
Clone this repository and move to the repo folder:
### 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
```
It will install the Capsule controller in a dedicated namespace `capsule-system`.
# Create your first Tenant
In Capsule, a _Tenant_ is an abstraction to group togheter multiple namespaces in a single entity within a set of bundaries defined by the Cluster Administrator. The tenant is then assigned to a user or group of users who is called _Tenant Owner_.
### Install with Helm Chart
Please, refer to the instructions reported in the Capsule Helm Chart [README](https://github.com/clastix/capsule/blob/master/charts/capsule/README.md).
Capsule defines a Tenant as Custom Resource with cluster scope:
# Create your first Tenant
In Capsule, a _Tenant_ is an abstraction to group multiple namespaces in a single entity within a set of boundaries defined by the Cluster Administrator. The tenant is then assigned to a user or group of users who is called _Tenant Owner_.
Capsule defines a Tenant as Custom Resource with cluster scope.
Create the tenant as cluster admin:
```yaml
cat <<EOF > oil_tenant.yaml
apiVersion: capsule.clastix.io/v1alpha1
kubectl create -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owner:
name: alice
owners:
- name: alice
kind: User
namespaceQuota: 3
EOF
```
Apply as cluster admin:
```
$ kubectl apply -f oil_tenant.yaml
tenant.capsule.clastix.io/oil created
```
You can check the tenant just created as cluster admin
You can check the tenant just created
```
$ kubectl get tenants
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND NODE SELECTOR AGE
oil 3 0 alice User 1m
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
oil Active 0 10s
```
## Tenant owners
@@ -65,27 +58,21 @@ Assignment to a group depends on the authentication strategy in your cluster.
For example, if you are using `capsule.clastix.io`, users authenticated through a _X.509_ certificate must have `capsule.clastix.io` as _Organization_: `-subj "/CN=${USER}/O=capsule.clastix.io"`
Users authenticated through an _OIDC token_ must have
Users authenticated through an _OIDC token_ must have in their token:
```json
...
"users_groups": [
"capsule.clastix.io",
"other_group"
"capsule.clastix.io",
"other_group"
]
```
in their token.
The [hack/create-user.sh](../../hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `alice` user acting as owner of a tenant called `oil`
The [hack/create-user.sh](https://github.com/clastix/capsule/blob/master/hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `alice` user acting as owner of a tenant called `oil`
```bash
./hack/create-user.sh alice oil
creating certs in TMPDIR /tmp/tmp.4CLgpuime3
Generating RSA private key, 2048 bit long modulus (2 primes)
............+++++
........................+++++
e is 65537 (0x010001)
...
certificatesigningrequest.certificates.k8s.io/alice-oil created
certificatesigningrequest.certificates.k8s.io/alice-oil approved
kubeconfig file is: alice-oil.kubeconfig
@@ -112,7 +99,7 @@ $ kubectl -n oil-development run nginx --image=docker.io/nginx
$ kubectl -n oil-development get pods
```
but limited to only your own namespaces:
but limited to only your namespaces:
```
$ kubectl -n kube-system get pods
@@ -120,4 +107,4 @@ Error from server (Forbidden): pods is forbidden: User "alice" cannot list resou
```
# Whats next
The Tenant Owners have full administrative permissions limited to only the namespaces in the assigned tenant. However, their permissions can be controlled by the Cluster Admin by setting rules and policies on the assigned tenant. See the [use cases](./use-cases/overview.md) page for more getting more cool things you can do with Capsule.
The Tenant Owners have full administrative permissions limited to only the namespaces in the assigned tenant. However, their permissions can be controlled by the Cluster Admin by setting rules and policies on the assigned tenant. See the [use cases](/docs/operator/use-cases/overview) page for more getting more cool things you can do with Capsule.

View File

@@ -1,7 +1,6 @@
# Capsule with Amazon EKS
This is an example how to install Amazon EKS cluster and one user
manged by capsule.
# Capsule on AWS EKS
This is an example of how to install AWS 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/)
@@ -115,7 +114,7 @@ EOF
----
Export "admin" kubeconfig to be able to install capsule:
Export "admin" kubeconfig to be able to install Capsule:
```bash
export KUBECONFIG=kubeconfig.conf
@@ -131,7 +130,7 @@ helm upgrade --install --version 0.0.19 --namespace capsule-system --create-name
Use the default Tenant example:
```bash
kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/samples/capsule_v1alpha1_tenant.yaml
kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/samples/capsule_v1beta1_tenant.yaml
```
Based on the tenant configuration above the user `alice` should be able

View File

@@ -0,0 +1,16 @@
# Capsule over Managed Kubernetes
Capsule Operator can be easily installed on a Managed Kubernetes Service. Since in these services, you do not have access to the Kubernetes APIs Server, you should check with your service provider following pre-requisites:
- the default `cluster-admin` ClusterRole is accessible
- the following Admission Webhooks are enabled on the APIs Server:
- PodNodeSelector
- LimitRanger
- ResourceQuota
- MutatingAdmissionWebhook
- ValidatingAdmissionWebhook
* [AWS EKS](/docs/operator/managed-kubernetes/aws-eks)
* CoAKS - Capsule over Azure Kubernetes Service
* Google Cloud GKE
* IBM Cloud
* OVH

View File

@@ -0,0 +1,181 @@
# Monitoring Capsule
The Capsule dashboard allows you to track the health and performance of Capsule manager and tenants, with particular attention to resources saturation, server responses, and latencies.
## Requirements
### Prometheus
Prometheus is an open-source monitoring system and time series database; it is based on a multi-dimensional data model and uses PromQL, a powerful query language, to leverage it.
- Minimum version: 1.0.0
### Grafana
Grafana is an open-source monitoring solution that offers a flexible way to generate visuals and configure dashboards.
- Minimum version: 7.5.5
To fastly deploy this monitoring stack, consider installing the [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator).
---
## Quick Start
The Capsule Helm [charts](https://github.com/clastix/capsule/tree/master/charts/capsule) allow you to automatically create Kubernetes minimum resources needed for the proper functioning of the dashboard:
* ServiceMonitor
* Role
* RoleBinding
N.B: we assume that a ServiceAccount resource has already been created so it can easily interact with the Prometheus API.
### Helm install
During Capsule installation, set the `serviceMonitor` fields as follow:
```yaml
serviceMonitor:
enabled: true
[...]
serviceAccount:
name: <prometheus-sa>
namespace: <prometheus-sa-namespace>
```
Take a look at the Helm charts [README.md](https://github.com/clastix/capsule/blob/master/charts/capsule/README.md#customize-the-installation) file for further customization.
### Check Service Monitor
Verify that the service monitor is working correctly through the Prometheus "targets" page :
![Prometheus Targets](../assets/prometheus_targets.png)
### Deploy dashboard
Simply upload [dashboard.json](https://github.com/clastix/capsule/blob/master/config/grafana/dashboard.json) file to Grafana through _Create_ -> _Import_,
making sure to select the correct Prometheus data source:
![Grafana Import](../assets/upload_json.png)
## In-depth view
### Features
* [Manager controllers](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#manager-controllers)
* [Webhook error rate](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#webhook-error-rate)
* [Webhook latency](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#webhook-latency)
* [REST client latency](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#rest-client-latency)
* [REST client error rate](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#rest-client-error-rate)
* [Saturation](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#saturation)
* [Workqueue](https://github.com/clastix/capsule/blob/master/docs/operator/monitoring.md#workqueue)
---
#### Manager controllers
![Manager controllers](../assets/manager-controllers.png)
##### Description
This section provides information about the medium time delay between manager client input, side effects, and new state determination (reconciliation).
##### Dependant variables and available values
* Controller name
- capsuleconfiguration
- clusterrole
- clusterrolebinding
- endpoints
- endpointslice
- secret
- service
- tenant
#### Webhook error rate
![Webhook error rate](../assets/webhook-error-rate.png)
##### Description
This section provides information about webhook requests response, mainly focusing on server-side errors research.
##### Dependant variables and available values
* Webhook
- cordoning
- ingresses
- namespace-owner-reference
- namespaces
- networkpolicies
- persistentvolumeclaims
- pods
- services
- tenants
#### Webhook latency
![Webhook latency](../assets/webhook-latency.png)
##### Description
This section provides information about the medium time delay between webhook trigger, side effects, and data written on etcd.
##### Dependant variables and available values
* Webhook
- cordoning
- ingresses
- namespace-owner-reference
- namespaces
- networkpolicies
- persistentvolumeclaims
- pods
- services
- tenants
#### REST client latency
![REST client latency](../assets/rest-client-latency.png)
##### Description
This section provides information about the medium time delay between all the calls done by the controller and the API server.
Data display may depend on the REST client verb considered and on available REST client URLs.
YMMV
##### Dependant variables and available values
* REST client URL
* REST client verb
- GET
- PUT
- POST
- PATCH
- DELETE
#### REST client error rate
![REST client error rate](../assets/rest-client-error-rate.png)
##### Description
This section provides information about client total rest requests response per unit time, grouped by thrown code.
#### Saturation
![Saturation](../assets/saturation.png)
##### Description
This section provides information about resources, giving a detailed picture of the systems state and the amount of requested work per active controller.
#### Workqueue
![Workqueue](../assets/workqueue.png)
##### Description
This section provides information about "actions" in the queue, particularly:
- Workqueue latency: time to complete a series of actions in the queue ;
- Workqueue rate: number of actions per unit time ;
- Workqueue depth: number of pending actions waiting in the queue.

View File

@@ -0,0 +1,77 @@
# Allow self-service management of Network Policies
**Profile Applicability:** L2
**Type:** Behavioral
**Category:** Self-Service Operations
**Description:** Tenants should be able to perform self-service operations by creating their own network policies in their namespaces.
**Rationale:** Enables self-service management of network-policies.
**Audit:**
As cluster admin, create a tenant
```yaml
kubectl create -f - <<EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owners:
- kind: User
name: alice
networkPolicies:
items:
- ingress:
- from:
- namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: oil
podSelector: {}
policyTypes:
- Egress
- Ingress
EOF
./create-user.sh alice oil
```
As tenant owner, run the following command to create a namespace in the given tenant
```bash
kubectl --kubeconfig alice create ns oil-production
kubectl --kubeconfig alice config set-context --current --namespace oil-production
```
As tenant owner, retrieve the networkpolicies resources in the tenant namespace
```bash
kubectl --kubeconfig alice get networkpolicies
NAME POD-SELECTOR AGE
capsule-oil-0 <none> 7m5s
```
As a tenant, checks for permissions to manage networkpolicy for each verb
```bash
kubectl --kubeconfig alice auth can-i get networkpolicies
kubectl --kubeconfig alice auth can-i create networkpolicies
kubectl --kubeconfig alice auth can-i update networkpolicies
kubectl --kubeconfig alice auth can-i patch networkpolicies
kubectl --kubeconfig alice auth can-i delete networkpolicies
kubectl --kubeconfig alice auth can-i deletecollection networkpolicies
```
Each command must return 'yes'
**Cleanup:**
As cluster admin, delete all the created resources
```bash
kubectl --kubeconfig cluster-admin delete tenant oil
```

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