Compare commits

..

115 Commits

Author SHA1 Message Date
Dario Tranchitella
a7fff597fa feat: providing log upon CapsuleConfiguration change 2021-05-31 16:15:44 +02:00
Dario Tranchitella
a4128b5744 chore(make): reorg helm params 2021-05-31 16:15:44 +02:00
Dario Tranchitella
b349042265 chore: no need of fmt or vet, already managed by golanci-lint 2021-05-31 16:15:44 +02:00
Dario Tranchitella
40bdf0cd25 test(e2e): typo on feature documentation By group 2021-05-31 16:15:44 +02:00
Dario Tranchitella
20d0ef8ed0 docs: documenting CapsuleConfiguration CRD and options 2021-05-31 16:15:44 +02:00
Dario Tranchitella
61034947fd test(e2e): modifying CapsuleConfiguration at runtime 2021-05-31 16:15:44 +02:00
Dario Tranchitella
ca7b85971b build(helm): deletion of CRB using names rather than label 2021-05-31 16:15:44 +02:00
Dario Tranchitella
73e6a17527 build(helm)!: support for CapsuleConfiguration CRD 2021-05-31 16:15:44 +02:00
Dario Tranchitella
9103a14506 build(kustomize)!: support for CapsuleConfiguration CRD 2021-05-31 16:15:44 +02:00
Dario Tranchitella
d532f1633c refactor: simplifying RBAC managed with multiple user groups 2021-05-31 16:15:44 +02:00
Dario Tranchitella
3570b02427 feat!: using CapsuleConfiguration CRD with reload at runtime 2021-05-31 16:15:44 +02:00
Dario Tranchitella
994a4c282d chore: using last git commit as build date 2021-05-31 16:15:44 +02:00
Dario Tranchitella
eff1282e34 chore: upgrading kubebuilder project to v3 2021-05-31 16:15:44 +02:00
Dario Tranchitella
52a73e011c docs: block of NodePort services using Tenant annotation 2021-05-29 00:31:17 +02:00
Dario Tranchitella
4ccef411ab docs: Pod Priority Class enforcement using Tenant annotations 2021-05-29 00:31:17 +02:00
Dario Tranchitella
dfb0a536b7 test: testing enforced Pod Priority Class using Tenant annotations 2021-05-29 00:31:17 +02:00
Dario Tranchitella
9ef64d0f8c build(helm): providing webhook for Pod Priority Class 2021-05-29 00:31:17 +02:00
Dario Tranchitella
5649283058 build(kustomize): installing Pod Priority Class webhook 2021-05-29 00:31:17 +02:00
Dario Tranchitella
0481822555 feat: enforcing Pod Priority Class 2021-05-29 00:31:17 +02:00
Dario Tranchitella
bcbd9c2781 build(helm): using different names for Job hooks 2021-05-29 00:08:07 +02:00
Maksim Fedotov
229b569b50 fix: the ClusterRoleBindings capsule-namespace-provisioner are not reconciled when --capsule-user-group changes 2021-05-28 09:32:38 +02:00
Maksim Fedotov
ef6eea62dc fix: wrong order of checks in validating-external-service-ips webhook 2021-05-27 19:27:43 +02:00
Maksim Fedotov
bb6614d1e8 chore(ci): output diff files for manifests files 2021-05-25 14:46:05 +02:00
Maksim Fedotov
784f3a71df build(helm): use multiple groups as capsule-user-group. Remove capsule clusterrolebindings using label selector 2021-05-25 14:46:05 +02:00
Maksim Fedotov
3c9895e498 feat: use multiple groups as capsule-user-group 2021-05-25 14:46:05 +02:00
Dario Tranchitella
6dc83b16da fix: generating TLS certificate matching the deployed Namespace 2021-05-23 18:46:25 +02:00
Maksim Fedotov
e6da507d10 feat: block use of NodePort Services 2021-05-19 16:44:08 +02:00
Dario Tranchitella
5bca3b7da7 chore(go): upgrading to go 1.16 2021-05-14 13:55:51 +02:00
Dario Tranchitella
2e188d26f9 chore(operatorsdk): upgrading to v3 format 2021-05-14 13:55:51 +02:00
Dario Tranchitella
3afee659ff chore(kustomize): new CRD and webhooks for admission/v1 2021-05-14 13:55:51 +02:00
Dario Tranchitella
c22cb6cc88 refactor: moving to admission/v1 for Kubernetes +1.16 2021-05-14 13:55:51 +02:00
Dario Tranchitella
202a18c132 chore(mod): upgrading controller-runtime to v0.8.3 2021-05-14 13:55:51 +02:00
Dario Tranchitella
8441d8878a chore(make): upgrading to controller-tools v0.5.0 2021-05-14 13:55:51 +02:00
Dario Tranchitella
d5af190c51 test: checking runtime count for pods 2021-05-14 13:55:51 +02:00
Dario Tranchitella
82ae78b704 chore(kustomize): deprecating metrics RBAC proxy 2021-05-08 14:51:55 +02:00
Dario Tranchitella
6c44a6a4d3 chore(helm): deprecating metrics RBAC proxy 2021-05-08 14:51:55 +02:00
Adriano Pezzuto
d6e7437b6c docs: update capsule-proxy documentation 2021-05-07 20:07:30 +02:00
Dario Tranchitella
ac7114e975 chore: triggering Helm Charts CD upon tag release 2021-05-07 16:20:33 +02:00
Tim Bannister
2fdc08c2f4 docs: typo on README.md 2021-05-05 11:48:30 +02:00
Ludovico Russo
c2cede6287 refactor: better name variables in pkg/webhook/utils 2021-05-04 17:49:13 +02:00
Ludovico Russo
36c90d485e refactor: better name variables in pkg/webhook/tenantprefix 2021-05-04 17:49:13 +02:00
Ludovico Russo
34c958371b refactor: better name variables in pkg/webhook/tenant 2021-05-04 17:49:13 +02:00
Ludovico Russo
e5f17d1e0d refactor: better name variables in pkg/webhook/services 2021-05-04 17:49:13 +02:00
Ludovico Russo
e1b203727d refactor: better name variables in pkg/webhook/registry 2021-05-04 17:49:13 +02:00
Ludovico Russo
cec8cc0573 refactor: better name variables in pkg/webhook/pvc 2021-05-04 17:49:13 +02:00
Ludovico Russo
7ca9fe0c63 refactor: better name variables in pkg/webhook/ownerreference 2021-05-04 17:49:13 +02:00
Ludovico Russo
b87a6c022f refactor: better name variables in pkg/webhook/namespacequota 2021-05-04 17:49:13 +02:00
Ludovico Russo
01b75a5094 refactor: better name variables in pkg/webhook/ingress 2021-05-04 17:49:13 +02:00
Ludovico Russo
2c6dcf0dd7 refactor: better name variables in pkg/webhook 2021-05-04 17:49:13 +02:00
Ludovico Russo
7994ae1da1 refactor: better name variables in main.go 2021-05-04 17:49:13 +02:00
stg
12237ae106 feat: adding name label to each Namespace (#242)
Co-authored-by: Santiago Sanchez Paz <sanchezpaz@gmail.com>
2021-03-24 19:28:45 +01:00
Dario Tranchitella
d8449fee24 Helm and Kustomize to v0.0.5 (#239)
* build(kustomize): ready for v0.0.5

* build(helm): ready for release v0.0.5
2021-03-20 17:25:56 +01:00
Dario Tranchitella
37ec9911d9 chore: non embedding certs for kubeconfig file generation (#238) 2021-03-17 17:28:57 +01:00
Ludovico Russo
36124d2aba build(helm): remove options allow-ingress-hostname-collision and allow-tenant-ingress-hostnames-collision (#233)
These are going to be implemented once 0.0.5 is out with new flags.

Co-authored-by: Ludovico Russo <ludovico@ludusrusso.space>
2021-03-17 11:44:57 +01:00
Dario Tranchitella
5ecabaad3e refactor: ignoring requests from kube-system ServiceAccount resources 2021-03-17 11:43:11 +01:00
Valentino Uberti
56adfe6a35 feat: user script for Openshift (#230)
Tested by @ValentinoUberti on OCP 4.7.1
2021-03-10 15:11:15 +01:00
Dario Tranchitella
4119a69e02 fix: hostname collision between different Tenant namespaces 2021-03-06 20:50:55 +01:00
Dario Tranchitella
51de469551 bug: syncing Namespace annotations in a single place 2021-03-06 17:41:18 +01:00
Dario Tranchitella
87a360bfaf build(helm): support for --allow-tenant-ingress-hostnames-collision flag 2021-03-06 16:58:44 +01:00
Dario Tranchitella
bdce4a7b4f doc: documenting --allow-tenant-ingress-hostnames-collision new CLI flag 2021-03-06 16:58:44 +01:00
Dario Tranchitella
0dedd48789 test: new flag --allow-tenant-ingress-hostnames-collision 2021-03-06 16:58:44 +01:00
Dario Tranchitella
dfb7a5e227 feat: allowing Tenants with collided Ingress hostnames
A new flag (`--allow-tenant-ingress-hostnames-collision`) is added,
defaulted to false: when toggled, Capsule will not check if each
declared hostname in `.spec.IngressHostnames.allowed` is already in use
on any other Tenant.
2021-03-06 16:58:44 +01:00
Dario Tranchitella
d78bcd8b00 test(e2e): using default timeout and interval periods 2021-03-06 15:57:25 +01:00
Dario Tranchitella
0cad87e1ed test(e2e): avoiding reaping of unhealthy nodes blocking CI 2021-03-06 15:57:25 +01:00
Dario Tranchitella
74b0594cf4 feat(helm): customizable liveness and readiness probes 2021-03-06 15:57:25 +01:00
Dario Tranchitella
7fef4e5237 bug: type-switching on Ingress webhook for hostname collision 2021-03-06 15:06:18 +01:00
Dario Tranchitella
4a7c522eb5 bug: disabling ingresses.networking.k8s.io indexers on k8s < 1.19 2021-03-06 15:06:18 +01:00
Dario Tranchitella
8319bd3a85 build(helm): support for allow Ingress hostname collision 2021-03-05 22:50:35 +01:00
Dario Tranchitella
5d3770ae8d doc: documenting --allow-ingress-hostname-collision CLI flag 2021-03-05 22:50:35 +01:00
Dario Tranchitella
3fa78ea3df test: testing Ingress hostname collision 2021-03-05 22:50:35 +01:00
Dario Tranchitella
4fbede0989 feat: Ingress hostnames collision check
Disabled by default to avoid breaking changes for upcoming release,
although minor will be enabled by default.

Using the new `--allow-ingress-hostname-collision` flag Capsule can
ignore the Ingress hostnames collision allowing the Cluster
Administrator to put in place a non-opinionated hostnames allocation.
2021-03-05 22:50:35 +01:00
Davide Imola
d7b19a4930 build: using Docker build args for build metadata (#217) 2021-03-05 21:02:41 +01:00
Dario Tranchitella
452bceff34 fix: additional metadata must be controlled just from Tenant manifest (#211) 2021-03-04 10:02:14 +01:00
Erin Corson
2ea36db5d6 fix(typo): fixing typo in several webhook error messages (#212)
Co-authored-by: Erin Corson <ecorson@vmware.com>
2021-03-04 08:24:11 +01:00
Davide Imola
737b6ce65a Fix link to script (#210) 2021-03-01 22:50:24 +01:00
Unai Arríen
666faeb72a build(helm): making customizable components Docker images (#209) 2021-03-01 17:26:43 +01:00
Don High
4f34483dee Documentation Spelling Mistakes #197 (#203)
* Update README.md

Proof Read the README.md

* Update index.md

Proof Read index.md

* Update overview.md

Proof Read overview.md

* Update onboarding.md

Proof Read onboarding.md

* Update create-namespaces.md

Proof Read create-namespaces.md

* Update permissions.md

Proof Read permissons.md

* Update resources-quota-limits.md

Proof Read resources-quota-limits.md

* Update nodes-pool.md

Proof Read nodes-pool.md

* Update ingress-classes.md

Proof Read ingress-classes.md

* Update ingress-hostnames.md

Proof Read ingress-hostnames.md

* Update storage-classes.md

Proof Read storage-classes.md

* Update images-registries.md

Proof Read images-registries.md

* Update custom-resources.md

Proof Read custom-resources.md

* Update multiple-tenants.md

Proof Read multiple-tenants.md

* Update README.md

Updated the Suggested text

* Update README.md

Made the correction

* Update docs/operator/use-cases/images-registries.md

Co-authored-by: Don High <donghigh@yahoo.com>

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2021-02-19 11:40:20 +01:00
Erin Corson
e3b927f112 some typos and whatnot (#201)
Co-authored-by: Erin Corson <ecorson@vmware.com>
2021-02-16 22:18:02 +01:00
Dario Tranchitella
d9220f1e15 build(helm): avoiding deletion of Capsule secrets on Helm upgrade (#194) 2021-02-08 17:23:44 +01:00
Dario Tranchitella
f03e36e774 test: creating namespace and forcing upload of last built image (#195) 2021-02-08 17:19:23 +01:00
Brian Fox
7c30390206 docs: fix type in README.md (#198) 2021-02-03 16:00:18 +01:00
Dario Tranchitella
16906db309 Validating Tenant also on UPDATE (#191) 2021-01-21 07:11:59 +01:00
Adriano Pezzuto
d25ed7f2df Helm Chart icon fix (#192) 2021-01-16 14:01:13 +01:00
Dario Tranchitella
51f5bec5a6 Fixing the IngressClass return logic breaking Hostnames check (#185) 2021-01-15 09:45:09 +01:00
Dario Tranchitella
d3f3f93a24 CRD schema do not must preserving unknown fields (#188) 2021-01-15 09:44:04 +01:00
Dario Tranchitella
24bd363ee0 Updating v0.0.4 also for Kustomization installation (#186) 2021-01-14 19:22:14 +01:00
Dario Tranchitella
504241a948 Bumping Capsule to v0.0.4 (#183) 2021-01-14 00:00:02 +01:00
Dario Tranchitella
d2700556dd Adding linters and aligning code (#169)
* Adding linters and aligning code

* Aligning ingressHostnames to AllowedListSpec
2021-01-13 23:49:11 +01:00
Paolo Carta
89c66de7c6 Implementing allowed Ingress hostnames (#162)
Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2021-01-13 22:18:09 +01:00
Adriano Pezzuto
a2109b74ef add architecture diagram in readme (#182) 2021-01-07 19:23:38 +01:00
Maxim Fedotov
4dc92451ea IsInCapsuleGroup binary search is case-sensitive broken (#181)
Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
2021-01-05 13:10:27 +01:00
Adriano Pezzuto
46a7a0b917 Update documentation with capsule-proxy (#179)
* update docs for capsule-proxy
* update docs with minor enhancements
* fix broken link
2021-01-02 14:20:57 +01:00
Geofrey Ernest
1ed5d703e6 Short circuit error returns (#175) 2020-12-23 15:08:15 +01:00
Dario Tranchitella
cb986384db Letting tests to accept eventually value, rather than strict expectation (#176) 2020-12-23 10:49:14 +01:00
Dario Tranchitella
49c8131eb5 Adding k8s 1.20 to E2E testing matrix and updating 1.19 to latest stable release (#171) 2020-12-20 23:47:34 +01:00
Dario Tranchitella
82bbd238fb Making tests less flaky (#172) 2020-12-20 23:29:54 +01:00
Dario Tranchitella
03eb6e633e No loop on ResourceQuota outer updates and error handling improvements (#168)
* Avoiding loop on updating outer resource quota

* Using retryOnConflict on Tenant status update

* Using errgroup instead of bare go routines

* Testing Namespace Capsule default label presence
2020-12-20 12:25:41 +01:00
Adriano Pezzuto
6e24aad094 Improve documentation (#146)
* move docs in a separate folder
* review of readme and add faq
* rewrite use cases
* more use cases
* add new project logo
* minor improvements
2020-12-15 00:03:07 +01:00
bsctl
aa6881e32e delete conflicting doc files 2020-12-15 00:00:17 +01:00
Dario Tranchitella
98e441f1e9 Enforcing Service external IPs (#161) 2020-12-11 19:17:46 +01:00
Dario Tranchitella
007bdff512 Only owner Tenant specification key is mandatory (#153)
* Only Tenant owner specification key is mandatory

* Increasing default timeout to avoid e2e flakiness on GH Actions

* Ensuring also empty Namespace annotations and labels
2020-12-11 15:47:29 +01:00
Dario Tranchitella
a3c77b3531 Enhancing Helm Chart lifecycle (#156) 2020-12-10 14:21:41 +01:00
Dario Tranchitella
3e38884a6c Annotating Tenant's Namespaces with allowed registries (#154)
* Updating allowed registries docs w/ Namespace annotations
2020-12-09 15:20:14 +01:00
Dario Tranchitella
40130696bb Annotating ResourceQuota with Hard quota (#158) 2020-12-09 15:19:16 +01:00
Dario Tranchitella
12a8c469e8 Requiring Helm Chart version 2020-12-06 02:26:37 +01:00
Dario Tranchitella
27cdd84b3b Updating Helm instructions (#149) 2020-12-01 23:43:35 +01:00
Dario Tranchitella
f6fd0cfe3f Helm Charts are now inside of the repository (#147)
* Adding Helm chart source

* Pointing to new Chart location

* Setting GitHub Action for remote Helm Chart release

* Updating Go dependencies

* Using Helm as default installation tool

* Separating diff and e2e jobs

* Aligning tests to Helm labels

* Checking fmt and vet, and fixing it

* We don't need limits on E2E
2020-12-01 23:30:31 +01:00
Dario Tranchitella
0641350575 Releasing v0.0.3 (#144) 2020-11-25 17:15:20 +01:00
Dario Tranchitella
5aed7a01d5 Enforcing container registry via list or regex (#142)
Adding also NamespaceSelector to specific webhooks in order to decrease
the chance ov breaking other critical Namespaces in case of Capsule
failures.
2020-11-24 00:40:40 +01:00
Dario Tranchitella
8442eef72b Logging timestamp to ISO 8601 (#140) 2020-11-19 07:58:24 +01:00
Dario Tranchitella
d3bc9f4870 Provide a more meaningful error message when not admitted Storage/Ingress Classes are used (#141)
* Providing further details on non allowed Storage Classes

* Providing further details on non allowed Ingress Classes
2020-11-17 23:44:08 +01:00
Dario Tranchitella
6541f19b67 Automating version pick-up according to current git version and minor Kustomize hotfixes (#135) 2020-11-17 19:20:31 +01:00
Dario Tranchitella
45709f7bd3 Providing additional print column for the nodeSelector Tenant spec (#138) 2020-11-17 16:58:03 +01:00
Dario Tranchitella
2d628e1cd0 Upgrading GitHub actions (#136)
* New KinD GitHub action

* Upgrading golangci-lint
2020-11-17 09:27:53 +01:00
Dario Tranchitella
ea599ba6e6 Supporting additional Role Bindings per Tenant (#133)
* Enabling Capsule to run on a cluster with PodSecurityPolicy enabled

* Supporting additional Role Binding per Tenant

* Documenting the additionalRoleBindings specification
2020-11-16 13:51:44 +01:00
191 changed files with 10645 additions and 3618 deletions

View File

@@ -40,4 +40,5 @@ you'd get this by running `kubectl -n capsule-system logs deploy/capsule-control
# Additional context
- Capsule version: (`capsule --version`)
- Helm Chart version: (`helm list -n capsule-system`)
- Kubernetes version: (`kubectl version`)

44
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CI
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2.3.0
with:
version: latest
only-new-issues: false
args: --timeout 2m --config .golangci.yml
diff:
name: diff
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Cache Go modules
uses: actions/cache@v1
env:
cache-name: go-mod
with:
path: |
~/go/pkg/mod
/home/runner/work/capsule/capsule
key: ${{ runner.os }}-build-${{ env.cache-name }}
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
- run: make manifests
- name: Checking if manifests are disaligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
- name: Checking if manifests generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted
run: test -z "$(git diff 2> /dev/null)"

50
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: e2e
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
jobs:
kind:
name: Kubernetes
strategy:
matrix:
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.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
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 }}-
- run: make manifests
- name: Checking if manifests are disaligned
run: test -z "$(git diff 2> /dev/null)"
- name: Checking if manifests generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Installing Ginkgo
run: go get github.com/onsi/ginkgo/ginkgo
- uses: actions/setup-go@v2
with:
go-version: '^1.13.8'
- uses: engineerd/setup-kind@v0.5.0
with:
skipClusterCreation: true
- uses: azure/setup-helm@v1
with:
version: 3.3.4
- name: e2e testing
run: make e2e/${{ matrix.k8s-version }}

35
.github/workflows/helm.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Helm Chart
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: azure/setup-helm@v1
with:
version: 3.3.4
- name: Linting Chart
run: helm lint ./charts/capsule
release:
if: startsWith(github.ref, 'refs/tags/helm-v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish Helm chart
uses: stefanprodan/helm-gh-pages@master
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
charts_dir: charts
charts_url: https://clastix.github.io/charts
owner: clastix
repository: charts
branch: gh-pages
target_dir: .
commit_username: prometherion
commit_email: dario@tranchitella.eu

View File

@@ -1,61 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2.2.0
with:
# version of golangci-lint to use in form of v1.2.3
version: latest
# if set to true and the action runs on a pull request - the action outputs only newly found issues
only-new-issues: false
args: --timeout 2m
kind:
name: e2e
strategy:
matrix:
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.1']
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Cache Go modules and Docker images
uses: actions/cache@v1
env:
cache-name: gomod-docker
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 }}-
- name: Removing kustomize
run: sudo snap remove kustomize && sudo rm -rf $(which kustomize)
- name: Installing Ginkgo
run: go get github.com/onsi/ginkgo/ginkgo
- uses: actions/setup-go@v2
with:
go-version: '^1.13.8'
- uses: engineerd/setup-kind@v0.4.0
with:
skipClusterCreation: true
- name: e2e testing
run: make e2e/${{ matrix.k8s-version }}

6
.gitignore vendored
View File

@@ -23,4 +23,8 @@ bin
*.swo
*~
hack/*.kubeconfig
**/*.kubeconfig
**/*.crt
**/*.key
.DS_Store

59
.golangci.yml Normal file
View File

@@ -0,0 +1,59 @@
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0
maligned:
suggest-new: true
goimports:
local-prefixes: github.com/clastix/capsule
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- goconst
- gocritic
- gofmt
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- nolintlint
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
- maligned
issues:
exclude:
- Using the variable on range scope .* in function literal
service:
golangci-lint-version: 1.33.x
run:
skip-files:
- "zz_.*\\.go$"

View File

@@ -1,5 +1,12 @@
# Build the manager binary
FROM golang:1.13 as builder
FROM golang:1.16 as builder
ARG GIT_HEAD_COMMIT
ARG GIT_TAG_COMMIT
ARG GIT_LAST_TAG
ARG GIT_MODIFIED
ARG GIT_REPO
ARG BUILD_DATE
WORKDIR /workspace
# Copy the Go Modules manifests
@@ -11,15 +18,16 @@ RUN go mod download
# Copy the go source
COPY main.go main.go
COPY version.go version.go
COPY api/ api/
COPY controllers/ controllers/
COPY pkg/ pkg/
COPY version/ version/
ARG VERSION
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -ldflags "-X github.com/clastix/capsule/version.Version=${VERSION}" -o manager main.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build \
-gcflags "-N -l" \
-ldflags "-X main.GitRepo=$GIT_REPO -X main.GitTag=$GIT_LAST_TAG -X main.GitCommit=$GIT_HEAD_COMMIT -X main.GitDirty=$GIT_MODIFIED -X main.BuildTime=$BUILD_DATE" \
-o manager
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details

View File

@@ -1,5 +1,5 @@
# Current Operator version
VERSION ?= 0.0.1
VERSION ?= $$(git describe --abbrev=0 --tags)
# Default bundle image tag
BUNDLE_IMG ?= quay.io/clastix/capsule:$(VERSION)-bundle
@@ -15,7 +15,7 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# Image URL to use all building/pushing image targets
IMG ?= quay.io/clastix/capsule:$(VERSION)
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true"
CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
@@ -24,10 +24,19 @@ else
GOBIN=$(shell go env GOBIN)
endif
# Get information about git current status
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")
GIT_MODIFIED ?= $$(echo "$(GIT_MODIFIED_1)$(GIT_MODIFIED_2)")
GIT_REPO ?= $$(git config --get remote.origin.url)
BUILD_DATE ?= $$(git log -1 --format="%at" | xargs -I{} date -d @{} +%Y-%m-%dT%H:%M:%S)
all: manager
# Run tests
test: generate fmt vet manifests
test: generate manifests
go test ./... -coverprofile cover.out
# Build manager binary
@@ -35,7 +44,7 @@ manager: generate fmt vet
go build -o bin/manager main.go
# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate fmt vet manifests
run: generate manifests
go run ./main.go
# Install CRDs into a cluster
@@ -55,27 +64,24 @@ deploy: manifests kustomize
remove: manifests kustomize
$(KUSTOMIZE) build config/default | kubectl delete -f -
kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-provisioner --ignore-not-found
kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
# Generate manifests e.g. CRD, RBAC etc.
manifests: controller-gen
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# Run go fmt against code
fmt:
go fmt ./...
# Run go vet against code
vet:
go vet ./...
# Generate code
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
# Build the docker image
docker-build: test
docker build . --build-arg=VERSION=${VERSION} -t ${IMG}
docker build . -t ${IMG} --build-arg GIT_HEAD_COMMIT=$(GIT_HEAD_COMMIT) \
--build-arg GIT_TAG_COMMIT=$(GIT_TAG_COMMIT) \
--build-arg GIT_MODIFIED=$(GIT_MODIFIED) \
--build-arg GIT_REPO=$(GIT_REPO) \
--build-arg GIT_LAST_TAG=$(VERSION) \
--build-arg BUILD_DATE=$(BUILD_DATE)
# Push the docker image
docker-push:
@@ -90,7 +96,7 @@ ifeq (, $(shell which controller-gen))
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$CONTROLLER_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
}
CONTROLLER_GEN=$(GOBIN)/controller-gen
@@ -131,7 +137,7 @@ goimports:
# Linting code as PR is expecting
.PHONY: golint
golint:
golangci-lint run
golangci-lint run -c .golangci.yml
# Running e2e tests in a KinD instance
.PHONY: e2e
@@ -139,7 +145,17 @@ e2e/%:
kind create cluster --name capsule --image=kindest/node:$*
make docker-build
kind load docker-image --nodes capsule-control-plane --name capsule $(IMG)
make deploy
while [ -z $$(kubectl -n capsule-system get secret capsule-tls -o jsonpath='{.data.tls\.crt}') ]; do echo "waiting Capsule to be up and running..." && sleep 5; done
helm upgrade \
--debug \
--install \
--namespace capsule-system \
--create-namespace \
--set 'manager.image.pullPolicy=Never' \
--set 'manager.resources=null'\
--set "manager.image.tag=$(VERSION)" \
--set 'manager.livenessProbe.failureThreshold=10' \
--set 'manager.readinessProbe.failureThreshold=10' \
capsule \
./charts/capsule
ginkgo -v -tags e2e ./e2e
kind delete cluster --name capsule

25
PROJECT
View File

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

193
README.md
View File

@@ -1,10 +1,5 @@
# Capsule
<p align="center">
<img src="assets/logo/space-capsule3.png" />
</p>
<p align="center">
<p align="left">
<img src="https://img.shields.io/github/license/clastix/capsule"/>
<img src="https://img.shields.io/github/go-mod/go-version/clastix/capsule"/>
<a href="https://github.com/clastix/capsule/releases">
@@ -12,33 +7,60 @@
</a>
</p>
<p align="center">
<img src="assets/logo/capsule_medium.png" />
</p>
---
# Kubernetes multi-tenancy made simple
**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?
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well know phenomena of the _clusters sprawl_.
# A multi-tenant operator for Kubernetes
This project provides a custom operator for implementing a strong
multi-tenant environment in _Kubernetes_. **Capsule** is not intended to be yet another _PaaS_, instead, it has been designed as a lightweight tool with a minimalist approach leveraging only the standard features of upstream Kubernetes.
# Entering Capsule
Capsule takes a different approach. In a single cluster, the Capsule Controller aggregates multiple namespaces in a lightweight abstraction called _Tenant_, basically a grouping of Kubernetes Namespaces. Within each tenant, users are free to create their namespaces and share all the assigned resources while the Capsule Policy Engine keeps the different tenants isolated from each other.
# Which is the problem to solve?
Kubernetes introduced the _namespace_ resource to create logical partitions of the
cluster. A Kubernetes namespace creates a sort of isolated *slice* in the
cluster: _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, and
_RBAC_ can be used to enforce isolation among different namespaces. Namespace isolation shines when Kubernetes is used to isolate the different environments or the different types of applications. Also, it works well to isolate applications serving different users when implementing the SaaS delivery model.
The _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, _RBAC_, and other policies defined at the tenant level are automatically inherited by all the namespaces in the tenant. Then users are free to operate their tenants in autonomy, without the intervention of the cluster administrator. Take a look at following diagram:
However, implementing advanced multi-tenancy scenarios, for example, a private or public _Container-as-a-Service_ platform, it becomes soon complicated because of the flat structure of Kubernetes namespaces. In such scenarios, different groups of users get assigned a pool of namespaces with a limited amount of resources (e.g.: _nodes_, _vCPU_, _RAM_, _ephemeral and persistent storage_). When users need more namespaces or move resources from one namespace to another, they always need the intervention of the cluster admin because each namespace still works as an isolated environment. To work around this, and not being overwhelmed by continuous users' requests, cluster admins often choose to create multiple smaller clusters and assign a dedicated cluster to each organization or group of users leading to the well know and painful phenomena of the _clusters sprawl_.
<p align="center" style="padding: 60px 20px">
<img src="assets/capsule-operator.svg" />
</p>
**Capsule** takes a different approach. It aggregates multiple namespaces assigned to an organization or group of users in a lightweight abstraction called _Tenant_. Within each tenant, users are free to create their namespaces and share all the assigned resources between the namespaces of the tenant. The _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, _RBAC_, and other constraints defined at the tenant level are automatically inherited by all the namespaces in the tenant leaving the tenant's users to freely allocate resources without any intervention of the cluster administrator.
# Features
## Self-Service
Leave to developers the freedom to self-provision their cluster resources according to the assigned boundaries.
# Use cases for Capsule
Please, refer to the corresponding [section](use_cases.md) for a more detailed list of use cases that Capsule can address.
## Preventing Clusters Sprawl
Share a single cluster with multiple teams, groups of users, or departments by saving operational and management efforts.
## Governance
Leverage Kubernetes Admission Controllers to enforce the industry security best practices and meet legal requirements.
## Resources Control
Take control of the resources consumed by users while preventing them to overtake.
## Native Experience
Provide multi-tenancy with a native Kubernetes experience without introducing additional management layers, plugins, or customized binaries.
## GitOps ready
Capsule is completely declarative and GitOps ready.
## Bring your own device (BYOD)
Assign to tenants a dedicated set of compute, storage, and network resources and avoid the noisy neighbors' effect.
# Common use cases for Capsule
Please, refer to the corresponding [section](./docs/operator/use-cases/overview.md) in the project documentation for a detailed list of common use cases that Capsule can address.
# Installation
Make sure you have access to a Kubernetes cluster as an administrator.
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-helm-chart)
* Use the Helm Chart available [here](./charts/capsule/README.md)
* Use [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
## Install with kustomize
@@ -47,52 +69,46 @@ Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
Clone this repository and move to the repo folder:
```
make deploy
# /home/prometherion/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# cd config/manager && /usr/local/bin/kustomize edit set image controller=quay.io/clastix/capsule:latest
# /usr/local/bin/kustomize build config/default | kubectl apply -f -
# namespace/capsule-system created
# customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
# clusterrole.rbac.authorization.k8s.io/capsule-proxy-role created
# clusterrole.rbac.authorization.k8s.io/capsule-metrics-reader created
# clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding created
# clusterrolebinding.rbac.authorization.k8s.io/capsule-proxy-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
# mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration created
# validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration created
$ git clone https://github.com/clastix/capsule
$ cd capsule
$ make deploy
```
Log verbosity of the Capsule controller can be increased by passing the `--zap-log-level` option with a value from `1` to `10` or the [basic keywords](https://godoc.org/go.uber.org/zap/zapcore#Level) although it is suggested to use the `--zap-devel` flag to get also stack traces.
It will install the Capsule controller in a dedicated namespace `capsule-system`.
During startup Capsule controller will create additional ClusterRoles `capsule-namespace-deleter`, `capsule-namespace-provisioner` and ClusterRoleBinding `capsule-namespace-provisioner`. These resources are used in order to allow Capsule users to manage their namespaces in tenants.
## How to create Tenants
Use the scaffold [Tenant](config/samples/capsule_v1alpha1_tenant.yaml) and simply apply as cluster admin.
You can disallow users to create namespaces matching a particular regexp by passing `--protected-namespace-regex` option with a value of regular expression.
```
$ kubectl apply -f config/samples/capsule_v1alpha1_tenant.yaml
tenant.capsule.clastix.io/oil created
```
## Admission Controllers
Capsule implements Kubernetes multi-tenancy capabilities using a minimum set of standard [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled on the Kubernetes APIs server: `--enable-admission-plugins=PodNodeSelector,LimitRanger,ResourceQuota,MutatingAdmissionWebhook,ValidatingAdmissionWebhook`. In addition to these default controllers, Capsule implements its own set of Admission Controllers through the [Dynamic Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/), providing callbacks to add further validation or resource patching.
You can check the tenant just created as
All these requests must be served via HTTPS and a CA must be provided to ensure that
the API Server is communicating with the right client. Capsule upon installation is setting its custom Certificate Authority as a client certificate as well, updating all the required resources to minimize the operational tasks.
```
$ kubectl get tenants
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND NODE SELECTOR AGE
oil 3 0 alice User 1m
```
## Tenant users
Each tenant comes with a delegated user acting as the tenant admin. In the Capsule jargon, this user is called the _Tenant Owner_. Other users can operate inside a tenant with different levels of permissions and authorizations assigned directly by the Tenant owner.
## Tenant owners
Each tenant comes with a delegated user or group of users acting as the tenant admin. In the Capsule jargon, this is called the _Tenant Owner_. Other users can operate inside a tenant with different levels of permissions and authorizations assigned directly by the Tenant Owner.
Capsule does not care about the authentication strategy used in the cluster and all the Kubernetes methods of [authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) are supported. The only requirement to use Capsule is to assign tenant users to the the group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
Assignment to a group depends on the authentication strategy in your cluster. For example, if you are using `capsule.clastix.io` as your `--capsule-user-group`, users authenticated through a _X.509_ certificate must have `capsule.clastix.io` as _Organization_: `-subj "/CN=${USER}/O=capsule.clastix.io"`
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
```json
...
"users_groups": [
"/capsule.clastix.io",
"capsule.clastix.io",
"other_group"
]
]
```
in their token.
@@ -112,51 +128,62 @@ kubeconfig file is: alice-oil.kubeconfig
to use it as alice export KUBECONFIG=alice-oil.kubeconfig
```
## How to create a Tenant
Use the [scaffold Tenant](config/samples/capsule_v1alpha1_tenant.yaml)
and simply apply as Cluster Admin.
## Working with Tenants
Log in to the Kubernetes cluster as `alice` tenant owner
```
kubectl apply -f config/samples/capsule_v1alpha1_tenant.yaml
tenant.capsule.clastix.io/oil created
$ export KUBECONFIG=alice-oil.kubeconfig
```
The related Tenant owner `alice` can create Namespaces according to their assigned quota: happy Kubernetes cluster administration!
and create a couple of new namespaces
```
$ kubectl create namespace oil-production
$ kubectl create namespace oil-development
```
As user `alice` you can operate with fully admin permissions:
```
$ kubectl -n oil-development run nginx --image=docker.io/nginx
$ kubectl -n oil-development get pods
```
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"
```
# 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.
```
make remove
# /home/prometherion/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# /usr/local/bin/kustomize build config/default | kubectl delete -f -
# namespace "capsule-system" deleted
# customresourcedefinition.apiextensions.k8s.io "tenants.capsule.clastix.io" deleted
# clusterrole.rbac.authorization.k8s.io "capsule-proxy-role" deleted
# clusterrole.rbac.authorization.k8s.io "capsule-metrics-reader" deleted
# clusterrolebinding.rbac.authorization.k8s.io "capsule-manager-rolebinding" deleted
# clusterrolebinding.rbac.authorization.k8s.io "capsule-proxy-rolebinding" deleted
# secret "capsule-ca" deleted
# secret "capsule-tls" deleted
# service "capsule-controller-manager-metrics-service" deleted
# service "capsule-webhook-service" deleted
# deployment.apps "capsule-controller-manager" deleted
# mutatingwebhookconfiguration.admissionregistration.k8s.io "capsule-mutating-webhook-configuration" deleted
# validatingwebhookconfiguration.admissionregistration.k8s.io "capsule-validating-webhook-configuration" deleted
$ make remove
```
# How to contribute
Any contribution is welcome! Please refer to the corresponding [section](contributing.md).
# Production Grade
Although under frequent development and improvements, Capsule is ready to be used in production environments: check out the **Release** page for a detailed list of available versions.
# FAQ
tbd
- Q. How to pronounce Capsule?
# Changelog
tbd
A. It should be pronounced as `/ˈkæpsjuːl/`.
# Roadmap
tbd
- 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.
- Q. Does it work with my Kubernetes XYZ distribution?
A. We tested Capsule with vanilla Kubernetes 1.16+ on private environments and public clouds. We expect it to work smoothly on any other Kubernetes distribution. Please, let us know if you find it doesn't.
- Q. Do you provide commercial support?
A. Yes, we're available to help and provide commercial support. [Clastix](https://clastix.io) is the company behind Capsule. Please, contact us for a quote.

View File

@@ -17,27 +17,30 @@ limitations under the License.
package v1alpha1
import (
"regexp"
"sort"
"strings"
)
type NamespaceList []string
func (n NamespaceList) Len() int {
return len(n)
type AllowedListSpec struct {
Exact []string `json:"allowed,omitempty"`
Regex string `json:"allowedRegex,omitempty"`
}
func (n NamespaceList) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
func (n NamespaceList) Less(i, j int) bool {
return strings.ToLower(n[i]) < strings.ToLower(n[j])
}
func (n NamespaceList) IsStringInList(value string) (ok bool) {
sort.Sort(n)
i := sort.SearchStrings(n, value)
ok = i < n.Len() && n[i] == value
func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
if len(in.Exact) > 0 {
sort.SliceStable(in.Exact, func(i, j int) bool {
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
})
i := sort.SearchStrings(in.Exact, value)
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
}
func (in AllowedListSpec) RegexMatch(value string) (ok bool) {
if len(in.Regex) > 0 {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
}

View File

@@ -0,0 +1,80 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAllowedListSpec_ExactMatch(t *testing.T) {
type tc struct {
In []string
True []string
False []string
}
for _, tc := range []tc{
{
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"foo", "bar", "bizz", "buzz"},
[]string{"bing", "bong"},
},
{
[]string{"one", "two", "three"},
[]string{"one", "two", "three"},
[]string{"a", "b", "c"},
},
{
nil,
nil,
[]string{"any", "value"},
},
} {
a := AllowedListSpec{
Exact: tc.In,
}
for _, ok := range tc.True {
assert.True(t, a.ExactMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.ExactMatch(ko))
}
}
}
func TestAllowedListSpec_RegexMatch(t *testing.T) {
type tc struct {
Regex string
True []string
False []string
}
for _, tc := range []tc{
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
{``, nil, []string{"any", "value"}},
} {
a := AllowedListSpec{
Regex: tc.Regex,
}
for _, ok := range tc.True {
assert.True(t, a.RegexMatch(ok))
}
for _, ko := range tc.False {
assert.False(t, a.RegexMatch(ko))
}
}
}

View File

@@ -0,0 +1,68 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CapsuleConfigurationSpec defines the Capsule configuration
// nolint:maligned
type CapsuleConfigurationSpec struct {
// Names of the groups for Capsule users.
// +kubebuilder:default={capsule.clastix.io}
UserGroups []string `json:"userGroups,omitempty"`
// 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.
// +kubebuilder:default=false
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
// +kubebuilder:resource:scope=Cluster
// CapsuleConfiguration is the Schema for the Capsule configuration API
type CapsuleConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CapsuleConfigurationSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// CapsuleConfigurationList contains a list of CapsuleConfiguration
type CapsuleConfigurationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CapsuleConfiguration `json:"items"`
}
func init() {
SchemeBuilder.Register(&CapsuleConfiguration{}, &CapsuleConfigurationList{})
}

View File

@@ -16,6 +16,7 @@ limitations under the License.
package domain
type SearchIn interface {
IsStringInList(value string) bool
type AllowedList interface {
ExactMatch(value string) bool
RegexMatch(value string) bool
}

View File

@@ -0,0 +1,51 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package domain
import (
"regexp"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
)
const (
podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed"
podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex"
)
func NewPodPriority(object metav1.Object) (allowed *v1alpha1.AllowedListSpec) {
annotations := object.GetAnnotations()
if v, ok := annotations[podPriorityAllowedAnnotation]; ok {
allowed = &v1alpha1.AllowedListSpec{}
allowed.Exact = strings.Split(v, ",")
}
if v, ok := annotations[podPriorityAllowedRegexAnnotation]; ok {
if _, err := regexp.Compile(v); err == nil {
if allowed == nil {
allowed = &v1alpha1.AllowedListSpec{}
}
allowed.Regex = v
}
}
return
}

View File

@@ -0,0 +1,85 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package domain
import (
"regexp"
)
const defaultRegistryName = "docker.io"
type registry map[string]string
func (r registry) Registry() string {
res, ok := r["registry"]
if !ok {
return ""
}
if len(res) == 0 {
return defaultRegistryName
}
return res
}
func (r registry) Repository() string {
res, ok := r["repository"]
if !ok {
return ""
}
if res == defaultRegistryName {
return ""
}
return res
}
func (r registry) Image() string {
res, ok := r["image"]
if !ok {
return ""
}
return res
}
func (r registry) Tag() string {
res, ok := r["tag"]
if !ok {
return ""
}
if len(res) == 0 {
res = "latest"
}
return res
}
func NewRegistry(value string) Registry {
reg := make(registry)
r := regexp.MustCompile(`(((?P<registry>[a-zA-Z0-9-.]+)\/)?((?P<repository>[a-zA-Z0-9-.]+)\/))?(?P<image>[a-zA-Z0-9-.]+)(:(?P<tag>[a-zA-Z0-9-.]+))?`)
match := r.FindStringSubmatch(value)
for i, name := range r.SubexpNames() {
if i > 0 && i <= len(match) {
reg[name] = match[i]
}
}
return reg
}
type Registry interface {
Registry() string
Repository() string
Image() string
Tag() string
}

View File

@@ -0,0 +1,78 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package domain
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRegistry(t *testing.T) {
type tc struct {
registry string
repo string
image string
tag string
}
for name, tc := range map[string]tc{
"docker.io/my-org/my-repo:v0.0.1": {
registry: "docker.io",
repo: "my-org",
image: "my-repo",
tag: "v0.0.1",
},
"unnamed/repository:1.2.3": {
registry: "docker.io",
repo: "unnamed",
image: "repository",
tag: "1.2.3",
},
"quay.io/clastix/capsule:v1.0.0": {
registry: "quay.io",
repo: "clastix",
image: "capsule",
tag: "v1.0.0",
},
"docker.io/redis:alpine": {
registry: "docker.io",
repo: "",
image: "redis",
tag: "alpine",
},
"nginx:alpine": {
registry: "docker.io",
repo: "",
image: "nginx",
tag: "alpine",
},
"nginx": {
registry: "docker.io",
repo: "",
image: "nginx",
tag: "latest",
},
} {
t.Run(name, func(t *testing.T) {
r := NewRegistry(name)
assert.Equal(t, tc.registry, r.Registry())
assert.Equal(t, tc.repo, r.Repository())
assert.Equal(t, tc.image, r.Image())
assert.Equal(t, tc.tag, r.Tag())
})
}
}

View File

@@ -18,26 +18,25 @@ package v1alpha1
import (
"sort"
"strings"
)
type StorageClassList []string
type IngressHostnamesList []string
func (n StorageClassList) Len() int {
return len(n)
func (hostnames IngressHostnamesList) Len() int {
return len(hostnames)
}
func (n StorageClassList) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
func (hostnames IngressHostnamesList) Swap(i, j int) {
hostnames[i], hostnames[j] = hostnames[j], hostnames[i]
}
func (n StorageClassList) Less(i, j int) bool {
return strings.ToLower(n[i]) < strings.ToLower(n[j])
func (hostnames IngressHostnamesList) Less(i, j int) bool {
return hostnames[i] < hostnames[j]
}
func (n StorageClassList) IsStringInList(value string) (ok bool) {
sort.Sort(n)
i := sort.SearchStrings(n, value)
ok = i < n.Len() && n[i] == value
func (hostnames IngressHostnamesList) IsStringInList(value string) (ok bool) {
sort.Sort(hostnames)
i := sort.SearchStrings(hostnames, value)
ok = i < hostnames.Len() && hostnames[i] == value
return
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"fmt"
)
const (
@@ -25,8 +25,14 @@ const (
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
)
func UsedQuotaFor(resource corev1.ResourceName) string {
func UsedQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/used-" + resource.String()
}
func HardQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/hard-" + resource.String()
}

View File

@@ -23,7 +23,11 @@ import (
)
func (t *Tenant) IsFull() bool {
return t.Status.Namespaces.Len() >= int(t.Spec.NamespaceQuota)
// we don't have limits on assigned Namespaces
if t.Spec.NamespaceQuota == nil {
return false
}
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
}
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {

View File

@@ -21,6 +21,7 @@ import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -34,6 +35,8 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
return "capsule.clastix.io/network-policy", nil
case *corev1.ResourceQuota:
return "capsule.clastix.io/resource-quota", nil
case *rbacv1.RoleBinding:
return "capsule.clastix.io/role-binding", nil
default:
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}

View File

@@ -19,49 +19,51 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:validation:Minimum=1
type NamespaceQuota uint
type AdditionalMetadata struct {
// +nullable
AdditionalLabels map[string]string `json:"additionalLabels"`
// +nullable
AdditionalAnnotations map[string]string `json:"additionalAnnotations"`
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}
type StorageClassesSpec struct {
// +nullable
Allowed StorageClassList `json:"allowed"`
// +nullable
AllowedRegex string `json:"allowedRegex"`
type IngressHostnamesSpec struct {
Allowed IngressHostnamesList `json:"allowed"`
AllowedRegex string `json:"allowedRegex"`
}
type IngressClassesSpec struct {
// +nullable
Allowed IngressClassList `json:"allowed"`
// +nullable
AllowedRegex string `json:"allowedRegex"`
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPs struct {
Allowed []AllowedIP `json:"allowed"`
}
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
Owner OwnerSpec `json:"owner"`
// +kubebuilder:validation:Optional
NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata"`
// +kubebuilder:validation:Optional
ServicesMetadata AdditionalMetadata `json:"servicesMetadata"`
StorageClasses StorageClassesSpec `json:"storageClasses"`
IngressClasses IngressClassesSpec `json:"ingressClasses"`
// +kubebuilder:validation:Optional
NodeSelector map[string]string `json:"nodeSelector"`
NamespaceQuota NamespaceQuota `json:"namespaceQuota"`
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges []corev1.LimitRangeSpec `json:"limitRanges"`
// +kubebuilder:validation:Optional
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas"`
//+kubebuilder:validation:Minimum=1
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata,omitempty"`
ServicesMetadata AdditionalMetadata `json:"servicesMetadata,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges []corev1.LimitRangeSpec `json:"limitRanges,omitempty"`
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
AdditionalRoleBindings []AdditionalRoleBindings `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPs `json:"externalServiceIPs,omitempty"`
}
type AdditionalRoleBindings struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}
// OwnerSpec defines tenant owner name and kind
@@ -79,10 +81,8 @@ func (k Kind) String() string {
// TenantStatus defines the observed state of Tenant
type TenantStatus struct {
Size uint `json:"size"`
Namespaces NamespaceList `json:"namespaces,omitempty"`
Users []string `json:"users,omitempty"`
Groups []string `json:"groups,omitempty"`
Size uint `json:"size"`
Namespaces []string `json:"namespaces,omitempty"`
}
// +kubebuilder:object:root=true
@@ -92,6 +92,7 @@ type TenantStatus struct {
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// +kubebuilder:printcolumn:name="Owner name",type="string",JSONPath=".spec.owner.name",description="The assigned Tenant owner"
// +kubebuilder:printcolumn:name="Owner kind",type="string",JSONPath=".spec.owner.kind",description="The assigned Tenant owner kind"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API

View File

@@ -23,6 +23,7 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -56,63 +57,182 @@ func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in IngressClassList) DeepCopyInto(out *IngressClassList) {
{
in := &in
*out = make(IngressClassList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressClassList.
func (in IngressClassList) DeepCopy() IngressClassList {
if in == nil {
return nil
}
out := new(IngressClassList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressClassesSpec) DeepCopyInto(out *IngressClassesSpec) {
func (in *AdditionalRoleBindings) DeepCopyInto(out *AdditionalRoleBindings) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(IngressClassList, len(*in))
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]rbacv1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressClassesSpec.
func (in *IngressClassesSpec) DeepCopy() *IngressClassesSpec {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindings.
func (in *AdditionalRoleBindings) DeepCopy() *AdditionalRoleBindings {
if in == nil {
return nil
}
out := new(IngressClassesSpec)
out := new(AdditionalRoleBindings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in NamespaceList) DeepCopyInto(out *NamespaceList) {
{
in := &in
*out = make(NamespaceList, len(*in))
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceList.
func (in NamespaceList) DeepCopy() NamespaceList {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedListSpec.
func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
if in == nil {
return nil
}
out := new(NamespaceList)
out := new(AllowedListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfiguration) DeepCopyInto(out *CapsuleConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfiguration.
func (in *CapsuleConfiguration) DeepCopy() *CapsuleConfiguration {
if in == nil {
return nil
}
out := new(CapsuleConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CapsuleConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfigurationList) DeepCopyInto(out *CapsuleConfigurationList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]CapsuleConfiguration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfigurationList.
func (in *CapsuleConfigurationList) DeepCopy() *CapsuleConfigurationList {
if in == nil {
return nil
}
out := new(CapsuleConfigurationList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CapsuleConfigurationList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec) {
*out = *in
if in.UserGroups != nil {
in, out := &in.UserGroups, &out.UserGroups
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfigurationSpec.
func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
if in == nil {
return nil
}
out := new(CapsuleConfigurationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPs) DeepCopyInto(out *ExternalServiceIPs) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]AllowedIP, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPs.
func (in *ExternalServiceIPs) DeepCopy() *ExternalServiceIPs {
if in == nil {
return nil
}
out := new(ExternalServiceIPs)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in IngressHostnamesList) DeepCopyInto(out *IngressHostnamesList) {
{
in := &in
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesList.
func (in IngressHostnamesList) DeepCopy() IngressHostnamesList {
if in == nil {
return nil
}
out := new(IngressHostnamesList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressHostnamesSpec) DeepCopyInto(out *IngressHostnamesSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesSpec.
func (in *IngressHostnamesSpec) DeepCopy() *IngressHostnamesSpec {
if in == nil {
return nil
}
out := new(IngressHostnamesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
@@ -128,45 +248,6 @@ func (in *OwnerSpec) DeepCopy() *OwnerSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in StorageClassList) DeepCopyInto(out *StorageClassList) {
{
in := &in
*out = make(StorageClassList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageClassList.
func (in StorageClassList) DeepCopy() StorageClassList {
if in == nil {
return nil
}
out := new(StorageClassList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageClassesSpec) DeepCopyInto(out *StorageClassesSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(StorageClassList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageClassesSpec.
func (in *StorageClassesSpec) DeepCopy() *StorageClassesSpec {
if in == nil {
return nil
}
out := new(StorageClassesSpec)
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
@@ -230,10 +311,33 @@ func (in *TenantList) DeepCopyObject() runtime.Object {
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
out.Owner = in.Owner
if in.NamespaceQuota != nil {
in, out := &in.NamespaceQuota, &out.NamespaceQuota
*out = new(int32)
**out = **in
}
in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata)
in.ServicesMetadata.DeepCopyInto(&out.ServicesMetadata)
in.StorageClasses.DeepCopyInto(&out.StorageClasses)
in.IngressClasses.DeepCopyInto(&out.IngressClasses)
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressClasses != nil {
in, out := &in.IngressClasses, &out.IngressClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressHostnames != nil {
in, out := &in.IngressHostnames, &out.IngressHostnames
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
@@ -262,6 +366,18 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindings, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPs)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.
@@ -279,16 +395,6 @@ func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = *in
if in.Namespaces != nil {
in, out := &in.Namespaces, &out.Namespaces
*out = make(NamespaceList, len(*in))
copy(*out, *in)
}
if in.Users != nil {
in, out := &in.Users, &out.Users
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Groups != nil {
in, out := &in.Groups, &out.Groups
*out = make([]string, len(*in))
copy(*out, *in)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1 +0,0 @@
Icons made by [Roundicons](https://www.flaticon.com/authors/roundicons) from [www.flaticon.com](https://www.flaticon.com).

BIN
assets/logo/capsule.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

101
assets/logo/capsule.svg Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Livello_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.28 841.89" style="enable-background:new 0 0 595.28 841.89;" xml:space="preserve">
<style type="text/css">
.st0{fill:#274872;}
.st1{fill:#314A70;}
.st2{fill:#5783AB;}
.st3{fill:#EAECEC;}
</style>
<path class="st0" d="M243.53,178.65c-0.06-4.5-0.37-9.02,0-13.49c0.1-1.22,2.13-3.09,3.45-3.25c6.99-0.88,14.03-1.47,21.07-1.8
c2.43-0.12,3.48-1.05,4.29-3.12c2-5.14,4.08-10.25,6.32-15.29c0.86-1.93,0.56-2.83-1.2-4.09c-4.42-3.15-4.97-8.41-1.6-12.08
c3.7-4.04,8.88-4.09,12.65-0.12c3.5,3.68,3.07,8.88-1.39,12.08c-1.93,1.39-2.08,2.44-1.22,4.44c2.19,5.06,3.96,10.31,6.33,15.27
c0.65,1.37,2.73,2.73,4.28,2.89c7.57,0.77,15.19,1.17,22.79,1.64c2.69,0.16,4.13,1.28,4.21,4.15c0.1,3.95,0.43,7.89,0.66,11.84
c-1.51,0.05-3.03,0.22-4.53,0.13c-12.54-0.76-37.47-2.65-37.47-2.65S254.81,177.52,243.53,178.65z"/>
<g>
<path class="st1" d="M73.32,483.91c-5.2-2.69-9.26-6.43-12.18-11.22c-2.92-4.78-4.38-10.21-4.38-16.28c0-6.07,1.46-11.5,4.38-16.28
c2.92-4.78,6.98-8.52,12.18-11.22c5.2-2.69,11.06-4.04,17.59-4.04c6.45,0,12.09,1.35,16.91,4.04c4.82,2.7,8.33,6.55,10.53,11.56
l-13.78,7.4c-3.19-5.62-7.78-8.43-13.78-8.43c-4.63,0-8.47,1.52-11.5,4.55c-3.04,3.04-4.55,7.17-4.55,12.41
c0,5.24,1.52,9.38,4.55,12.41c3.04,3.04,6.87,4.55,11.5,4.55c6.07,0,10.66-2.81,13.78-8.43l13.78,7.52
c-2.2,4.86-5.71,8.65-10.53,11.39c-4.82,2.73-10.46,4.1-16.91,4.1C84.38,487.95,78.52,486.6,73.32,483.91z"/>
<path class="st1" d="M175.17,431.64c5.08,4.52,7.63,11.33,7.63,20.44v34.96h-16.62v-7.63c-3.34,5.69-9.56,8.54-18.67,8.54
c-4.71,0-8.79-0.8-12.24-2.39c-3.46-1.59-6.09-3.79-7.91-6.6c-1.82-2.81-2.73-6-2.73-9.56c0-5.69,2.14-10.17,6.43-13.44
c4.29-3.26,10.91-4.9,19.87-4.9h14.12c0-3.87-1.18-6.85-3.53-8.94c-2.35-2.09-5.88-3.13-10.59-3.13c-3.26,0-6.47,0.51-9.62,1.54
c-3.15,1.03-5.83,2.41-8.03,4.16l-6.38-12.41c3.34-2.35,7.34-4.17,12.01-5.47c4.67-1.29,9.47-1.94,14.4-1.94
C162.8,424.87,170.08,427.13,175.17,431.64z M160.03,473.89c2.35-1.4,4.02-3.47,5.01-6.21v-6.26h-12.18
c-7.29,0-10.93,2.39-10.93,7.17c0,2.28,0.89,4.08,2.68,5.41c1.78,1.33,4.23,1.99,7.34,1.99
C154.98,475.99,157.67,475.29,160.03,473.89z"/>
<path class="st1" d="M250.6,428.8c4.67,2.62,8.33,6.3,10.99,11.04c2.66,4.75,3.99,10.27,3.99,16.57s-1.33,11.82-3.99,16.57
c-2.66,4.75-6.32,8.43-10.99,11.04s-9.85,3.93-15.54,3.93c-7.82,0-13.97-2.47-18.45-7.4v28.58h-17.76v-83.35h16.97v7.06
c4.4-5.31,10.82-7.97,19.24-7.97C240.76,424.87,245.94,426.18,250.6,428.8z M243.2,468.76c2.92-3.07,4.38-7.19,4.38-12.35
s-1.46-9.28-4.38-12.35c-2.92-3.07-6.66-4.61-11.22-4.61s-8.29,1.54-11.22,4.61c-2.92,3.07-4.38,7.19-4.38,12.35
s1.46,9.28,4.38,12.35c2.92,3.07,6.66,4.61,11.22,4.61S240.28,471.84,243.2,468.76z"/>
<path class="st1" d="M283.11,486.07c-4.86-1.25-8.73-2.83-11.61-4.73l5.92-12.75c2.73,1.75,6.03,3.17,9.91,4.27
c3.87,1.1,7.67,1.65,11.39,1.65c7.51,0,11.27-1.86,11.27-5.58c0-1.75-1.03-3-3.07-3.76c-2.05-0.76-5.2-1.4-9.45-1.94
c-5.01-0.76-9.15-1.63-12.41-2.62c-3.26-0.99-6.09-2.73-8.48-5.24s-3.59-6.07-3.59-10.7c0-3.87,1.12-7.3,3.36-10.3
c2.24-3,5.5-5.33,9.79-7c4.29-1.67,9.35-2.5,15.2-2.5c4.33,0,8.63,0.48,12.92,1.42c4.29,0.95,7.84,2.26,10.65,3.93l-5.92,12.64
c-5.39-3.04-11.27-4.55-17.65-4.55c-3.8,0-6.64,0.53-8.54,1.59c-1.9,1.06-2.85,2.43-2.85,4.1c0,1.9,1.02,3.23,3.07,3.99
c2.05,0.76,5.31,1.48,9.79,2.16c5.01,0.84,9.11,1.73,12.3,2.68c3.19,0.95,5.96,2.68,8.31,5.18c2.35,2.5,3.53,6,3.53,10.48
c0,3.8-1.14,7.17-3.42,10.13c-2.28,2.96-5.6,5.26-9.96,6.89c-4.37,1.63-9.55,2.45-15.54,2.45
C292.94,487.95,287.97,487.32,283.11,486.07z"/>
<path class="st1" d="M399.59,425.78v61.26h-16.85v-7.29c-2.35,2.66-5.16,4.69-8.43,6.09c-3.26,1.4-6.79,2.11-10.59,2.11
c-8.05,0-14.42-2.31-19.13-6.95c-4.71-4.63-7.06-11.5-7.06-20.61v-34.61h17.76v32c0,9.87,4.14,14.8,12.41,14.8
c4.25,0,7.67-1.38,10.25-4.16c2.58-2.77,3.87-6.89,3.87-12.35v-30.29H399.59z"/>
<path class="st1" d="M416.1,402.55h17.76v84.49H416.1V402.55z"/>
<path class="st1" d="M510.04,461.42H463.7c0.83,3.8,2.81,6.79,5.92,9c3.11,2.2,6.98,3.3,11.61,3.3c3.19,0,6.01-0.47,8.48-1.42
c2.47-0.95,4.76-2.45,6.89-4.5l9.45,10.25c-5.77,6.6-14.2,9.91-25.28,9.91c-6.91,0-13.02-1.35-18.33-4.04
c-5.31-2.69-9.41-6.43-12.3-11.22c-2.89-4.78-4.33-10.21-4.33-16.28c0-6,1.42-11.4,4.27-16.23c2.85-4.82,6.76-8.58,11.73-11.27
c4.97-2.69,10.53-4.04,16.68-4.04c6,0,11.42,1.29,16.28,3.87c4.86,2.58,8.67,6.28,11.44,11.1c2.77,4.82,4.16,10.42,4.16,16.79
C510.38,456.86,510.27,458.46,510.04,461.42z M468.48,441.72c-2.73,2.28-4.4,5.39-5.01,9.34h30.17c-0.61-3.87-2.28-6.96-5.01-9.28
c-2.73-2.31-6.07-3.47-10.02-3.47C474.59,438.3,471.21,439.44,468.48,441.72z"/>
</g>
<g>
<g>
<path class="st2" d="M144.97,316.25c2.88-4.14,5.7-8.31,8.68-12.38c0.84-1.14,2.13-1.94,3.22-2.9c8.67,2.77,17.24,5.98,26.06,8.18
c7.28,1.81,7.49,1.33,11.08-5.55c9.52-18.28,18.99-36.58,28.42-54.91c3.55-6.9,7.04-13.85,10.34-20.87c1.87-3.99,1-5.28-3.27-5.1
c-5.07,0.21-10.13,0.68-15.19,1.04c1.72-2.35,3.24-4.87,5.2-7.01c4.47-4.88,9.14-9.57,13.74-14.34c1.84-0.03,3.68,0.02,5.52-0.1
c14.62-1.03,29.24-2.1,43.86-3.16c-0.08,0.84-0.24,1.68-0.24,2.52c0.01,48.41,0.03,96.83,0.05,145.24
c-15.73,0.85-30.48,0.97-47.48-0.65c-16.01-1.04-30.66-3.54-46.6-5.49c-13.64-1.67-26.85-5.2-39.21-11.4
c-4.77-2.4-5.86-5.41-4.24-10.45C145.16,318.1,144.96,317.14,144.97,316.25z"/>
<path class="st3" d="M282.42,346.9c-0.02-48.41-0.04-96.83-0.05-145.24c0-0.84,0.05-1.64,0.04-2.48
c5.63,0.1,11.47-0.06,17.08,0.32c11.35,0.78,22.67,1.83,34.01,2.77c2.69,3.09,5.47,6.1,8.05,9.28c3.38,4.17,6.61,8.47,9.9,12.71
c-6.04-0.52-12.07-1.2-18.13-1.49c-4.12-0.2-4.91,1.24-3.08,4.81c9.87,19.27,19.73,38.54,29.65,57.78
c4.02,7.79,8.22,15.49,12.24,23.29c1.46,2.83,3.6,3.9,6.61,3.17c11.52-2.81,23.03-5.68,34.54-8.52c1.8,3.04,3.52,6.13,5.42,9.1
c0.89,1.39,2.13,2.56,3.21,3.83c0,0.56-0.19,1.22,0.04,1.66c3.28,6.31-0.16,9.95-5.82,12.53c-14.18,6.44-29.11,9.85-44.52,11.41
c-12.89,1.31-25.79,2.51-38.68,3.77c-6.24,0.61-12.47,1.45-18.72,1.79c-4.58,0.24-9.2-0.17-13.81-0.3
c-5.95-0.04-11.9-0.08-17.85-0.12L282.42,346.9z"/>
<path class="st2" d="M413.28,303.3c-11.51,2.84-23.02,5.71-34.54,8.52c-3.01,0.74-5.15-0.34-6.61-3.17
c-4.02-7.79-8.22-15.49-12.24-23.29c-9.92-19.24-19.79-38.51-29.65-57.78c-1.83-3.57-1.04-5.01,3.08-4.81
c6.05,0.29,12.09,0.97,18.13,1.49c1.89,0.4,2.54,0.15,5.06,3.74c17.1,24.41,37.01,47.73,54.85,71.62
C412.17,300.72,412.64,302.07,413.28,303.3z"/>
<path class="st3" d="M155.06,302.38c11.51,2.84,22.26,5.47,33.78,8.28c3.01,0.74,5.15-0.34,6.61-3.17
c4.02-7.79,8.22-15.49,12.24-23.29c9.92-19.24,17.3-37.26,26.37-56.7c1.83-3.57,0.68-4.95-3.44-4.75
c-6.05,0.29-10.08,0.42-16.13,0.94c-2.11,1.25-2.46,1.66-3.84,3.47c-18.01,23.75-35.83,47.64-53.67,71.53
C156.18,299.79,155.7,301.14,155.06,302.38z"/>
<path class="st0" d="M421.92,316.24c0,0.56-0.19,1.22,0.04,1.66c3.28,6.31-0.16,9.95-5.82,12.53
c-14.18,6.44-29.11,9.85-44.52,11.41c-12.89,1.31-25.79,2.51-38.68,3.77c-6.24,0.61-12.94,1.22-18.94,1.29
c-4.59,0.05-8.98,0.32-13.59,0.2c-5.95-0.04-11.9-0.08-17.85-0.12c0,0-0.12-0.08-0.12-0.08c-15.36,0.35-28.73,0.35-46.17-1.19
c-15.98-1.41-31.97-2.99-47.91-4.95c-13.64-1.67-26.85-5.2-39.21-11.4c-4.77-2.4-5.86-5.41-4.24-10.45
c0.26-0.81,0.06-1.77,0.07-2.66c-6.55,2.47-11.33,6.45-12.86,13.75c-1.74,8.28,0.69,15.31,5.77,21.67
c1.43,1.79,2.4,3.22,0.07,5.22c-0.71,0.61-0.81,3.27-0.15,3.89c6.36,6.04,13.89,10.11,22.37,12.36c2.35,0.62,4.12,0.02,4.62-2.85
c0.11-0.64,1.63-1.63,2.27-1.49c8.66,1.96,17.26,4.13,25.91,6.14c1.98,0.46,2.73,1,1.52,3.01c-1.45,2.4-0.41,3.92,2,4.93
c8.64,3.63,17.82,3.98,26.97,4.34c2.18,0.08,4.54-0.9,3.51-3.88c-1.11-3.22,0.45-3.2,2.83-2.99c8.57,0.73,17.14,1.44,25.72,1.95
c3.13,0.19,3.98,1.04,2.41,3.98c-1.6,2.98-0.26,4.76,2.9,4.77c14.82,0.08,29.65,0.17,44.46-0.08c4.59-0.08,5.1-1.29,3.36-5.63
c-0.84-2.1-0.97-2.87,1.76-3.02c9.16-0.52,18.32-1.21,27.45-2.12c2.5-0.25,3.06,0.34,2.55,2.56c-0.53,2.31,0.05,4.05,2.72,4.11
c9.52,0.21,18.91-0.53,27.82-4.34c1.95-0.83,3.09-2.06,1.71-4.23c-1.72-2.71-0.09-3.15,2.17-3.67c8.24-1.87,16.46-3.83,24.64-5.93
c1.82-0.47,3-0.77,3.21,1.6c0.26,2.99,2.1,3.32,4.53,2.61c8.11-2.36,15.55-5.98,21.6-11.99c0.69-0.69,1.03-2.99,0.55-3.39
c-3.18-2.71-1.41-4.64,0.51-6.95C437.87,340.92,439.33,322.67,421.92,316.24z"/>
</g>
</g>
<path class="st3" d="M324.35,192.94c-6.72-0.27-13.4-0.35-20.23-0.52c-7.13-0.17-18.9-0.51-18.9-0.51s-1.27,0.04-2.44,0
c0,0-0.63-0.01-0.63,0.18c-0.01-5.67,0.01-11.83,0-17.5c12.58,0.95,24.65,1.94,37.19,2.72c1.5,0.09,3.29-0.07,4.8-0.12
C324.19,182.43,324.33,187.69,324.35,192.94z"/>
<path class="st2" d="M243.35,193.45c6.72-0.27,10.02-0.35,16.86-0.52c7.13-0.17,18.9-0.51,18.9-0.51s1.27,0.04,2.44,0
c0,0,0.63-0.53,0.63-0.34c0.01-5.67-0.01-11.83,0-17.5c-12.58,0.95-21.28,1.94-33.82,2.72c-1.5,0.09-3.29-0.07-4.8-0.12
C243.51,182.43,243.38,188.21,243.35,193.45z"/>
<path class="st0" d="M327.57,193.15c-1.31-0.1-2.62-0.17-3.93-0.26c-13.33-0.32-26.66-0.63-39.99-0.95v0c-0.03,0-0.06,0-0.1,0
c-0.03,0-0.06,0-0.1,0v0c-13.33,0.32-26.66,0.63-39.99,0.95c-1.31,0.08-2.62,0.15-3.93,0.26c-6.26,0.5-6.88,1.16-6.73,7.17
c0.02,0.7,0.18,1.39,0.27,2.09c1.91-0.03,3.82,0.02,5.72-0.1c14.92-1.02,28.65-2.07,43.57-3.11c14.92,1.04,31.01,2.1,45.93,3.11
c1.9,0.13,3.81,0.07,5.72,0.1c0.09-0.7,0.25-1.39,0.27-2.09C334.45,194.31,333.82,193.65,327.57,193.15z"/>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
assets/logo/capsule_raw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,107 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 504.123 504.123" style="enable-background:new 0 0 504.123 504.123;" xml:space="preserve">
<path style="fill:#2477AA;" d="M265.665,468.582c0.378-1.276,0.646-2.615,0.646-4.033v-63.827c0-7.483-5.805-13.564-13.162-14.131
l0,0c-0.354-0.039-0.709-0.118-1.087-0.11c-7.869,0-14.249,6.372-14.249,14.241v63.835c0,1.41,0.268,2.749,0.646,4.025
c-4.112,1.772-7.026,5.868-7.026,10.65v3.812v3.419c0,6.396,5.175,11.587,11.579,11.587h18.093c6.396,0,11.571-5.183,11.571-11.587
v-7.231C272.675,474.451,269.777,470.355,265.665,468.582z"/>
<ellipse style="fill:#7BC6C6;" cx="252.062" cy="85.851" rx="7.404" ry="67.245"/>
<circle style="fill:#FF7F00;" cx="252.062" cy="269.722" r="183.863"/>
<path style="fill:#FF5B00;" d="M252.062,85.858c101.541,0,183.863,82.322,183.863,183.863c0,101.557-82.322,183.879-183.863,183.879
"/>
<path style="fill:#25618E;" d="M110.222,386.733h283.672c25.647-31.051,41.173-70.719,41.874-113.971H68.348
C69.065,316.014,84.582,355.675,110.222,386.733z"/>
<g>
<path style="fill:#18456D;" d="M252.062,272.762v113.971h141.832c0-0.008,0.024-0.024,0.031-0.039
c0.055-0.063,0.095-0.142,0.165-0.197c3.411-4.151,6.609-8.476,9.657-12.926c1.166-1.686,2.198-3.45,3.277-5.167
c1.827-2.851,3.631-5.742,5.309-8.704c1.26-2.229,2.41-4.537,3.584-6.829c1.276-2.505,2.489-5.049,3.671-7.625
c1.197-2.67,2.355-5.38,3.426-8.113c0.874-2.221,1.678-4.474,2.465-6.735c1.087-3.119,2.15-6.246,3.072-9.429
c0.512-1.757,0.922-3.545,1.378-5.325c3.497-13.674,5.585-27.932,5.837-42.646c0-0.079,0-0.15,0.016-0.221H252.062V272.762z"/>
<path style="fill:#18456D;" d="M152.174,298.977c0,1.883-1.528,3.426-3.419,3.426h-28.499c-1.883,0-3.419-1.536-3.419-3.426l0,0
c0-1.883,1.528-3.426,3.419-3.426h28.499C150.646,295.55,152.174,297.094,152.174,298.977L152.174,298.977z"/>
<path style="fill:#18456D;" d="M152.174,318.354c0,1.883-1.528,3.419-3.419,3.419h-28.499c-1.883,0-3.419-1.528-3.419-3.419l0,0
c0-1.89,1.528-3.426,3.419-3.426h28.499C150.646,314.927,152.174,316.463,152.174,318.354L152.174,318.354z"/>
<path style="fill:#18456D;" d="M152.174,337.723c0,1.89-1.528,3.426-3.419,3.426h-28.499c-1.883,0-3.419-1.528-3.419-3.426l0,0
c0-1.883,1.528-3.419,3.419-3.419h28.499C150.646,334.305,152.174,335.841,152.174,337.723L152.174,337.723z"/>
</g>
<path style="fill:#25618E;" d="M380.109,352.54c0,10.075-8.153,18.235-18.219,18.235h-67.253c-10.059,0-18.219-8.16-18.219-18.235
v-45.584c0-10.067,8.16-18.227,18.219-18.227h67.253c10.067,0,18.219,8.16,18.219,18.227V352.54z"/>
<g>
<path style="fill:#3479A3;" d="M367.577,347.034c0,7.633-6.183,13.832-13.824,13.832h-50.987c-7.641,0-13.832-6.199-13.832-13.832
v-34.572c0-7.633,6.191-13.824,13.832-13.824h50.987c7.641,0,13.824,6.191,13.824,13.824V347.034z"/>
<path style="fill:#3479A3;" d="M289.666,81.865c0,7.239-5.868,13.107-13.107,13.107h-49.01c-7.231,0-13.099-5.868-13.099-13.107
l0,0c0-7.239,5.868-13.107,13.099-13.107h49.01C283.798,68.758,289.666,74.626,289.666,81.865L289.666,81.865z"/>
</g>
<path style="fill:#18456D;" d="M276.559,68.758c7.239,0,13.107,5.868,13.107,13.107l0,0c0,7.239-5.868,13.107-13.107,13.107h-49.01
c-7.231,0-13.099-5.868-13.099-13.107l0,0"/>
<circle style="fill:#B4E7ED;" cx="252.062" cy="152.718" r="14.438"/>
<path style="fill:#7BC6C6;" d="M252.062,138.279c7.979,0,14.438,6.467,14.438,14.438s-6.459,14.438-14.438,14.438"/>
<circle style="fill:#B4E7ED;" cx="252.062" cy="198.309" r="14.438"/>
<path style="fill:#7BC6C6;" d="M252.062,183.871c7.979,0,14.438,6.459,14.438,14.438c0,7.971-6.459,14.438-14.438,14.438"/>
<circle style="fill:#B4E7ED;" cx="252.069" cy="14.438" r="14.438"/>
<path style="fill:#7BC6C6;" d="M262.262,4.23c5.648,5.64,5.648,14.785,0.016,20.417c-5.64,5.632-14.785,5.632-20.417,0"/>
<circle style="fill:#B4E7ED;" cx="252.062" cy="243.893" r="14.438"/>
<g>
<path style="fill:#7BC6C6;" d="M252.062,229.455c7.979,0,14.438,6.467,14.438,14.438s-6.459,14.438-14.438,14.438"/>
<path style="fill:#7BC6C6;" d="M353.319,332.312c0,2.056-1.646,3.71-3.694,3.71h-13.107c-2.048,0-3.71-1.654-3.71-3.71l0,0
c0-2.048,1.662-3.702,3.71-3.702h13.107C351.673,328.609,353.319,330.264,353.319,332.312L353.319,332.312z"/>
</g>
<path style="fill:#FF5B00;" d="M185.194,440.95c-0.457-18.692-14.612-33.705-32.106-33.705c-6.231,0-12.012,2.001-16.951,5.309
C150.772,424.432,167.329,433.995,185.194,440.95z"/>
<path style="fill:#7BC6C6;" d="M161.225,412.782c5.561,5.569,5.561,14.588,0,20.157l-45.127,45.127
c-5.569,5.569-14.588,5.569-20.149,0l0,0c-5.561-5.553-5.561-14.58,0-20.149l45.135-45.127
C146.637,407.221,155.656,407.221,161.225,412.782L161.225,412.782z"/>
<path style="fill:#8DD8D6;" d="M141.084,412.782l-45.135,45.127c-1,1.008-1.78,2.143-2.41,3.34c0.228,0.276,0.433,0.583,0.693,0.851
c5.585,5.569,14.588,5.569,20.157,0l45.127-45.127c1.016-1.008,1.764-2.143,2.41-3.34c-0.236-0.284-0.433-0.583-0.701-0.851
C155.656,407.221,146.637,407.221,141.084,412.782z"/>
<path style="fill:#25618E;" d="M130.859,485.888c0,10.067-8.153,18.235-18.227,18.235H84.141c-10.059,0-18.235-8.168-18.235-18.235
v-11.39c0-10.075,8.176-18.235,18.235-18.235h28.491c10.075,0,18.227,8.16,18.227,18.235V485.888z"/>
<path style="fill:#2477AA;" d="M117.752,457.074c-1.638-0.488-3.332-0.819-5.12-0.819H84.141c-10.059,0-18.235,8.16-18.235,18.235
v6.018c1.638,0.48,3.332,0.819,5.128,0.819h28.491c10.075,0,18.227-8.16,18.227-18.227V457.074z"/>
<path style="fill:#FF7F00;" d="M318.921,440.95c0.465-18.692,14.62-33.705,32.114-33.705c6.223,0,12.012,2.001,16.951,5.309
C353.351,424.432,336.786,433.995,318.921,440.95z"/>
<path style="fill:#7BC6C6;" d="M342.898,412.782c-5.561,5.569-5.561,14.588,0,20.157l45.127,45.127
c5.577,5.569,14.588,5.569,20.157,0l0,0c5.561-5.553,5.561-14.58,0-20.149L363.04,412.79
C357.486,407.221,348.459,407.221,342.898,412.782L342.898,412.782z"/>
<path style="fill:#8DD8D6;" d="M363.032,412.782l45.143,45.127c1,1.008,1.772,2.143,2.41,3.34c-0.228,0.276-0.433,0.583-0.693,0.851
c-5.577,5.569-14.588,5.569-20.165,0L344.6,416.973c-1.016-1.008-1.764-2.143-2.41-3.34c0.236-0.284,0.433-0.583,0.701-0.851
C348.459,407.221,357.486,407.221,363.032,412.782z"/>
<path style="fill:#25618E;" d="M373.264,485.888c0,10.067,8.153,18.235,18.227,18.235h28.491c10.059,0,18.235-8.168,18.235-18.235
v-11.39c0-10.075-8.176-18.235-18.235-18.235h-28.491c-10.075,0-18.227,8.16-18.227,18.235V485.888z"/>
<path style="fill:#2477AA;" d="M386.371,457.074c1.638-0.488,3.332-0.819,5.12-0.819h28.491c10.059,0,18.235,8.16,18.235,18.235
v6.018c-1.638,0.48-3.332,0.819-5.128,0.819h-28.491c-10.075,0-18.227-8.16-18.227-18.227V457.074z"/>
<path style="fill:#B4E7ED;" d="M72.428,230.747c1.788,0.591,3.679,0.985,5.671,0.985h98.013c10.067,0,18.219-8.168,18.219-18.235
V95.232C133.152,115.468,86.221,166.896,72.428,230.747z"/>
<path style="fill:#7BC6C6;" d="M78.1,231.731h98.013c10.067,0,18.219-8.168,18.219-18.235V95.232"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

28
charts/capsule/Chart.yaml Normal file
View File

@@ -0,0 +1,28 @@
apiVersion: v2
type: application
description: A Helm chart to deploy the Capsule Operator for easily implementing,
managing, and maintaining mutitenancy and access control in Kubernetes.
home: https://github.com/clastix/capsule
icon: https://github.com/clastix/capsule/raw/master/assets/logo/capsule_small.png
keywords:
- kubernetes
- operator
- multi-tenancy
- multi-tenant
- multitenancy
- multitenant
- namespace
maintainers:
- email: hello@clastix.io
name: Clastix Labs Team
name: capsule
sources:
- https://github.com/clastix/capsule
# 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
# 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

119
charts/capsule/README.md Normal file
View File

@@ -0,0 +1,119 @@
# Deploying the Capsule Operator
Use the Capsule Operator for easily implementing, managing, and maintaining mutitenancy and access control in Kubernetes.
## Requirements
* [Helm 3](https://github.com/helm/helm/releases) is required when installing the Capsule Operator chart. Follow Helms official [steps](https://helm.sh/docs/intro/install/) for installing helm on your particular operating system.
* A Kubernetes cluster 1.16+ with following [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled:
* PodNodeSelector
* LimitRanger
* ResourceQuota
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
* A [`kubeconfig`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file accessing the Kubernetes cluster with cluster admin permissions.
## Quick Start
The Capsule Operator Chart can be used to instantly deploy the Capsule Operator on your Kubernetes cluster.
1. Add this repository:
$ helm repo add clastix https://clastix.github.io/charts
2. Install the Chart:
$ helm install capsule clastix/capsule -n capsule-system
3. Show the status:
$ helm status capsule -n capsule-system
4. Upgrade the Chart
$ helm upgrade capsule clastix/capsule -n capsule-system
5. Uninstall the Chart
$ helm uninstall capsule -n capsule-system
## Customize the installation
There are two methods for specifying overrides of values during chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the chart:
$ helm install capsule capsule-helm-chart --values myvalues.yaml -n capsule-system
The values in your overrides file `myvalues.yaml` will override their counterparts in the charts values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
$ helm install capsule capsule-helm-chart --set force_tenant_prefix=false -n capsule-system
Here the values you can override:
Parameter | Description | Default
--- | --- | ---
`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.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`
`manager.livenessProbe` | Configure the liveness probe using Deployment probe spec | `GET :10080/healthz`
`manager.readinessProbe` | Configure the readiness probe using Deployment probe spec | `GET :10080/readyz`
`manager.resources.requests/cpu` | Set the CPU requests assigned to the controller. | `200m`
`manager.resources.requests/memory` | Set the memory requests assigned to the controller. | `128Mi`
`manager.resources.limits/cpu` | Set the CPU limits assigned to the controller. | `200m`
`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`
`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. | `{}`
`serviceAccount.name` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template | `capsule`
`podAnnotations` | Annotations to add to the Capsule pod. | `{}`
`priorityClassName` | Set the priority class name of the Capsule pod. | `null`
`nodeSelector` | Set the node selector for the Capsule pod. | `{}`
`tolerations` | Set list of tolerations for the Capsule pod. | `[]`
`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`
## Created resources
This Helm Chart cretes the following Kubernetes resources in the release namespace:
* Capsule Namespace
* Capsule Operator Deployment
* Capsule Service
* CA Secret
* Certfificate Secret
* Tenant Custom Resource Definition
* MutatingWebHookConfiguration
* ValidatingWebHookConfiguration
* RBAC Cluster Roles
* Metrics Service
And optionally, depending on the values set:
* Capsule ServiceAccount
* PodSecurityPolicy
* RBAC ClusterRole and RoleBinding for pod security policy
## Notes on installing Custom Resource Definitions with Helm3
Capsule, as many other add-ons, defines its own set of Custom Resource Definitions (CRDs). Helm3 removed the old CRDs installation method for a more simple methodology. In the Helm Chart, there is now a special directory called `crds` to hold the CRDs. These CRDs are not templated, but will be installed by default when running a `helm install` for the chart. If the CRDs already exist (for example, you already executed `helm install`), it will be skipped with a warning. When you wish to skip the CRDs installation, and do not see the warning, you can pass the `--skip-crds` flag to the `helm install` command.
## More
See Capsule [use cases](https://github.com/clastix/capsule/blob/master/use_cases.md) for more information about how to use Capsule.

View File

@@ -0,0 +1,62 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
creationTimestamp: null
name: capsuleconfigurations.capsule.clastix.io
spec:
group: capsule.clastix.io
names:
kind: CapsuleConfiguration
listKind: CapsuleConfigurationList
plural: capsuleconfigurations
singular: capsuleconfiguration
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: CapsuleConfiguration is the Schema for the Capsule configuration API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: 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:
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:
description: Disallow creation of namespaces, whose name matches this regexp
type: string
userGroups:
default:
- capsule.clastix.io
description: Names of the groups for Capsule users.
items:
type: string
type: array
type: object
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

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

View File

@@ -0,0 +1,19 @@
- Capsule Operator Helm Chart deployed:
# Check the capsule logs
$ kubectl logs -f deployment/{{ template "capsule.fullname" . }}-controller-manager -c manager -n {{ .Release.Namespace }}
# Check the capsule logs
$ kubectl logs -f deployment/{{ template "capsule.fullname" . }}-controller-manager -c manager -n{{ .Release.Namespace }}
- Manage this chart:
# Upgrade Capsule
$ helm upgrade {{ .Release.Name }} -f <values.yaml> capsule -n {{ .Release.Namespace }}
# Show this status again
$ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
# Uninstall Capsule
$ helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }}

View File

@@ -0,0 +1,104 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "capsule.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "capsule.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "capsule.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "capsule.labels" -}}
helm.sh/chart: {{ include "capsule.chart" . }}
{{ include "capsule.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "capsule.selectorLabels" -}}
app.kubernetes.io/name: {{ include "capsule.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "capsule.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "capsule.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Create the manager fully-qualified Docker image to use
*/}}
{{- define "capsule.managerFullyQualifiedDockerImage" -}}
{{- printf "%s:%s" .Values.manager.image.repository ( .Values.manager.image.tag | default (printf "v%s" .Chart.AppVersion) ) -}}
{{- end }}
{{/*
Create the proxy fully-qualified Docker image to use
*/}}
{{- define "capsule.proxyFullyQualifiedDockerImage" -}}
{{- printf "%s:%s" .Values.proxy.image.repository .Values.proxy.image.tag -}}
{{- end }}
{{/*
Create the jobs fully-qualified Docker image to use
*/}}
{{- define "capsule.jobsFullyQualifiedDockerImage" -}}
{{- printf "%s:%s" .Values.jobs.image.repository .Values.jobs.image.tag -}}
{{- end }}
{{/*
Create the Capsule Deployment name to use
*/}}
{{- define "capsule.deploymentName" -}}
{{- printf "%s-controller-manager" (include "capsule.fullname" .) -}}
{{- end }}
{{/*
Create the Capsule CA Secret name to use
*/}}
{{- define "capsule.secretCaName" -}}
{{- printf "%s-ca" (include "capsule.fullname" .) -}}
{{- end }}
{{/*
Create the Capsule TLS Secret name to use
*/}}
{{- define "capsule.secretTlsName" -}}
{{- printf "%s-tls" (include "capsule.fullname" .) -}}
{{- end }}

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
name: {{ include "capsule.secretCaName" . }}
data:

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
name: {{ include "capsule.secretTlsName" . }}
data:

View File

@@ -0,0 +1,13 @@
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
forceTenantPrefix: {{ .Values.manager.options.forceTenantPrefix }}
userGroups:
{{- range .Values.manager.options.capsuleUserGroups }}
- {{ . }}
{{- end}}
protectedNamespaceRegex: {{ .Values.manager.options.protectedNamespaceRegex | quote }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}

View File

@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "capsule.deploymentName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "capsule.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "capsule.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
priorityClassName: {{ .Values.priorityClassName }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: cert
secret:
defaultMode: 420
secretName: {{ include "capsule.fullname" . }}-tls
containers:
- name: manager
command:
- /manager
args:
- --enable-leader-election
- --zap-log-level={{ default 4 .Values.manager.options.logLevel }}
- --configuration-name=default
image: {{ include "capsule.managerFullyQualifiedDockerImage" . }}
imagePullPolicy: {{ .Values.manager.image.pullPolicy }}
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: webhook-server
containerPort: 9443
protocol: TCP
- name: metrics
containerPort: 8080
protocol: TCP
livenessProbe:
{{- toYaml .Values.manager.livenessProbe | nindent 12}}
readinessProbe:
{{- toYaml .Values.manager.readinessProbe | nindent 12}}
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert
readOnly: true
resources:
{{- toYaml .Values.manager.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "capsule.fullname" . }}-controller-manager-metrics-service
labels:
{{- include "capsule.labels" . | nindent 4 }}
spec:
ports:
- port: 8080
name: metrics
protocol: TCP
targetPort: 8080
selector:
{{- include "capsule.selectorLabels" . | nindent 4 }}
sessionAffinity: None
type: ClusterIP

View File

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

View File

@@ -0,0 +1,54 @@
{{- if .Values.podSecurityPolicy.enabled }}
kind: PodSecurityPolicy
apiVersion: policy/v1beta1
metadata:
name: {{ include "capsule.fullname" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
spec:
fsGroup:
rule: RunAsAny
hostPorts:
- max: 0
min: 0
runAsUser:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- secret
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-use-psp
labels:
{{- include "capsule.labels" . | nindent 4 }}
rules:
- apiGroups:
- extensions
resources:
- podsecuritypolicies
resourceNames:
- {{ include "capsule.fullname" . }}
verbs:
- use
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-use-psp
labels:
{{- include "capsule.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "capsule.fullname" . }}-use-psp
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "capsule.serviceAccountName" . }}
{{- end }}

View File

@@ -0,0 +1,39 @@
{{- $cmd := "while [ -z $$(kubectl -n $NAMESPACE get secret capsule-tls -o jsonpath='{.data.tls\\\\.crt}') ];" -}}
{{- $cmd = printf "%s do echo 'waiting Capsule to be up and running...' && sleep 5;" $cmd -}}
{{- $cmd = printf "%s done" $cmd -}}
apiVersion: batch/v1
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 }}"
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
spec:
template:
metadata:
name: "{{ .Release.Name }}"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
restartPolicy: Never
containers:
- name: post-install-job
image: {{ include "capsule.jobsFullyQualifiedDockerImage" . }}
imagePullPolicy: {{ .Values.jobs.image.pullPolicy }}
command: ["sh", "-c", "{{ $cmd }}"]
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
serviceAccountName: {{ include "capsule.serviceAccountName" . }}

View File

@@ -0,0 +1,40 @@
{{- $cmd := printf "kubectl scale deployment -n $NAMESPACE %s --replicas 0 &&" (include "capsule.deploymentName" .) -}}
{{- $cmd = printf "%s kubectl delete secret -n $NAMESPACE %s %s --ignore-not-found &&" $cmd (include "capsule.secretTlsName" .) (include "capsule.secretCaName" .) -}}
{{- $cmd = printf "%s kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found &&" $cmd -}}
{{- $cmd = printf "%s kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found" $cmd -}}
apiVersion: batch/v1
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 }}"
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
spec:
template:
metadata:
name: "{{ .Release.Name }}"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
restartPolicy: Never
containers:
- name: pre-delete-job
image: {{ include "capsule.jobsFullyQualifiedDockerImage" . }}
imagePullPolicy: {{ .Values.jobs.image.pullPolicy }}
command: [ "sh", "-c", "{{ $cmd }}"]
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
serviceAccountName: {{ include "capsule.serviceAccountName" . }}

View File

@@ -0,0 +1,61 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-proxy-role
labels:
{{- include "capsule.labels" . | nindent 4 }}
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-metrics-reader
labels:
{{- include "capsule.labels" . | nindent 4 }}
rules:
- nonResourceURLs:
- /metrics
verbs:
- get
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-proxy-rolebinding
labels:
{{- include "capsule.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "capsule.fullname" . }}-proxy-role
subjects:
- kind: ServiceAccount
name: {{ include "capsule.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "capsule.fullname" . }}-manager-rolebinding
labels:
{{- include "capsule.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: {{ include "capsule.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "capsule.serviceAccountName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,292 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
webhooks:
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-ingress
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: ingress-v1beta1.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-ingress
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: ingress-v1.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- ingresses
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validate-v1-namespace-quota
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: quota.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-network-policy
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: validating.network-policy.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- networkpolicies
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: system
path: /validating-v1-podpriority
failurePolicy: Ignore
name: podpriority.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-pvc
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: pvc.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- persistentvolumeclaims
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-tenant
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: tenant.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- tenants
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-namespace-tenant-prefix
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: prefix.namespace.capsule.clastix.io
namespaceSelector: {}
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-v1-registry
port: 443
failurePolicy: Ignore
matchPolicy: Exact
name: pod.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: {{ include "capsule.fullname" . }}-webhook-service
namespace: {{ .Release.Namespace }}
path: /validating-external-service-ips
port: 443
failurePolicy: Fail
matchPolicy: Exact
name: validating-external-service-ips.capsule.clastix.io
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
objectSelector: {}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
scope: '*'
sideEffects: NoneOnDryRun
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "capsule.fullname" . }}-webhook-service
labels:
{{- include "capsule.labels" . | nindent 4 }}
spec:
ports:
- port: 443
name: https
protocol: TCP
targetPort: 9443
selector:
{{- include "capsule.selectorLabels" . | nindent 4 }}
sessionAffinity: None
type: ClusterIP

View File

@@ -0,0 +1,58 @@
# Default values for capsule.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
manager:
image:
repository: quay.io/clastix/capsule
pullPolicy: IfNotPresent
tag: ''
# Additional Capsule options
options:
logLevel: '4'
forceTenantPrefix: false
capsuleUserGroups: ["capsule.clastix.io"]
protectedNamespaceRegex: ""
allowIngressHostnameCollision: true
allowTenantIngressHostnamesCollision: false
livenessProbe:
httpGet:
path: /healthz
port: 10080
readinessProbe:
httpGet:
path: /readyz
port: 10080
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 200m
memory: 128Mi
jobs:
image:
repository: bitnami/kubectl
pullPolicy: IfNotPresent
tag: "1.18"
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30
imagePullSecrets: []
serviceAccount:
create: true
annotations: {}
name: "capsule"
podAnnotations: {}
priorityClassName: '' #system-cluster-critical
nodeSelector: {}
# node-role.kubernetes.io/master: ""
tolerations: []
#- key: CriticalAddonsOnly
# operator: Exists
#- effect: NoSchedule
# key: node-role.kubernetes.io/master
replicaCount: 1
affinity: {}
podSecurityPolicy:
enabled: false

View File

@@ -0,0 +1,65 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
creationTimestamp: null
name: capsuleconfigurations.capsule.clastix.io
spec:
group: capsule.clastix.io
names:
kind: CapsuleConfiguration
listKind: CapsuleConfigurationList
plural: capsuleconfigurations
singular: capsuleconfiguration
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: CapsuleConfiguration is the Schema for the Capsule configuration API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: CapsuleConfigurationSpec defines the Capsule configuration nolint:maligned
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:
description: Disallow creation of namespaces, whose name matches this regexp
type: string
userGroups:
default:
- capsule.clastix.io
description: Names of the groups for Capsule users.
items:
type: string
type: array
type: object
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
# It should be run by config/default
resources:
- bases/capsule.clastix.io_tenants.yaml
- bases/capsule.clastix.io_capsuleconfigurations.yaml
# +kubebuilder:scaffold:crdkustomizeresource
# the following config is for teaching kustomize how to do kustomization for CRDs.

View File

@@ -22,8 +22,4 @@ bases:
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
- manager_webhook_patch.yaml

View File

@@ -1,25 +0,0 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=10"
ports:
- containerPort: 8443
name: https
- name: manager
args:
- "--metrics-addr=127.0.0.1:8080"
- "--enable-leader-election"

View File

@@ -12,6 +12,9 @@ spec:
- containerPort: 9443
name: webhook-server
protocol: TCP
- containerPort: 8080
name: metrics
protocol: TCP
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert

View File

@@ -0,0 +1,10 @@
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
userGroups: ["capsule.clastix.io"]
forceTenantPrefix: false
protectedNamespaceRegex: ""
allowTenantIngressHostnamesCollision: false
allowIngressHostnameCollision: false

View File

@@ -1,7 +1,10 @@
resources:
- configuration.yaml
- manager.yaml
- metrics_service.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: quay.io/clastix/capsule
newTag: 0.0.1
- name: controller
newName: quay.io/clastix/capsule
newTag: v0.0.5

View File

@@ -29,12 +29,13 @@ spec:
- --enable-leader-election
- --zap-encoder=console
- --zap-log-level=debug
- --configuration-name=capsule-default
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/clastix/capsule:latest
image: controller
imagePullPolicy: IfNotPresent
name: manager
resources:

View File

@@ -7,8 +7,8 @@ metadata:
namespace: system
spec:
ports:
- name: https
port: 8443
targetPort: https
- name: metrics
port: 8080
targetPort: metrics
selector:
control-plane: controller-manager

View File

@@ -10,7 +10,7 @@ metadata:
spec:
endpoints:
- path: /metrics
port: https
port: metrics
selector:
matchLabels:
control-plane: controller-manager

View File

@@ -1,7 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs: ["/metrics"]
verbs: ["get"]

View File

@@ -1,13 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: proxy-role
rules:
- apiGroups: ["authentication.k8s.io"]
resources:
- tokenreviews
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources:
- subjectaccessreviews
verbs: ["create"]

View File

@@ -1,9 +1,8 @@
resources:
- role_binding.yaml
# Comment the following 4 lines if you want to disable
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
# which protects your /metrics endpoint.
- auth_proxy_service.yaml
- auth_proxy_role.yaml
- auth_proxy_role_binding.yaml
- auth_proxy_client_clusterrole.yaml
# Uncomment the following 3 lines if you are running Capsule
# in a cluster where [Pod Security Policies](https://kubernetes.io/docs/concepts/policy/pod-security-policy/)
# are enabled.
# - psp_policy.yaml
# - psp_role.yaml
# - psp_role_binding.yaml

View File

@@ -0,0 +1,18 @@
kind: PodSecurityPolicy
apiVersion: policy/v1beta1
metadata:
name: capsule
spec:
fsGroup:
rule: RunAsAny
hostPorts:
- max: 0
min: 0
runAsUser:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- secret

View File

@@ -0,0 +1,9 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: capsule-psp
rules:
- apiGroups: ['extensions']
resources: ['podsecuritypolicies']
resourceNames: ['capsule-psp']
verbs: ['use']

View File

@@ -1,12 +1,12 @@
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: proxy-rolebinding
name: capsule-use-psp
namespace: system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: proxy-role
name: capsule-psp
subjects:
- kind: ServiceAccount
name: default
namespace: system
- kind: ServiceAccount
name: default

View File

@@ -0,0 +1,11 @@
---
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
userGroups: ["capsule.clastix.io"]
forceTenantPrefix: false
protectedNamespaceRegex: ""
allowTenantIngressHostnamesCollision: false
allowIngressHostnameCollision: false

View File

@@ -4,6 +4,11 @@ kind: Tenant
metadata:
name: oil
spec:
ingressHostnames:
allowed:
- my.oil.acmecorp.com
- my.gas.acmecorp.com
allowedRegex: "^.*acmecorp.com$"
ingressClasses:
allowed:
- default
@@ -90,3 +95,7 @@ spec:
allowed:
- default
allowedRegex: ""
containerRegistries:
allowed:
- docker.io
allowedRegex: ""

View File

View File

@@ -1,3 +1,4 @@
## This file is auto-generated, do not modify ##
resources:
- capsule_v1alpha1_capsuleconfiguration.yaml
- capsule_v1alpha1_tenant.yaml

View File

@@ -2,5 +2,13 @@ resources:
- manifests.yaml
- service.yaml
patchesJson6902:
- target:
group: admissionregistration.k8s.io
kind: ValidatingWebhookConfiguration
name: validating-webhook-configuration
version: v1
path: patch_ns_selector.yaml
configurations:
- kustomizeconfig.yaml

View File

@@ -1,13 +1,14 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -23,15 +24,18 @@ webhooks:
- CREATE
resources:
- namespaces
sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1beta1
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: validating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -49,8 +53,10 @@ webhooks:
- UPDATE
resources:
- ingresses
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -67,8 +73,10 @@ webhooks:
- UPDATE
resources:
- ingresses
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -84,8 +92,10 @@ webhooks:
- CREATE
resources:
- namespaces
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -103,8 +113,29 @@ webhooks:
- DELETE
resources:
- networkpolicies
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-podpriority
failurePolicy: Ignore
name: podpriority.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -120,8 +151,49 @@ webhooks:
- CREATE
resources:
- persistentvolumeclaims
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-registry
failurePolicy: Ignore
name: pod.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-external-service-ips
failurePolicy: Fail
name: validating-external-service-ips.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -135,10 +207,13 @@ webhooks:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- tenants
- clientConfig:
caBundle: Cg==
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
@@ -154,3 +229,4 @@ webhooks:
- CREATE
resources:
- namespaces
sideEffects: None

View File

@@ -0,0 +1,42 @@
- op: add
path: /webhooks/0/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/1/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/3/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/4/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/5/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/6/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/7/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists

View File

@@ -0,0 +1,86 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rbac
import (
"context"
"github.com/go-logr/logr"
"github.com/pkg/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
type Manager struct {
Log logr.Logger
Client client.Client
}
// InjectClient injects the Client interface, required by the Runnable interface
func (r *Manager) InjectClient(c client.Client) error {
r.Client = c
return nil
}
func filterByName(objName, desired string) bool {
return objName == desired
}
func forOptionPerInstanceName(instanceName string) builder.ForOption {
return builder.WithPredicates(predicate.Funcs{
CreateFunc: func(event event.CreateEvent) bool {
return filterByName(event.Object.GetName(), instanceName)
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return filterByName(deleteEvent.Object.GetName(), instanceName)
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return filterByName(updateEvent.ObjectNew.GetName(), instanceName)
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return filterByName(genericEvent.Object.GetName(), instanceName)
},
})
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
Complete(r)
}
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)
cfg := configuration.NewCapsuleConfiguration(r.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)
return
}

View File

@@ -23,48 +23,54 @@ import (
"github.com/go-logr/logr"
"github.com/hashicorp/go-multierror"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/equality"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
type Manager struct {
CapsuleGroup string
Log logr.Logger
Client client.Client
Log logr.Logger
Client client.Client
Configuration configuration.Configuration
}
// Using the Client interface, required by the Runnable interface
// InjectClient injects the Client interface, required by the Runnable interface
func (r *Manager) InjectClient(c client.Client) error {
r.Client = c
return nil
}
func (r *Manager) filterByClusterRolesNames(name string) bool {
func (r *Manager) filterByNames(name string) bool {
return name == ProvisionerRoleName || name == DeleterRoleName
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager) (err error) {
//nolint:dupl
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) (err error) {
crErr := ctrl.NewControllerManagedBy(mgr).
For(&rbacv1.ClusterRole{}, builder.WithPredicates(predicate.Funcs{
CreateFunc: func(event event.CreateEvent) bool {
return r.filterByClusterRolesNames(event.Object.GetName())
return r.filterByNames(event.Object.GetName())
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return r.filterByClusterRolesNames(deleteEvent.Object.GetName())
return r.filterByNames(deleteEvent.Object.GetName())
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return r.filterByClusterRolesNames(updateEvent.ObjectNew.GetName())
return r.filterByNames(updateEvent.ObjectNew.GetName())
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return r.filterByClusterRolesNames(genericEvent.Object.GetName())
return r.filterByNames(genericEvent.Object.GetName())
},
})).
Complete(r)
@@ -74,18 +80,27 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) (err error) {
crbErr := ctrl.NewControllerManagedBy(mgr).
For(&rbacv1.ClusterRoleBinding{}, builder.WithPredicates(predicate.Funcs{
CreateFunc: func(event event.CreateEvent) bool {
return event.Object.GetName() == ProvisionerRoleName
return r.filterByNames(event.Object.GetName())
},
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
return deleteEvent.Object.GetName() == ProvisionerRoleName
return r.filterByNames(deleteEvent.Object.GetName())
},
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
return updateEvent.ObjectNew.GetName() == ProvisionerRoleName
return r.filterByNames(updateEvent.ObjectNew.GetName())
},
GenericFunc: func(genericEvent event.GenericEvent) bool {
return genericEvent.Object.GetName() == ProvisionerRoleName
return r.filterByNames(genericEvent.Object.GetName())
},
})).
Watches(source.NewKindWithCache(&v1alpha1.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{
UpdateFunc: func(updateEvent event.UpdateEvent, limitingInterface workqueue.RateLimitingInterface) {
if updateEvent.ObjectNew.GetName() == configurationName {
if crbErr := r.EnsureClusterRoleBindings(); crbErr != nil {
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
}
}
},
}).
Complete(r)
if crbErr != nil {
err = multierror.Append(err, crbErr)
@@ -93,91 +108,98 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) (err error) {
return
}
// This reconcile function is serving both ClusterRole and ClusterRoleBinding: that's ok, we're watching for multiple
// Reconcile serves both required ClusterRole and ClusterRoleBinding resources: that's ok, we're watching for multiple
// Resource kinds and we're just interested to the ones with the said name since they're bounded together.
func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
switch request.Name {
case ProvisionerRoleName:
if err = r.EnsureClusterRole(ProvisionerRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ProvisionerRoleName)
break
}
if err = r.EnsureClusterRoleBinding(); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRoleBinding failed", "ClusterRoleBinding", ProvisionerRoleName)
if err = r.EnsureClusterRoleBindings(); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed")
break
}
case DeleterRoleName:
if err = r.EnsureClusterRole(DeleterRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", DeleterRoleName)
break
}
}
return reconcile.Result{}, err
return
}
func (r *Manager) EnsureClusterRoleBinding() (err error) {
func (r *Manager) EnsureClusterRoleBindings() (err error) {
crb := &rbacv1.ClusterRoleBinding{
ObjectMeta: v1.ObjectMeta{
ObjectMeta: metav1.ObjectMeta{
Name: ProvisionerRoleName,
},
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, crb, func() error {
// RoleRef is immutable, so we need to delete and recreate ClusterRoleBinding if it changed
if crb.ResourceVersion != "" && !equality.Semantic.DeepDerivative(provisionerClusterRoleBinding.RoleRef, crb.RoleRef) {
return ImmutableClusterRoleBindingError{}
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, crb, func() (err error) {
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
crb.Subjects = []rbacv1.Subject{
{
crb.Subjects = []rbacv1.Subject{}
for _, group := range r.Configuration.UserGroups() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: "Group",
Name: r.CapsuleGroup,
},
Name: group,
})
}
return nil
})
if err != nil {
if _, ok := err.(ImmutableClusterRoleBindingError); ok {
if err = r.Client.Delete(context.TODO(), crb); err != nil {
r.Log.Error(err, "Cannot delete CRB during reset due to RoleRef change")
return
}
return r.Client.Create(context.TODO(), provisionerClusterRoleBinding, &client.CreateOptions{})
}
r.Log.Error(err, "Cannot CreateOrUpdate CRB")
return
}
})
return
}
func (r *Manager) EnsureClusterRole(roleName string) (err error) {
role, ok := clusterRoles[roleName]
if !ok {
return fmt.Errorf("ClusterRole %s is not mapped", roleName)
return fmt.Errorf("clusterRole %s is not mapped", roleName)
}
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: v1.ObjectMeta{
ObjectMeta: metav1.ObjectMeta{
Name: role.GetName(),
},
}
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, clusterRole, func() error {
clusterRole.Rules = role.Rules
return nil
})
return
}
// This is the Runnable function that is triggered upon Manager start-up to perform the first RBAC reconciliation
// Start is the Runnable function triggered upon Manager start-up to perform the first RBAC reconciliation
// since we're not creating empty CR and CRB upon Capsule installation: it's a run-once task, since the reconciliation
// is handled by the Reconciler implemented interface.
func (r *Manager) Start(ctx context.Context) (err error) {
func (r *Manager) Start(ctx context.Context) error {
for roleName := range clusterRoles {
r.Log.Info("setting up ClusterRoles", "ClusterRole", roleName)
if err = r.EnsureClusterRole(roleName); err != nil {
return
if err := r.EnsureClusterRole(roleName); err != nil {
if errors.IsAlreadyExists(err) {
continue
}
return err
}
}
r.Log.Info("setting up ClusterRoleBindings", "ClusterRoleBinding", ProvisionerRoleName)
err = r.EnsureClusterRoleBinding()
return
r.Log.Info("setting up ClusterRoleBindings")
if err := r.EnsureClusterRoleBindings(); err != nil {
if errors.IsAlreadyExists(err) {
return nil
}
return err
}
return nil
}

View File

@@ -20,10 +20,10 @@ import (
"bytes"
"context"
"errors"
"sync"
"time"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -37,25 +37,22 @@ import (
"github.com/clastix/capsule/pkg/cert"
)
type CaReconciler struct {
type CAReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Namespace string
}
func (r *CaReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, forOptionPerInstanceName(caSecretName)).
Complete(r)
}
func (r CaReconciler) UpdateValidatingWebhookConfiguration(wg *sync.WaitGroup, ch chan error, caBundle []byte) {
defer wg.Done()
var err error
ch <- retry.RetryOnConflict(retry.DefaultBackoff, func() error {
//nolint:dupl
func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
vw := &v1.ValidatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-validating-webhook-configuration"}, vw)
if err != nil {
@@ -72,12 +69,9 @@ func (r CaReconciler) UpdateValidatingWebhookConfiguration(wg *sync.WaitGroup, c
})
}
func (r CaReconciler) UpdateMutatingWebhookConfiguration(wg *sync.WaitGroup, ch chan error, caBundle []byte) {
defer wg.Done()
var err error
ch <- retry.RetryOnConflict(retry.DefaultBackoff, func() error {
//nolint:dupl
func (r CAReconciler) UpdateMutatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
mw := &v1.MutatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-mutating-webhook-configuration"}, mw)
if err != nil {
@@ -94,7 +88,7 @@ func (r CaReconciler) UpdateMutatingWebhookConfiguration(wg *sync.WaitGroup, ch
})
}
func (r CaReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
var err error
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
@@ -108,7 +102,7 @@ func (r CaReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
return reconcile.Result{}, err
}
var ca cert.Ca
var ca cert.CA
var rq time.Duration
ca, err = getCertificateAuthority(r.Client, r.Namespace)
if err != nil && errors.Is(err, MissingCaError{}) {
@@ -131,28 +125,24 @@ func (r CaReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
var crt *bytes.Buffer
var key *bytes.Buffer
crt, _ = ca.CaCertificatePem()
key, _ = ca.CaPrivateKeyPem()
crt, _ = ca.CACertificatePem()
key, _ = ca.CAPrivateKeyPem()
instance.Data = map[string][]byte{
certSecretKey: crt.Bytes(),
privateKeySecretKey: key.Bytes(),
}
wg := &sync.WaitGroup{}
wg.Add(2)
ch := make(chan error, 2)
group := errgroup.Group{}
group.Go(func() error {
return r.UpdateMutatingWebhookConfiguration(crt.Bytes())
})
group.Go(func() error {
return r.UpdateValidatingWebhookConfiguration(crt.Bytes())
})
go r.UpdateMutatingWebhookConfiguration(wg, ch, crt.Bytes())
go r.UpdateValidatingWebhookConfiguration(wg, ch, crt.Bytes())
wg.Wait()
close(ch)
for err = range ch {
if err != nil {
return reconcile.Result{}, err
}
if err = group.Wait(); err != nil {
return reconcile.Result{}, err
}
}

View File

@@ -30,7 +30,7 @@ import (
"github.com/clastix/capsule/pkg/cert"
)
func getCertificateAuthority(client client.Client, namespace string) (ca cert.Ca, err error) {
func getCertificateAuthority(client client.Client, namespace string) (ca cert.CA, err error) {
instance := &corev1.Secret{}
err = client.Get(context.TODO(), types.NamespacedName{

View File

@@ -17,9 +17,11 @@ limitations under the License.
package secret
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"syscall"
"time"
@@ -34,20 +36,20 @@ import (
"github.com/clastix/capsule/pkg/cert"
)
type TlsReconciler struct {
type TLSReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Namespace string
}
func (r *TlsReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *TLSReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, forOptionPerInstanceName(tlsSecretName)).
Complete(r)
}
func (r TlsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
var err error
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
@@ -61,7 +63,7 @@ func (r TlsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctr
return reconcile.Result{}, err
}
var ca cert.Ca
var ca cert.CA
var rq time.Duration
ca, err = getCertificateAuthority(r.Client, r.Namespace)
@@ -81,8 +83,9 @@ func (r TlsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctr
r.Log.Info("Missing Capsule TLS certificate")
rq = 6 * 30 * 24 * time.Hour
opts := cert.NewCertOpts(time.Now().Add(rq), "capsule-webhook-service.capsule-system.svc")
crt, key, err := ca.GenerateCertificate(opts)
opts := cert.NewCertOpts(time.Now().Add(rq), fmt.Sprintf("capsule-webhook-service.%s.svc", r.Namespace))
var crt, key *bytes.Buffer
crt, key, err = ca.GenerateCertificate(opts)
if err != nil {
r.Log.Error(err, "Cannot generate new TLS certificate")
return reconcile.Result{}, err

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service_labels
package servicelabels
import (
"context"

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service_labels
package servicelabels
import (
"github.com/go-logr/logr"

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service_labels
package servicelabels
import (
"github.com/go-logr/logr"

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service_labels
package servicelabels
import "fmt"

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service_labels
package servicelabels
import (
"github.com/go-logr/logr"

View File

@@ -19,12 +19,12 @@ package controllers
import (
"context"
"fmt"
"hash/fnv"
"strconv"
"strings"
"sync"
"github.com/go-logr/logr"
"github.com/hashicorp/go-multierror"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
@@ -76,56 +76,56 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
return reconcile.Result{}, nil
}
r.Log.Error(err, "Error reading the object")
return reconcile.Result{}, err
return
}
// Ensuring all namespaces are collected
r.Log.Info("Ensuring all Namespaces are collected")
if err := r.collectNamespaces(instance); err != nil {
if err = r.collectNamespaces(instance); err != nil {
r.Log.Error(err, "Cannot collect Namespace resources")
return reconcile.Result{}, err
return
}
r.Log.Info("Starting processing of Namespaces", "items", instance.Status.Namespaces.Len())
if err := r.syncNamespaces(instance); err != nil {
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 reconcile.Result{}, err
return
}
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
if err := r.syncNetworkPolicies(instance); err != nil {
if err = r.syncNetworkPolicies(instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
return reconcile.Result{}, err
}
r.Log.Info("Starting processing of Node Selector")
if err := r.ensureNodeSelector(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespaces Node Selector items")
return reconcile.Result{}, err
return
}
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges))
if err := r.syncLimitRanges(instance); err != nil {
if err = r.syncLimitRanges(instance); err != nil {
r.Log.Error(err, "Cannot sync LimitRange items")
return reconcile.Result{}, err
return
}
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota))
if err := r.syncResourceQuotas(instance); err != nil {
if err = r.syncResourceQuotas(instance); err != nil {
r.Log.Error(err, "Cannot sync ResourceQuota items")
return reconcile.Result{}, err
return
}
r.Log.Info("Ensuring PSP for owner")
if err = r.syncAdditionalRoleBindings(instance); err != nil {
r.Log.Error(err, "Cannot sync additional Role Bindings items")
return
}
r.Log.Info("Ensuring RoleBinding for owner")
if err := r.ownerRoleBinding(instance); err != nil {
if err = r.ownerRoleBinding(instance); err != nil {
r.Log.Error(err, "Cannot sync owner RoleBinding")
return reconcile.Result{}, err
return
}
r.Log.Info("Ensuring Namespace count")
if err := r.ensureNamespaceCount(instance); err != nil {
if err = r.ensureNamespaceCount(instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace count")
return reconcile.Result{}, err
return
}
r.Log.Info("Tenant reconciling completed")
@@ -149,7 +149,8 @@ func (r *TenantReconciler) pruningResources(ns string, keys []string, obj client
s = s.Add(*exists)
if len(keys) > 0 {
notIn, err := labels.NewRequirement(capsuleLabel, selection.NotIn, keys)
var notIn *labels.Requirement
notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys)
if err != nil {
return err
}
@@ -157,7 +158,7 @@ func (r *TenantReconciler) pruningResources(ns string, keys []string, obj client
}
r.Log.Info("Pruning objects with label selector " + s.String())
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
ListOptions: client.ListOptions{
LabelSelector: s,
@@ -166,56 +167,114 @@ func (r *TenantReconciler) pruningResources(ns string, keys []string, obj client
DeleteOptions: client.DeleteOptions{},
})
})
if err != nil {
return err
}
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 *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName, qt resource.Quantity, list ...corev1.ResourceQuota) (err error) {
ch := make(chan error, len(list))
func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName, actual, limit resource.Quantity, list ...corev1.ResourceQuota) error {
g := errgroup.Group{}
wg := &sync.WaitGroup{}
wg.Add(len(list))
f := func(rq corev1.ResourceQuota, wg *sync.WaitGroup, ch chan error) {
defer wg.Done()
ch <- retry.RetryOnConflict(retry.DefaultBackoff, func() error {
// Retrieving from the cache the actual ResourceQuota
for _, item := range list {
rq := item
g.Go(func() error {
found := &corev1.ResourceQuota{}
_ = r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found)
// 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)
if err := r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
return err
}
found.Labels = rq.Labels
found.Annotations[capsulev1alpha1.UsedQuotaFor(resourceName)] = qt.String()
// Updating the Resource according to the qt.Cmp result
found.Spec.Hard = rq.Spec.Hard
return r.Update(context.TODO(), found, &client.UpdateOptions{})
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[capsulev1alpha1.UsedQuotaFor(resourceName)] = actual.String()
found.Annotations[capsulev1alpha1.HardQuotaFor(resourceName)] = limit.String()
// Updating the Resource according to the actual.Cmp result
found.Spec.Hard = rq.Spec.Hard
return nil
})
return err
})
})
}
for _, rq := range list {
go f(rq, wg, ch)
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())
}
wg.Wait()
close(ch)
for e := range ch {
if e != nil {
// We had an error and we mark the whole transaction as failed
// to process it another time acording to the Tenant controller back-off factor.
r.Log.Error(e, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
err = fmt.Errorf("update of outer ResourceQuota items has failed")
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 *capsulev1alpha1.Tenant) (err error) {
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hash := func(value string) string {
h := fnv.New64a()
_, _ = h.Write([]byte(value))
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.ClusterRoleName))
}
var tl, ll string
tl, err = capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
if err != nil {
return
}
ll, err = capsulev1alpha1.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 := range tenant.Spec.AdditionalRoleBindings {
lv := hash(i.ClusterRoleName)
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%s", tenant.Name, i.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: i.ClusterRoleName,
}
rb.Subjects = i.Subjects
return controllerutil.SetControllerReference(tenant, rb, r.Scheme)
})
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
return nil
}
// We're relying on the ResourceQuota resource to represent the resource quota for the single Tenant rather than the
@@ -320,7 +379,7 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
default:
// The Tenant is respecting the Hard quota:
// restoring the default one for all the elements,
// also for the reconciliated one.
// 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{}
@@ -329,7 +388,7 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
}
target.Spec = q
}
if err := r.resourceQuotasUpdate(rn, qt, rql.Items...); err != nil {
if err := r.resourceQuotasUpdate(rn, qt, q.Hard[rn], rql.Items...); err != nil {
r.Log.Error(err, "cannot proceed with outer ResourceQuota")
return err
}
@@ -393,55 +452,58 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
return nil
}
func (r *TenantReconciler) syncNamespace(namespace string, ingressClassesSpec capsulev1alpha1.IngressClassesSpec, storageClassesSpec capsulev1alpha1.StorageClassesSpec, nsMetadata capsulev1alpha1.AdditionalMetadata, tenantLabel string, wg *sync.WaitGroup, channel chan error) {
defer wg.Done()
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1alpha1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
ns := &corev1.Namespace{}
if err = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
return
}
ns := &corev1.Namespace{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
channel <- err
}
channel <- retry.RetryOnConflict(retry.DefaultBackoff, func() error {
a := ns.GetAnnotations()
a := tnt.Spec.NamespacesMetadata.AdditionalAnnotations
if a == nil {
a = make(map[string]string)
}
if len(ingressClassesSpec.Allowed) > 0 {
a[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(ingressClassesSpec.Allowed, ",")
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 len(ingressClassesSpec.AllowedRegex) > 0 {
a[capsulev1alpha1.AvailableIngressClassesRegexpAnnotation] = ingressClassesSpec.AllowedRegex
}
if len(storageClassesSpec.Allowed) > 0 {
a[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(storageClassesSpec.Allowed, ",")
}
if len(storageClassesSpec.AllowedRegex) > 0 {
a[capsulev1alpha1.AvailableStorageClassesRegexpAnnotation] = storageClassesSpec.AllowedRegex
}
if aa := nsMetadata.AdditionalAnnotations; aa != nil {
for k, v := range aa {
a[k] = v
if tnt.Spec.IngressClasses != nil {
if len(tnt.Spec.IngressClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
}
if len(tnt.Spec.IngressClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
a[capsulev1alpha1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
a[capsulev1alpha1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
ns.SetAnnotations(a)
l := ns.GetLabels()
l := tnt.Spec.NamespacesMetadata.AdditionalLabels
if l == nil {
l = make(map[string]string)
}
capsuleLabel, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
if err != nil {
return err
}
l[capsuleLabel] = tenantLabel
if al := nsMetadata.AdditionalLabels; al != nil {
for k, v := range al {
l[k] = v
}
}
l["name"] = namespace
capsuleLabel, _ := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
l[capsuleLabel] = tnt.GetName()
ns.SetLabels(l)
ns.SetAnnotations(a)
return r.Client.Update(context.TODO(), ns, &client.UpdateOptions{})
})
@@ -449,22 +511,18 @@ func (r *TenantReconciler) syncNamespace(namespace string, ingressClassesSpec ca
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err error) {
ch := make(chan error, tenant.Status.Namespaces.Len())
group := errgroup.Group{}
wg := &sync.WaitGroup{}
wg.Add(tenant.Status.Namespaces.Len())
for _, ns := range tenant.Status.Namespaces {
go r.syncNamespace(ns, tenant.Spec.IngressClasses, tenant.Spec.StorageClasses, tenant.Spec.NamespacesMetadata, tenant.GetName(), wg, ch)
for _, item := range tenant.Status.Namespaces {
namespace := item
group.Go(func() error {
return r.syncNamespaceMetadata(namespace, tenant)
})
}
wg.Wait()
close(ch)
for e := range ch {
if e != nil {
err = multierror.Append(e, err)
}
if err = group.Wait(); err != nil {
r.Log.Error(err, "Cannot sync Namespaces")
err = fmt.Errorf("cannot sync Namespaces: %s", err.Error())
}
return
}
@@ -572,47 +630,9 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
return nil
}
func (r *TenantReconciler) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (err error) {
if tenant.Spec.NodeSelector == nil {
return
}
for _, namespace := range tenant.Status.Namespaces {
selectorMap := tenant.Spec.NodeSelector
if selectorMap == nil {
return
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
if ns.Annotations == nil {
ns.Annotations = make(map[string]string)
}
var selector []string
for k, v := range selectorMap {
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
}
ns.Annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
return nil
})
r.Log.Info("Namespace Node sync result: "+string(res), "name", ns.Name)
if err != nil {
return err
}
}
return
}
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1alpha1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(tenant.Status.Namespaces.Len())
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1alpha1.Tenant{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
@@ -622,17 +642,19 @@ func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1alpha1.Tenant)
})
}
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.Tenant) (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 {
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.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
}
tenant.AssignNamespaces(nl.Items)
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, tenant.DeepCopy(), func() error {
return r.Client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
})
return
}

44
docs/index.md Normal file
View File

@@ -0,0 +1,44 @@
# 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](./operator/overview.md)
* [Capsule Proxy](./proxy/overview.md)
* [Capsule Lens extension](lens-extension/overview.md) Coming soon!
## Documents structure
```command
docs
├── index.md
├── lens-extension
│ └── overview.md
├── proxy
│ ├── overview.md
│ ├── sidecar.md
│ └── standalone.md
└── operator
├── contributing.md
├── getting-started.md
├── monitoring.md
├── overview.md
├── references.md
└── use-cases
├── create-namespaces.md
├── custom-resources.md
├── images-registries.md
├── ingress-classes.md
├── ingress-hostnames.md
├── multiple-tenants.md
├── network-policies.md
├── node-ports.md
├── nodes-pool.md
├── onboarding.md
├── overview.md
├── permissions.md
├── pod-priority-class.md
├── pod-security-policies.md
├── resources-quota-limits.md
├── storage-classes.md
└── taint-namespaces.md
```

View File

@@ -0,0 +1,2 @@
# Capsule extension for Mirantis Lens
Coming soon.

View File

@@ -1,11 +1,9 @@
# How to contribute to Capsule
First, thanks for your interest in Capsule, any contribution is welcome!
The first step is to set up your local development environment
The first step is to set up your local development environment as stated below:
## Setting up the development environment
The following dependencies are mandatory:
- [Go 1.13.8](https://golang.org/dl/)
@@ -16,7 +14,6 @@ The following dependencies are mandatory:
- [golangci-lint](https://github.com/golangci/golangci-lint)
### Installing Go dependencies
After cloning Capsule on any folder, access it and issue the following command
to ensure all dependencies are properly downloaded.
@@ -25,20 +22,17 @@ go mod download
```
### Installing Operator SDK
Some operations, like the Docker image build process or the code-generation of
the CRDs manifests, as well the deep copy functions, require _Operator SDK_:
the binary has to be installed into your `PATH`.
### Installing Kubebuilder
With the latest release of OperatorSDK there's a more tightly integration with
Kubebuilder and its opinionated testing suite: ensure to download the latest
binaries available from the _Releases_ GitHub page and place them into the
`/usr/local/kubebuilder/bin` folder, ensuring this is also in your `PATH`.
### Installing KinD
Capsule can run on any certified Kubernetes installation and locally
the whole development is performed on _KinD_, also knows as
[Kubernetes in Docker](https://github.com/kubernetes-sigs/kind).
@@ -72,75 +66,21 @@ The current `KUBECONFIG` will be populated with the `cluster-admin`
certificates and the context changed to the just born Kubernetes cluster.
### Build the Docker image and push it to KinD
From the root path, issue the _make_ recipe:
```
# make docker-build
/home/prometherion/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
main.go
go vet ./...
/home/prometherion/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go test ./... -coverprofile cover.out
...
docker build . -t quay.io/clastix/capsule:latest
Sending build context to Docker daemon 43.21MB
Step 1/15 : FROM golang:1.13 as builder
---> 67d10cb69049
Step 2/15 : WORKDIR /workspace
---> Using cache
---> d783cc2b7c33
Step 3/15 : COPY go.mod go.mod
---> Using cache
---> 0fec3ca39e50
Step 4/15 : COPY go.sum go.sum
---> Using cache
---> de15be20dbe7
Step 5/15 : RUN go mod download
---> Using cache
---> b525cd9abc67
Step 6/15 : COPY main.go main.go
---> 67d9d6538ffc
Step 7/15 : COPY api/ api/
---> 6243b250d170
Step 8/15 : COPY controllers/ controllers/
---> 4abf8ce85484
Step 9/15 : COPY pkg/ pkg/
---> 2cd289b1d496
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
---> Running in dac9a1e3b23f
Removing intermediate container dac9a1e3b23f
---> bb650a8efcb2
Step 11/15 : FROM gcr.io/distroless/static:nonroot
---> 131713291b92
Step 12/15 : WORKDIR /
---> Using cache
---> 677a73ab94d3
Step 13/15 : COPY --from=builder /workspace/manager .
---> 6ecb58a82c0a
Step 14/15 : USER nonroot:nonroot
---> Running in a0b8c95f85d4
Removing intermediate container a0b8c95f85d4
---> c4897d60a094
Step 15/15 : ENTRYPOINT ["/manager"]
---> Running in 1a42bab52aa7
Removing intermediate container 1a42bab52aa7
---> 37d2adbe2669
Successfully built 37d2adbe2669
Successfully tagged quay.io/clastix/capsule:latest
```
The image `quay.io/clastix/capsule:latest` will be available locally, you just
need to push it to _KinD_ with the following command.
The image `quay.io/clastix/capsule:<tag>` will be available locally. Built image `<tag>` is resulting last one available [release](https://github.com/clastix/capsule/releases).
Push it to _KinD_ with the following command:
```
# kind load docker-image --nodes capsule-control-plane --name capsule quay.io/clastix/capsule:latest
Image: "quay.io/clastix/capsule:latest" with ID "sha256:ebb8f640dda129a795ddc68bad125cb50af6bfb8803be210b56314ded6355759" not yet present on node "capsule-control-plane", loading...
# kind load docker-image --nodes capsule-control-plane --name capsule quay.io/clastix/capsule:<tag>
```
### Deploy the Kubernetes manifests
With the current `kind-capsule` context enabled, deploy all the required
manifests issuing the following command:
@@ -154,21 +94,6 @@ You can check if Capsule is running tailing the logs:
```
# kubectl -n capsule-system logs --all-containers -f -l control-plane=controller-manager
...
2020-08-03T15:37:44.031Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace-deleter", "namespace": "oil-dev"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace:admin", "namespace": "oil-production"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Role Binding sync result: unchanged {"Request.Name": "oil", "name": "namespace-deleter", "namespace": "oil-production"}
2020-08-03T15:37:44.032Z INFO controllers.Tenant Tenant reconciling completed {"Request.Name": "oil"}
2020-08-03T15:37:44.032Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "tenant", "request": "/oil"}
2020-08-03T15:37:46.945Z INFO controllers.Namespace Reconciling Namespace {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.953Z INFO controllers.Namespace Namespace reconciliation processed {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.953Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "namespace", "request": "/oil-staging"}
2020-08-03T15:37:46.957Z INFO controllers.Namespace Reconciling Namespace {"Request.Name": "oil-staging"}
2020-08-03T15:37:46.957Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "namespace", "request": "/oil-staging"}
I0803 15:16:01.763606 1 main.go:186] Valid token audiences:
I0803 15:16:01.763689 1 main.go:232] Generating self signed cert as no cert is provided
I0803 15:16:02.042022 1 main.go:281] Starting TCP socket on 0.0.0.0:8443
I0803 15:16:02.042364 1 main.go:288] Listening securely on 0.0.0.0:8443
```
Since Capsule is built using _OperatorSDK_, logging is handled by the zap
@@ -185,12 +110,10 @@ it is suggested to use the `--zap-devel` flag to get also stack traces.
> application to serve properly HTTPS requests.
### Run Capsule locally
Debugging remote applications is always struggling but Operators just need
access to the Kubernetes API Server.
#### Scaling down the remote Pod
First, ensure the Capsule pod is not running scaling down the Deployment.
```
@@ -201,7 +124,6 @@ deployment.apps/capsule-controller-manager scaled
> This is mandatory since Capsule uses Leader Election
#### Providing TLS certificate for webhooks
Next step is to replicate the same environment Capsule is expecting in the Pod,
it means creating a fake certificate to handle HTTP requests.
@@ -217,7 +139,6 @@ kubectl -n capsule-system get secret capsule-tls -o jsonpath='{.data.tls\.key}'
> to provide a self-signed certificate in the said directory.
#### Starting NGROK
In another session, we need a `ngrok` session, mandatory to debug also webhooks
(YMMV).
@@ -241,7 +162,6 @@ since we're going to use this default URL as the `url` parameter for the
_Dynamic Admissions Control Webhooks_.
#### Patching the MutatingWebhookConfiguration
Now it's time to patch the _MutatingWebhookConfiguration_ and the
_ValidatingWebhookConfiguration_ too, adding the said `ngrok` URL as base for
each defined webhook, as following:
@@ -270,7 +190,6 @@ webhooks:
```
#### Run Capsule
Finally, it's time to run locally Capsule using your preferred IDE (or not):
from the project root path, you can issue the following command.
@@ -282,21 +201,20 @@ All the logs will start to flow in your standard output, feel free to attach
your debugger to set breakpoints as well!
## Code convention
The changes must follow the Pull Request method where a _GitHub Action_ will
check the `golangci-lint`, so ensure your changes respect the coding standard.
### golint
You can easily check them issuing the _Make_ recipe `golint`.
```
# make golint
golangci-lint run
golangci-lint run -c .golangci.yml
```
### goimports
> Enabled linters and related options are defined in the [.golanci.yml file](../../.golangci.yml)
### goimports
Also, the Go import statements must be sorted following the best practice:
```
@@ -315,7 +233,6 @@ 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.

View File

@@ -0,0 +1,123 @@
# Getting started
Thanks for giving Capsule a try.
## Installation
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)
### Install with kustomize
Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
Clone this repository and move to the repo folder:
```
$ git clone https://github.com/clastix/capsule
$ cd capsule
$ make deploy
```
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_.
Capsule defines a Tenant as Custom Resource with cluster scope:
```yaml
cat <<EOF > oil_tenant.yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: oil
spec:
owner:
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
```
$ kubectl get tenants
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND NODE SELECTOR AGE
oil 3 0 alice User 1m
```
## Tenant owners
Each tenant comes with a delegated user or group of users acting as the tenant admin. In the Capsule jargon, this is called the _Tenant Owner_. Other users can operate inside a tenant with different levels of permissions and authorizations assigned directly by the Tenant Owner.
Capsule does not care about the authentication strategy used in the cluster and all the Kubernetes methods of [authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) are supported. The only requirement to use Capsule is to assign tenant users to the group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
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
```json
...
"users_groups": [
"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`
```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
```
Log as tenant owner
```
$ export KUBECONFIG=alice-oil.kubeconfig
```
and create a couple of new namespaces
```
$ kubectl create namespace oil-production
$ kubectl create namespace oil-development
```
As user `alice` you can operate with fully admin permissions:
```
$ kubectl -n oil-development run nginx --image=docker.io/nginx
$ kubectl -n oil-development get pods
```
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"
```
# 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.

View File

@@ -0,0 +1,2 @@
# Monitoring Capsule
Coming soon.

41
docs/operator/overview.md Normal file
View File

@@ -0,0 +1,41 @@
# Kubernetes multi-tenancy made simple
**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 minimalist approach, leveraging only on upstream Kubernetes.
# What's the problem with the current status?
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it becomes soon complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well know phenomena of the _clusters sprawl_.
# Entering Caspule
Capsule takes a different approach. In a single cluster, it aggregates multiple namespaces in a lightweight abstraction called _Tenant_. Within each tenant, users are free to create their namespaces and share all the assigned resources while a Policy Engine keeps different tenants isolated from each other. The _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, _RBAC_, and other policies defined at the tenant level are automatically inherited by all the namespaces in the tenant. And users are free to operate their tenants in authonomy, without the intervention of the cluster administrator.
# Features
## Self-Service
Leave to developers the freedom to self-provision their cluster resources according to the assigned boundaries.
## Preventing Clusters Sprawl
Share a single cluster with multiple teams, groups of users, or departments by saving operational and management efforts.
## Governance
Leverage Kubernetes Admission Controllers to enforce the industry security best practices and meet legal requirements.
## Resources Control
Take control of the resources consumed by users while preventing them to overtake.
## Native Experience
Provide multi-tenancy with a native Kubernetes experience without introducing additional management layers, plugins, or customised binaries.
## GitOps ready
Capsule is completely declarative and GitOps ready.
## Bring your own device (BYOD)
Assign to tenants a dedicated set of compute, storage, and network resources and avoid the noisy neighbors' effect.
# Common use cases for Capsule
Please, refer to the corresponding [section](./use-cases/overview.md) in the project documentation for a detailed list of common use cases that Capsule can address.
# Whats next
Have a fun with Capsule:
* [Getting Started](./getting-started.md)
* [Use Cases](./use-cases/overview.md)
* [Contributing](./contributing.md)
* [References](./references.md)

723
docs/operator/references.md Normal file
View File

@@ -0,0 +1,723 @@
# Reference
* [Custom Resource Definition](#customer-resource-definition)
* [Metadata](#metadata)
* [name](#name)
* [Spec](#spec)
* [owner](#owner)
* [nodeSelector](#nodeSelector)
* [namespaceQuota](#namespaceQuota)
* [namespacesMetadata](#namespacesMetadata)
* [servicesMetadata](#servicesMetadata)
* [ingressClasses](#ingressClasses)
* [ingressHostnames](#ingressHostnames)
* [storageClasses](#storageClasses)
* [containerRegistries](#containerRegistries)
* [additionalRoleBindings](#additionalRoleBindings)
* [resourceQuotas](#resourceQuotas)
* [limitRanges](#limitRanges)
* [networkPolicies](#networkPolicies)
* [externalServiceIPs](#externalServiceIPs)
* [Status](#status)
* [size](#size)
* [namespaces](#namespaces)
* [Role Based Access Control](#role-based-access-control)
* [Admission Controllers](#admission-controller)
* [Command Options](#command-options)
* [Created Resources](#created-resources)
## Custom Resource Definition
Capsule operator uses a single Custom Resources Definition (CRD) for _Tenants_. Please, see the [Tenant Custom Resource Definition](https://github.com/clastix/capsule/blob/master/config/crd/bases/capsule.clastix.io_tenants.yaml). In Caspule, Tenants are cluster wide resources. You need for cluster level permissions to work with tenants.
### Metadata
#### name
Metadata `name` can contain any valid symbol from the regex: `[a-z0-9]([-a-z0-9]*[a-z0-9])?`.
### Spec
#### owner
The field `owner` is the only mandatory spec in a _Tenant_ manifest. It specifies the ownership of the tenant:
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
owner: # required
name: <name>
kind: <User|Group>
```
The user and group names should be valid identities. Capsule does not care about the authentication strategy used in the cluster and all the Kubernetes methods of [Authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) are supported. The only requirement to use Capsule is to assign tenant users to the the group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
Assignment to a group depends on the used authentication strategy.
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
```json
...
"users_groups": [
"capsule.clastix.io",
"other_group"
]
```
Permissions are controlled by RBAC.
#### nodeSelector
Field `nodeSelector` specifies the label to control the placement of pods on a given pool of worker nodes:
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
nodeSelector:
<key>: <value>
```
All namesapces created within the tenant will have the annotation:
```yaml
kind: Namespace
apiVersion: v1
metadata:
annotations:
scheduler.alpha.kubernetes.io/node-selector: 'key=value'
```
This annotation tells the Kubernetes scheduler to place pods on the nodes having that label:
```yaml
kind: Pod
apiVersion: v1
metadata:
name: sample
spec:
nodeSelector:
<key>: <value>
```
> NB:
> While Capsule just enforces the annotation `scheduler.alpha.kubernetes.io/node-selector` at namespace level,
> the `nodeSelector` field in the pod template is under the control of the default _PodNodeSelector_ enabled
> on the Kubernetes API server using the flag `--enable-admission-plugins=PodNodeSelector`.
Please, see how to [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) documentation.
The tenant owner is not allowed to change or remove the annotation above from the namespace.
#### namespaceQuota
Field `namespaceQuota` specifies the maximum number of namespaces allowed for that tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
namespaceQuota: <quota>
```
Once the namespace quota assigned to the tenant has been reached, yhe tenant owner cannot create further namespaces.
#### namespacesMetadata
Field `namespacesMetadata` specifies additional labels and annotations the Capsule operator places on any _Namespace_ in the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
namespacesMetadata:
additionalAnnotations:
<annotations>
additionalLabels:
<key>: <value>
```
Al namespaces in the tenant will have:
```yaml
kind: Namespace
apiVersion: v1
metadata:
annotations:
<annotations>
labels:
<key>: <value>
```
The tenant owner is not allowed to change or remove such labels and annotations from the namespace.
#### servicesMetadata
Field `servicesMetadata` specifies additional labels and annotations the Capsule operator places on any _Service_ in the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
servicesMetadata:
additionalAnnotations:
<annotations>
additionalLabels:
<key>: <value>
```
Al services in the tenant will have:
```yaml
kind: Service
apiVersion: v1
metadata:
annotations:
<annotations>
labels:
<key>: <value>
```
The tenant owner is not allowed to change or remove such labels and annotations from the _Service_.
#### ingressClasses
Field `ingressClasses` specifies the _IngressClass_ assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
ingressClasses:
allowed:
- <class>
allowedRegex: <regex>
```
Capsule assures that all the _Ingress_ resources created in the tenant can use only one of the allowed _IngressClass_.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <name>
namespace:
annotations:
kubernetes.io/ingress.class: <class>
```
> NB: _Ingress_ resources are supported in both the versions, `networking.k8s.io/v1beta1` and `networking.k8s.io/v1`.
Allowed _IngressClasses_ are reported into namespaces as annotations, so the tenant owner can check them
```yaml
kind: Namespace
apiVersion: v1
metadata:
annotations:
capsule.clastix.io/ingress-classes: <class>
capsule.clastix.io/ingress-classes-regexp: <regex>
```
Any tentative of tenant owner to use a not allowed _IngressClass_ will fail.
#### ingressHostnames
Field `ingressHostnames` specifies the allowed hostnames in _Ingresses_ for the given tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
ingressHostnames:
allowed:
- <hostname>
allowedRegex: <regex>
```
Capsule assures that all _Ingress_ resources created in the tenant can use only one of the allowed hostnames.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <name>
namespace:
annotations:
spec:
rules:
- host: <hostname>
http: {}
```
> NB: _Ingress_ resources are supported in both the versions, `networking.k8s.io/v1beta1` and `networking.k8s.io/v1`.
Any tentative of tenant owner to use one of not allowed hostnames will fail.
#### storageClasses
Field `storageClasses` specifies the _StorageClasses_ assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
storageClasses:
allowed:
- <class>
allowedRegex: <regex>
```
Capsule assures that all _PersistentVolumeClaim_ resources created in the tenant can use only one of the allowed _StorageClasses_.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <name>
namespace:
spec:
storageClassName: <class>
```
Allowed _StorageClasses_ are reported into namespaces as annotations, so the tenant owner can check them
```yaml
kind: Namespace
apiVersion: v1
metadata:
annotations:
capsule.clastix.io/storage-classes: <class>
capsule.clastix.io/storage-classes-regexp: <regex>
```
Any tentative of tenant owner to use a not allowed _StorageClass_ will fail.
#### containerRegistries
Field `containerRegistries` specifies the ttrusted image registries assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
containerRegistries:
allowed:
- <registry>
allowedRegex: <regex>
```
Capsule assures that all _Pods_ resources created in the tenant can use only one of the allowed trusted registries.
Allowed registries are reported into namespaces as annotations, so the tenant owner can check them
```yaml
kind: Namespace
apiVersion: v1
metadata:
annotations:
capsule.clastix.io/allowed-registries-regexp: <regex>
capsule.clastix.io/registries: <registry>
```
Any tentative of tenant owner to use a not allowed registry will fail.
> NB:
> In case of naked and official images hosted on Docker Hub, Capsule is going
> to retrieve the registry even if it's not explicit: a `busybox:latest` Pod
> running on a Tenant allowing `docker.io` will not blocked, even if the image
> field is not explicit as `docker.io/busybox:latest`.
#### additionalRoleBindings
Field `additionalRoleBindings` specifies additional _RoleBindings_ assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
additionalRoleBindings:
- clusterRoleName: <ClusterRole>
subjects:
- kind: <Group|User|ServiceAccount>
apiGroup: rbac.authorization.k8s.io
name: <name>
```
Capsule will ensure that all namespaces in the tenant always contain the _RoleBinding_ for the given _ClusterRole_.
#### resourceQuotas
Field `resourceQuotas` specifies a list of _ResourceQuota_ resources assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
resourceQuotas:
- hard:
limits.cpu: <hard_value>
limits.memory: <hard_value>
requests.cpu: <hard_value>
requests.memory: <hard_value>
```
Please, refer to [ResourceQuota](https://kubernetes.io/docs/concepts/policy/resource-quotas/) documentation for the subject.
The assigned quota are inherited by any namespace created in the tenant
```yaml
kind: ResourceQuota
apiVersion: v1
metadata:
name: compute
namespace:
labels:
capsule.clastix.io/resource-quota=0
capsule.clastix.io/tenant=tenant
annotations:
# used resources in the tenant
quota.capsule.clastix.io/used-limits.cpu=<tenant_used_value>
quota.capsule.clastix.io/used-limits.memory=<tenant_used_value>
quota.capsule.clastix.io/used-requests.cpu=<tenant_used_value>
quota.capsule.clastix.io/used-requests.memory=<tenant_used_value>
# hard quota for the tenant
quota.capsule.clastix.io/hard-limits.cpu=<tenant_hard_value>
quota.capsule.clastix.io/hard-limits.memory=<tenant_hard_value>
quota.capsule.clastix.io/hard-requests.cpu=<tenant_hard_value>
quota.capsule.clastix.io/hard-requests.memory=<tenant_hard_value>
spec:
hard:
limits.cpu: <hard_value>
limits.memory: <hard_value>
requests.cpu: <hard_value>
requests.memory: <hard_value>
status:
hard:
limits.cpu: <namespace_hard_value>
limits.memory: <namespace_hard_value>
requests.cpu: <namespace_hard_value>
requests.memory: <namespace_hard_value>
used:
limits.cpu: <namespace_used_value>
limits.memory: <namespace_used_value>
requests.cpu: <namespace_used_value>
requests.memory: <namespace_used_value>
```
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.
The annotations
```yaml
quota.capsule.clastix.io/used-<resource>=<tenant_used_value>
quota.capsule.clastix.io/hard-<resource>=<tenant_hard_value>
```
are updated in realtime by Capsule, according to the actual aggredated usage of resource in the tenant.
> NB:
> While Capsule controls quota at tenant level, at namespace level the quota enforcement
> is under the control of the default _ResourceQuota Admission Controller_ enabled on the
> Kubernetes API server using the flag `--enable-admission-plugins=ResourceQuota`.
The tenant owner is not allowed to change or remove the _ResourceQuota_ from the namespace.
#### limitRanges
Field `limitRanges` specifies the _LimitRanges_ assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
limitRanges:
- limits:
- type: Pod
max:
cpu: <value>
memory: <value>
min:
cpu: <value>
memory: <value>
- type: Container
default:
cpu: <value>
memory: <value>
defaultRequest:
cpu: <value>
memory: <value>
max:
cpu: <value>
memory: <value>
min:
cpu: <value>
memory: <value>
- type: PersistentVolumeClaim
max:
storage: <value>
min:
storage: <value>
```
Please, refer to [LimitRange](https://kubernetes.io/docs/concepts/policy/limit-range/) documentation for the subject.
The assigned _LimitRanges_ are inherited by any namespace created in the tenant
```yaml
kind: LimitRange
apiVersion: v1
metadata:
name: <name>
namespace:
spec:
limits:
- type: Pod
max:
cpu: <value>
memory: <value>
min:
cpu: <value>
memory: <value>
- type: Container
default:
cpu: <value>
memory: <value>
defaultRequest:
cpu: <value>
memory: <value>
max:
cpu: <value>
memory: <value>
min:
cpu: <value>
memory: <value>
- type: PersistentVolumeClaim
max:
storage: <value>
min:
storage: <value>
```
> NB:
> Limit ranges enforcement for a single pod, container, and persistent volume
> claim is done by the default _LimitRanger Admission Controller_ enabled on
> the Kubernetes API server: using the flag
> `--enable-admission-plugins=LimitRanger`.
Being the limit range specific of single resources, there is no aggregate to count.
The tenant owner is not allowed to change or remove _LimitRanges_ from the namespace.
#### networkPolicies
Field `networkPolicies` specifies the _NetworkPolicies_ assigned to the tenant.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
networkPolicies:
- policyTypes:
- Ingress
- Egress
egress:
- to:
- ipBlock:
cidr: <value>
ingress:
- from:
- namespaceSelector: {}
- podSelector: {}
- ipBlock:
cidr: <value>
podSelector: {}
```
Please, refer to [NetworkPolicies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) documentation for the subjects of a _NetworkPolicy_.
The assigned _NetworkPolicies_ are inherited by any namespace created in the tenant.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <name>
namespace:
spec:
podSelector: {}
ingress:
- from:
- namespaceSelector: {}
- podSelector: {}
- ipBlock:
cidr: <value>
egress:
- to:
- ipBlock:
cidr: <value>
policyTypes:
- Ingress
- Egress
```
The tenant owner can create, patch and delete additional _NetworkPolicy_ to refine the assigned one. However, the tenant owner cannot delete the _NetworkPolicies_ set at tenant level.
#### externalServiceIPs
Field `externalServiceIPs` specifies the external IPs that can be used in _Services_ with type `ClusterIP`.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
externalServiceIPs:
allowed:
- <cidr>
```
Capsule will ensure that all _Services_ in the tenant can contain only the allowed external IPs. This mitigate the [_CVE-2020-8554_] vulnerability where a potential attacker, able to create a _Service_ with type `ClusterIP` and set the `externalIPs` field, can intercept traffic to that IP. Leave only the allowed CIDRs list to be set as `externalIPs` field in a _Service_ with type `ClusterIP`.
To prevent users to set the `externalIPs` field, use an empty allowed list:
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
externalServiceIPs:
allowed: []
```
> NB: Missing of this controller, it exposes your cluster to the vulnerability [_CVE-2020-8554_].
### Status
#### size
Status field `size` reports the number of namespaces belonging to the tenant. It is reported as `NAMESPACE COUNT` in the `kubectl` output:
```
$ kubectl get tnt
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND NODE SELECTOR AGE
cap 9 1 joe User {"pool":"cmp"} 5d4h
gas 6 2 alice User {"node":"worker"} 5d4h
oil 9 3 alice User {"pool":"cmp"} 5d4h
sample 9 0 alice User {"key":"value"} 29h
```
#### namespaces
Status field `namespaces` reports the list of all namespaces belonging to the tenant.
```yaml
...
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: tenant
spec:
...
status:
namespaces:
oil-development
oil-production
oil-marketing
size: 3
```
## Role Based Access Control
In the current implementation, the Capsule operator requires cluster admin permissions to fully operate.
## Admission Controllers
Capsule implements Kubernetes multi-tenancy capabilities using a minimum set of standard [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled on the Kubernetes APIs server.
Here the list of required Admission Controllers you have to enable to get full support from Capsule:
* PodNodeSelector
* LimitRanger
* ResourceQuota
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
In addition to the required controllers above, Capsule implements its own set through the [Dynamic Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) mechanism, providing callbacks to add further validation or resource patching.
To see Admission Controls installed by Capsule:
```
$ kubectl get ValidatingWebhookConfiguration
NAME WEBHOOKS AGE
capsule-validating-webhook-configuration 8 2h
$ kubectl get MutatingWebhookConfiguration
NAME WEBHOOKS AGE
capsule-mutating-webhook-configuration 1 2h
```
## Command Options
The Capsule operator provides following command options:
Option | Description | Default
--- | --- | ---
`--metrics-addr` | The address and port where `/metrics` are exposed. | `127.0.0.1:8080`
`--enable-leader-election` | Start a leader election client and gain leadership before executing the main loop. | `true`
`--zap-log-level` | The log verbosity with a value from 1 to 10 or the basic keywords. | `4`
`--zap-devel` | The flag to get the stack traces for deep debugging. | `null`
`--configuration-name` | The Capsule Configuration CRD name, a default is installed automatically | `default`
## Capsule Configuration
The Capsule configuration can be piloted by a Custom Resource definition named `CapsuleConfiguration`.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
userGroups: ["capsule.clastix.io"]
forceTenantPrefix: false
protectedNamespaceRegex: ""
allowTenantIngressHostnamesCollision: false
allowIngressHostnameCollision: false
```
Option | Description | Default
--- | --- | ---
`.spec.forceTenantPrefix` | Force the tenant name as prefix for namespaces: `<tenant_name>-<namespace>`. | `false`
`.spec.userGroups` | Array of Capsule groups to which all tenant owners must belong. | `[capsule.clastix.io]`
`.spec.protectedNamespaceRegex` | Disallows creation of namespaces matching the passed regexp. | `null`
`.spec.allowTenantIngressHostnamesCollision` | By default, Capsule allows Ingress hostname collision: set to `false` to enforce this policy. | `true`
`.spec.allowIngressHostnameCollision` | 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). | `false`
Upon installation using Kustomize or Helm, a `default` resource will be created.
The reference to this configuration is managed by the CLI flag `--configuration-name`.
## Created Resources
Once installed, the Capsule operator creates the following resources in your cluster:
```
NAMESPACE RESOURCE
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io
clusterrole.rbac.authorization.k8s.io/capsule-proxy-role
clusterrole.rbac.authorization.k8s.io/capsule-metrics-reader
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration
validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration
capsule-system clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding
capsule-system clusterrolebinding.rbac.authorization.k8s.io/capsule-proxy-rolebinding
capsule-system secret/capsule-ca
capsule-system secret/capsule-tls
capsule-system service/capsule-controller-manager-metrics-service
capsule-system service/capsule-webhook-service
capsule-system deployment.apps/capsule-controller-manager
```

View File

@@ -0,0 +1,98 @@
# Create namespaces
Alice can create a new namespace in her tenant, as simply:
```
alice@caas# kubectl create ns oil-production
```
> Note that Alice started the name of her namespace with an identifier of her
> tenant: this is not a strict requirement but it is highly suggested because
> it is likely that many different tenants would like to call their namespaces
> as `production`, `test`, or `demo`, etc.
>
> The enforcement of this naming convention is optional and can be controlled by the cluster administrator with the `--force-tenant-prefix` option as an argument of the Capsule controller.
When Alice creates the namespace, the Capsule controller listening for creation and deletion events assigns to Alice the following roles:
```yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: namespace:admin
namespace: oil-production
subjects:
- kind: User
name: alice
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: namespace-deleter
namespace: oil-production
subjects:
- kind: User
name: alice
roleRef:
kind: ClusterRole
name: namespace-deleter
apiGroup: rbac.authorization.k8s.io
```
Alice is the admin of the namespaces:
```
alice@caas# kubectl get rolebindings -n oil-production
NAME ROLE AGE
namespace:admin ClusterRole/admin 9m5s
namespace-deleter ClusterRole/admin 9m5s
```
The said Role Binding resources are automatically created by Capsule when Alice creates a namespace in the tenant.
Alice can deploy any resource in the namespace, according to the predefined
[`admin` cluster role](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles).
```
alice@caas# kubectl -n oil-development run nginx --image=docker.io/nginx
alice@caas# kubectl -n oil-development get pods
```
Alice can create additional namespaces, according to the `namespaceQuota` field of the tenant manifest:
```
alice@caas# kubectl create ns oil-development
alice@caas# kubectl create ns oil-test
```
While Alice creates namespace resources the Capsule controller updates the status of the tenant so Bill, the cluster admin, can check its status:
```
bill@caas# kubectl describe tenant oil
```
```yaml
...
status:
namespaces:
oil-development
oil-production
oil-test
size: 3 # current namespace count
...
```
Once the namespace quota assigned to the tenant has been reached, Alice cannot create further namespaces
```
alice@caas# kubectl create ns oil-training
Error from server (Cannot exceed Namespace quota: please, reach out to the system administrators): admission webhook "quota.namespace.capsule.clastix.io" denied the request.
```
The enforcement on the maximum number of Namespace resources per Tenant is the responsibility of the Capsule controller via its Dynamic Admission Webhook capability.
# Whats next
See how Alice, the tenant owner, can assign different user roles in the tenant. [Assign permissions](./permissions.md).

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