Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba072c88d | ||
|
|
9d02fb39eb | ||
|
|
1df430e71b | ||
|
|
75525ac192 | ||
|
|
132ffd57ea | ||
|
|
7602114835 | ||
|
|
82996c1c83 | ||
|
|
ede96f5cf4 | ||
|
|
2fc1be8bfe | ||
|
|
e0dbf47723 | ||
|
|
fb68795e90 | ||
|
|
a026e2f00c | ||
|
|
413208e7fe | ||
|
|
2771b63c18 | ||
|
|
9a1520ff66 | ||
|
|
c304fb2438 | ||
|
|
6d56237e23 | ||
|
|
c32166ba45 | ||
|
|
e4ecbe30d1 | ||
|
|
3435f5464b | ||
|
|
f216d0bd8d | ||
|
|
f9e7256746 | ||
|
|
5b46e8eb81 | ||
|
|
dd5ed4575e | ||
|
|
f9554d4cae |
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,10 +9,7 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Thanks for taking time reporting a Capsule bug!
|
||||
|
||||
We do our best to keep it reliable and working, so don't hesitate adding
|
||||
as many information as you can and keep in mind you can reach us on our
|
||||
Clastix Slack workspace: https://clastix.slack.com, #capsule channel.
|
||||
|
||||
-->
|
||||
|
||||
# Bug description
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Chat on Slack
|
||||
url: https://kubernetes.slack.com/archives/C03GETTJQRL
|
||||
about: Maybe chatting with the community can help
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -14,8 +14,6 @@ We're trying to build a community drive Open Source project, so don't
|
||||
hesitate proposing your enhancement ideas: keep in mind, since we would like
|
||||
to keep it as agnostic as possible, to motivate all your assumptions.
|
||||
|
||||
If you need to reach the maintainers, please join the Clastix Slack workspace:
|
||||
https://clastix.slack.com, #capsule channel.
|
||||
-->
|
||||
|
||||
# Describe the feature
|
||||
|
||||
10
.github/configs/ct.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
remote: origin
|
||||
target-branch: master
|
||||
chart-dirs:
|
||||
- charts
|
||||
helm-extra-args: "--timeout 600s"
|
||||
validate-chart-schema: false
|
||||
validate-maintainers: false
|
||||
validate-yaml: true
|
||||
exclude-deprecated: true
|
||||
check-version-increment: false
|
||||
43
.github/configs/lintconf.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
---
|
||||
rules:
|
||||
braces:
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 0
|
||||
min-spaces-inside-empty: -1
|
||||
max-spaces-inside-empty: -1
|
||||
brackets:
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 0
|
||||
min-spaces-inside-empty: -1
|
||||
max-spaces-inside-empty: -1
|
||||
colons:
|
||||
max-spaces-before: 0
|
||||
max-spaces-after: 1
|
||||
commas:
|
||||
max-spaces-before: 0
|
||||
min-spaces-after: 1
|
||||
max-spaces-after: 1
|
||||
comments:
|
||||
require-starting-space: true
|
||||
min-spaces-from-content: 1
|
||||
document-end: disable
|
||||
document-start: disable # No --- to start a file
|
||||
empty-lines:
|
||||
max: 2
|
||||
max-start: 0
|
||||
max-end: 0
|
||||
hyphens:
|
||||
max-spaces-after: 1
|
||||
indentation:
|
||||
spaces: consistent
|
||||
indent-sequences: whatever # - list indentation will handle both indentation and without
|
||||
check-multi-line-strings: false
|
||||
key-duplicates: enable
|
||||
line-length: disable # Lines can be any length
|
||||
new-line-at-end-of-file: enable
|
||||
new-lines:
|
||||
type: unix
|
||||
trailing-spaces: enable
|
||||
truthy:
|
||||
level: warning
|
||||
23
.github/maintainers.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
- name: Adriano Pezzuto
|
||||
github: https://github.com/bsctl
|
||||
company: Clastix
|
||||
projects:
|
||||
- https://github.com/clastix/capsule
|
||||
- https://github.com/clastix/capsule-proxy
|
||||
- name: Dario Tranchitella
|
||||
github: https://github.com/prometherion
|
||||
company: Clastix
|
||||
projects:
|
||||
- https://github.com/clastix/capsule
|
||||
- https://github.com/clastix/capsule-proxy
|
||||
- name: Maksim Fedotov
|
||||
github: https://github.com/MaxFedotov
|
||||
company: wargaming.net
|
||||
projects:
|
||||
- https://github.com/clastix/capsule
|
||||
- https://github.com/clastix/capsule-proxy
|
||||
- name: Oliver Bähler
|
||||
github: https://github.com/oliverbaehler
|
||||
company: Bedag Informatik AG
|
||||
projects:
|
||||
- https://github.com/clastix/capsule
|
||||
3
.github/workflows/ci.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
||||
- run: make installer
|
||||
- name: Checking if YAML installer file is not aligned
|
||||
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
|
||||
- run: make apidoc
|
||||
- name: Checking if the CRDs documentation is not aligned
|
||||
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> CRDs generated documentation have not been committed" && git --no-pager diff && exit 1; fi
|
||||
- name: Checking if YAML installer generated untracked files
|
||||
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
|
||||
- name: Checking if source code is not formatted
|
||||
|
||||
18
.github/workflows/gosec.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: CI gosec
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: ./...
|
||||
42
.github/workflows/helm.yml
vendored
@@ -12,11 +12,53 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: 3.3.4
|
||||
- name: Linting Chart
|
||||
run: helm lint ./charts/capsule
|
||||
- name: Setup Chart Linting
|
||||
id: lint
|
||||
uses: helm/chart-testing-action@v2.3.0
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
run: |
|
||||
changed=$(ct list-changed --config ./.github/configs/ct.yaml)
|
||||
if [[ -n "$changed" ]]; then
|
||||
echo "::set-output name=changed::true"
|
||||
fi
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --debug --config ./.github/configs/ct.yaml --lint-conf ./.github/configs/lintconf.yaml
|
||||
- name: Run docs-testing (helm-docs)
|
||||
id: helm-docs
|
||||
run: |
|
||||
make helm-docs
|
||||
if [[ $(git diff --stat) != '' ]]; then
|
||||
echo -e '\033[0;31mDocumentation outdated! (Run make helm-docs locally and commit)\033[0m ❌'
|
||||
git diff --color
|
||||
exit 1
|
||||
else
|
||||
echo -e '\033[0;32mDocumentation up to date\033[0m ✔'
|
||||
fi
|
||||
# Create KIND Cluster
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@v1.2.0
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
# Install Required Operators/CRDs
|
||||
- name: Prepare Cluster Operators/CRDs
|
||||
run: |
|
||||
# Cert-Manager CRDs
|
||||
kubectl create -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.crds.yaml
|
||||
|
||||
# Prometheus CRDs
|
||||
kubectl create -f https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.58.0/bundle.yaml
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
# Install Charts
|
||||
- name: Run chart-testing (install)
|
||||
run: ct install --debug --config ./.github/configs/ct.yaml
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/helm-v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.gitignore
vendored
@@ -30,3 +30,5 @@ bin
|
||||
.DS_Store
|
||||
*.tgz
|
||||
|
||||
capsule
|
||||
|
||||
|
||||
8
ADOPTERS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Adopters
|
||||
|
||||
This is a list of companies that have adopted Capsule, feel free to open a Pull-Request to get yours listed.
|
||||
|
||||
## Adopters list (alphabetically)
|
||||
|
||||
### [Bedag Informatik AG](https://www.bedag.ch/)
|
||||

|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement by contacting
|
||||
one of the [maintainers](https://raw.githubusercontent.com/clastix/capsule/master/.github/maintainers.yaml).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
45
Makefile
@@ -78,6 +78,25 @@ manifests: controller-gen
|
||||
generate: controller-gen
|
||||
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
|
||||
|
||||
apidoc: apidocs-gen
|
||||
$(APIDOCS_GEN) crdoc --resources config/crd/bases --output docs/content/general/tenant-crd.md --template docs/template/reference-cr.tmpl
|
||||
|
||||
# Helm
|
||||
SRC_ROOT = $(shell git rev-parse --show-toplevel)
|
||||
|
||||
helm-docs: HELMDOCS_VERSION := v1.11.0
|
||||
helm-docs: docker
|
||||
@docker run -v "$(SRC_ROOT):/helm-docs" jnorwood/helm-docs:$(HELMDOCS_VERSION) --chart-search-root /helm-docs
|
||||
|
||||
helm-lint: docker
|
||||
@docker run -v "$(SRC_ROOT):/workdir" --entrypoint /bin/sh quay.io/helmpack/chart-testing:v3.3.1 -c "cd /workdir && ct lint --config .github/configs/ct.yaml --lint-conf .github/configs/lintconf.yaml --all --debug"
|
||||
|
||||
docker:
|
||||
@hash docker 2>/dev/null || {\
|
||||
echo "You need docker" &&\
|
||||
exit 1;\
|
||||
}
|
||||
|
||||
# Setup development env
|
||||
# Usage:
|
||||
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
|
||||
@@ -115,7 +134,7 @@ dev-setup:
|
||||
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
|
||||
kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]" && \
|
||||
kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
@@ -123,11 +142,11 @@ dev-setup:
|
||||
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/ingresses\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespaces\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/networkpolicies\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]";
|
||||
|
||||
# Build the docker image
|
||||
@@ -147,9 +166,13 @@ CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
|
||||
controller-gen: ## Download controller-gen locally if necessary.
|
||||
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0)
|
||||
|
||||
APIDOCS_GEN = $(shell pwd)/bin/crdoc
|
||||
apidocs-gen: ## Download crdoc locally if necessary.
|
||||
$(call go-install-tool,$(APIDOCS_GEN),fybrik.io/crdoc@latest)
|
||||
|
||||
GINKGO = $(shell pwd)/bin/ginkgo
|
||||
ginkgo: ## Download ginkgo locally if necessary.
|
||||
$(call go-install-tool,$(KUSTOMIZE),github.com/onsi/ginkgo/ginkgo@v1.16.5)
|
||||
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/ginkgo@v1.16.5)
|
||||
|
||||
KUSTOMIZE = $(shell pwd)/bin/kustomize
|
||||
kustomize: ## Download kustomize locally if necessary.
|
||||
@@ -198,6 +221,9 @@ golint:
|
||||
# Running e2e tests in a KinD instance
|
||||
.PHONY: e2e
|
||||
e2e/%: ginkgo
|
||||
$(MAKE) e2e-build/$* && $(MAKE) e2e-exec || $(MAKE) e2e-destroy
|
||||
|
||||
e2e-build/%:
|
||||
kind create cluster --name capsule --image=kindest/node:$*
|
||||
make docker-build
|
||||
kind load docker-image --nodes capsule-control-plane --name capsule $(IMG)
|
||||
@@ -213,5 +239,10 @@ e2e/%: ginkgo
|
||||
--set 'manager.readinessProbe.failureThreshold=10' \
|
||||
capsule \
|
||||
./charts/capsule
|
||||
|
||||
e2e-exec:
|
||||
$(GINKGO) -v -tags e2e ./e2e
|
||||
|
||||
e2e-destroy:
|
||||
kind delete cluster --name capsule
|
||||
|
||||
|
||||
22
README.md
@@ -72,22 +72,38 @@ Capsule is Open Source with Apache 2 license and any contribution is welcome.
|
||||
|
||||
## Chart Development
|
||||
|
||||
The documentation for each chart is done with [helm-docs](https://github.com/norwoodj/helm-docs). This way we can ensure that values are consistent with the chart documentation.
|
||||
### Chart Linting
|
||||
|
||||
We have a script on the repository which will execute the helm-docs docker container, so that you don't have to worry about downloading the binary etc. Simply execute the script (Bash compatible):
|
||||
The chart is linted with [ct](https://github.com/helm/chart-testing). You can run the linter locally with this command:
|
||||
|
||||
```
|
||||
bash scripts/helm-docs.sh
|
||||
make helm-lint
|
||||
```
|
||||
|
||||
### Chart Documentation
|
||||
|
||||
The documentation for each chart is done with [helm-docs](https://github.com/norwoodj/helm-docs). This way we can ensure that values are consistent with the chart documentation. Run this anytime you make changes to a `values.yaml` file:
|
||||
|
||||
```
|
||||
make helm-docs
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
Join the community, share and learn from it. You can find all the resources to how to contribute code and docs, connect with people in the [community repository](https://github.com/clastix/capsule-community).
|
||||
|
||||
## Adopters
|
||||
|
||||
See the [ADOPTERS.md](ADOPTERS.md) file for a list of companies that are using Capsule.
|
||||
|
||||
# Governance
|
||||
|
||||
You can find how the Capsule project is governed [here](https://capsule.clastix.io/docs/contributing/governance).
|
||||
|
||||
## Maintainers
|
||||
|
||||
Please, refer to the maintainers file available [here](.github/maintainers.yaml).
|
||||
|
||||
# FAQ
|
||||
|
||||
- Q. How to pronounce Capsule?
|
||||
|
||||
@@ -21,8 +21,8 @@ sources:
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.11
|
||||
version: 0.1.12
|
||||
|
||||
# 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.1.2
|
||||
appVersion: 0.1.3
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
docs: HELMDOCS_VERSION := v1.8.1
|
||||
docs: docker
|
||||
@docker run --rm -v "$$(pwd):/helm-docs" -u $$(id -u) jnorwood/helm-docs:$(HELMDOCS_VERSION)
|
||||
|
||||
docker:
|
||||
@hash docker 2>/dev/null || {\
|
||||
echo "You need docker" &&\
|
||||
exit 1;\
|
||||
}
|
||||
@@ -64,8 +64,8 @@ Here the values you can override:
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | Set affinity rules for the Capsule pod |
|
||||
| certManager.generateCertificates | bool | `false` | Specifies whether capsule webhooks certificates should be generated using cert-manager |
|
||||
| customAnnotations | object | `{}` | Additional annotations which will be added to all resources created by Capsule helm chart |
|
||||
| customLabels | object | `{}` | Additional labels which will be added to all resources created by Capsule helm chart |
|
||||
| customAnnotations | object | `{}` | Additional annotations which will be added to all resources created by Capsule helm chart |
|
||||
| customLabels | object | `{}` | Additional labels which will be added to all resources created by Capsule helm chart |
|
||||
| jobs.image.pullPolicy | string | `"IfNotPresent"` | Set the image pull policy of the helm chart job |
|
||||
| jobs.image.repository | string | `"quay.io/clastix/kubectl"` | Set the image repository of the helm chart job |
|
||||
| jobs.image.tag | string | `""` | Set the image tag of the helm chart job |
|
||||
@@ -88,18 +88,18 @@ Here the values you can override:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| manager.hostNetwork | bool | `false` | Specifies if the container should be started in hostNetwork mode. Required for use in some managed kubernetes clusters (such as AWS EKS) with custom CNI (such as calico), because control-plane managed by AWS cannot communicate with pods' IP CIDR and admission webhooks are not working |
|
||||
| manager.hostNetwork | bool | `false` | Specifies if the container should be started in hostNetwork mode. Required for use in some managed kubernetes clusters (such as AWS EKS) with custom CNI (such as calico), because control-plane managed by AWS cannot communicate with pods' IP CIDR and admission webhooks are not working |
|
||||
| manager.image.pullPolicy | string | `"IfNotPresent"` | Set the image pull policy. |
|
||||
| manager.image.repository | string | `"clastix/capsule"` | Set the image repository of the capsule. |
|
||||
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| manager.imagePullSecrets | list | `[]` | Configuration for `imagePullSecrets` so that you can use a private images registry. |
|
||||
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
|
||||
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
|
||||
| manager.options.capsuleUserGroups | list | `["capsule.clastix.io"]` | Override the Capsule user groups |
|
||||
| manager.imagePullSecrets | list | `[]` | Configuration for `imagePullSecrets` so that you can use a private images registry. |
|
||||
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
|
||||
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
|
||||
| manager.options.capsuleUserGroups | list | `["capsule.clastix.io"]` | Override the Capsule user groups |
|
||||
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
|
||||
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
|
||||
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
|
||||
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
|
||||
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
|
||||
| manager.readinessProbe | object | `{"httpGet":{"path":"/readyz","port":10080}}` | Configure the readiness probe using Deployment probe spec |
|
||||
| manager.resources.limits.cpu | string | `"200m"` | |
|
||||
| manager.resources.limits.memory | string | `"128Mi"` | |
|
||||
|
||||
@@ -14,7 +14,7 @@ tls:
|
||||
# Manager Options
|
||||
manager:
|
||||
|
||||
# -- Set the controller deployment mode as `Deployment` or `DaemonSet`.
|
||||
# -- Set the controller deployment mode as `Deployment` or `DaemonSet`.
|
||||
kind: Deployment
|
||||
|
||||
image:
|
||||
@@ -25,7 +25,7 @@ manager:
|
||||
# -- Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ''
|
||||
|
||||
# -- Configuration for `imagePullSecrets` so that you can use a private images registry.
|
||||
# -- Configuration for `imagePullSecrets` so that you can use a private images registry.
|
||||
imagePullSecrets: []
|
||||
|
||||
# -- Specifies if the container should be started in hostNetwork mode.
|
||||
@@ -41,19 +41,19 @@ manager:
|
||||
logLevel: '4'
|
||||
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
|
||||
forceTenantPrefix: false
|
||||
# -- Override the Capsule user groups
|
||||
# -- Override the Capsule user groups
|
||||
capsuleUserGroups: ["capsule.clastix.io"]
|
||||
# -- If specified, disallows creation of namespaces matching the passed regexp
|
||||
# -- If specified, disallows creation of namespaces matching the passed regexp
|
||||
protectedNamespaceRegex: ""
|
||||
# -- Specifies whether capsule webhooks certificates should be generated by capsule operator
|
||||
generateCertificates: true
|
||||
|
||||
# -- Configure the liveness probe using Deployment probe spec
|
||||
# -- Configure the liveness probe using Deployment probe spec
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 10080
|
||||
|
||||
|
||||
# -- Configure the readiness probe using Deployment probe spec
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
@@ -75,7 +75,7 @@ podAnnotations: {}
|
||||
# scheduler.alpha.kubernetes.io/critical-pod: ''
|
||||
|
||||
# -- Set the priority class name of the Capsule pod
|
||||
priorityClassName: '' #system-cluster-critical
|
||||
priorityClassName: '' # system-cluster-critical
|
||||
|
||||
# -- Set the node selector for the Capsule pod
|
||||
nodeSelector: {}
|
||||
@@ -83,10 +83,10 @@ nodeSelector: {}
|
||||
|
||||
# -- Set list of tolerations for the Capsule pod
|
||||
tolerations: []
|
||||
#- key: CriticalAddonsOnly
|
||||
# operator: Exists
|
||||
#- effect: NoSchedule
|
||||
# key: node-role.kubernetes.io/master
|
||||
# - key: CriticalAddonsOnly
|
||||
# operator: Exists
|
||||
# - effect: NoSchedule
|
||||
# key: node-role.kubernetes.io/master
|
||||
|
||||
# -- Set the replica count for capsule pod
|
||||
replicaCount: 1
|
||||
@@ -120,10 +120,10 @@ certManager:
|
||||
# -- Specifies whether capsule webhooks certificates should be generated using cert-manager
|
||||
generateCertificates: false
|
||||
|
||||
# -- Additional labels which will be added to all resources created by Capsule helm chart
|
||||
# -- Additional labels which will be added to all resources created by Capsule helm chart
|
||||
customLabels: {}
|
||||
|
||||
# -- Additional annotations which will be added to all resources created by Capsule helm chart
|
||||
# -- Additional annotations which will be added to all resources created by Capsule helm chart
|
||||
customAnnotations: {}
|
||||
|
||||
# Webhooks configurations
|
||||
|
||||
@@ -1411,7 +1411,7 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
image: clastix/capsule:v0.1.2
|
||||
image: clastix/capsule:v0.1.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: manager
|
||||
ports:
|
||||
|
||||
@@ -7,4 +7,4 @@ kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: clastix/capsule
|
||||
newTag: v0.1.2
|
||||
newTag: v0.1.3
|
||||
|
||||
@@ -38,7 +38,10 @@ func (r *abstractServiceLabelsReconciler) InjectClient(c client.Client) error {
|
||||
func (r *abstractServiceLabelsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
|
||||
tenant, err := r.getTenant(ctx, request.NamespacedName, r.client)
|
||||
if err != nil {
|
||||
if errors.As(err, &NonTenantObjectError{}) || errors.As(err, &NoServicesMetadataError{}) {
|
||||
noTenantObjError := &NonTenantObjectError{}
|
||||
noSvcMetaError := &NoServicesMetadataError{}
|
||||
|
||||
if errors.As(err, &noTenantObjError) || errors.As(err, &noSvcMetaError) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
"github.com/clastix/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
||||
@@ -72,11 +73,7 @@ func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, t
|
||||
}
|
||||
|
||||
if tnt.Spec.NodeSelector != nil {
|
||||
var selector []string
|
||||
for k, v := range tnt.Spec.NodeSelector {
|
||||
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
|
||||
annotations = utils.BuildNodeSelector(tnt, annotations)
|
||||
}
|
||||
|
||||
if tnt.Spec.IngressOptions.AllowedClasses != nil {
|
||||
|
||||
@@ -115,6 +115,12 @@ upstream https://github.com/clastix/capsule.git (fetch)
|
||||
upstream https://github.com/clastix/capsule.git (push)
|
||||
```
|
||||
|
||||
Pull all tags
|
||||
|
||||
```
|
||||
$ git fetch --all && git pull upstream
|
||||
```
|
||||
|
||||
Build and deploy:
|
||||
|
||||
```shell
|
||||
|
||||
@@ -17,6 +17,8 @@ In the context of Capsule project, we consider the following roles:
|
||||
|
||||
The release process will be governed by Maintainers.
|
||||
|
||||
Please, refer to the [maintainers file](https://github.com/clastix/capsule/blob/master/.github/blob/master/maintainers.yaml) available in the source code.
|
||||
|
||||
## Roadmap Planning
|
||||
|
||||
Maintainers will share roadmap and release versions as milestones in GitHub.
|
||||
@@ -4,143 +4,9 @@ Reference document for Capsule Operator configuration
|
||||
|
||||
## Custom Resource Definition
|
||||
|
||||
Capsule operator uses a Custom Resources Definition (CRD) for _Tenants_. Tenants are cluster wide resources, so you need cluster level permissions to work with tenants. You can learn about tenant CRD by the `kubectl explain` command:
|
||||
|
||||
```
|
||||
kubectl explain tenant
|
||||
|
||||
KIND: Tenant
|
||||
VERSION: capsule.clastix.io/v1beta1
|
||||
|
||||
DESCRIPTION:
|
||||
Tenant is the Schema for the tenants API
|
||||
|
||||
FIELDS:
|
||||
apiVersion <string>
|
||||
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
|
||||
|
||||
kind <string>
|
||||
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
|
||||
|
||||
metadata <Object>
|
||||
Standard object's metadata. More info:
|
||||
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
|
||||
spec <Object>
|
||||
TenantSpec defines the desired state of Tenant
|
||||
|
||||
status <Object>
|
||||
Returns the observed state of the Tenant
|
||||
```
|
||||
|
||||
For Tenant spec:
|
||||
|
||||
```
|
||||
kubectl explain tenant.spec
|
||||
|
||||
KIND: Tenant
|
||||
VERSION: capsule.clastix.io/v1beta1
|
||||
|
||||
RESOURCE: spec <Object>
|
||||
|
||||
DESCRIPTION:
|
||||
TenantSpec defines the desired state of Tenant
|
||||
|
||||
FIELDS:
|
||||
additionalRoleBindings <[]Object>
|
||||
Specifies additional RoleBindings assigned to the Tenant. Capsule will
|
||||
ensure that all namespaces in the Tenant always contain the RoleBinding for
|
||||
the given ClusterRole. Optional.
|
||||
|
||||
containerRegistries <Object>
|
||||
Specifies the trusted Image Registries assigned to the Tenant. Capsule
|
||||
assures that all Pods resources created in the Tenant can use only one of
|
||||
the allowed trusted registries. Optional.
|
||||
|
||||
imagePullPolicies <[]string>
|
||||
Specify the allowed values for the imagePullPolicies option in Pod
|
||||
resources. Capsule assures that all Pod resources created in the Tenant can
|
||||
use only one of the allowed policy. Optional.
|
||||
|
||||
ingressOptions <Object>
|
||||
Specifies options for the Ingress resources, such as allowed hostnames and
|
||||
IngressClass. Optional.
|
||||
|
||||
limitRanges <Object>
|
||||
Specifies the resource min/max usage restrictions to the Tenant. The assigned
|
||||
values are inherited by any namespace created in the Tenant. Optional.
|
||||
|
||||
namespaceOptions <Object>
|
||||
Specifies options for the Namespaces, such as additional metadata or
|
||||
maximum number of namespaces allowed for that Tenant. Once the namespace
|
||||
quota assigned to the Tenant has been reached, the Tenant owner cannot
|
||||
create further namespaces. Optional.
|
||||
|
||||
networkPolicies <Object>
|
||||
Specifies the NetworkPolicies assigned to the Tenant. The assigned
|
||||
NetworkPolicies are inherited by any namespace created in the Tenant.
|
||||
Optional.
|
||||
|
||||
nodeSelector <map[string]string>
|
||||
Specifies the label to control the placement of pods on a given pool of
|
||||
worker nodes. All namesapces created within the Tenant will have the node
|
||||
selector annotation. This annotation tells the Kubernetes scheduler to
|
||||
place pods on the nodes having the selector label. Optional.
|
||||
|
||||
owners <[]Object> -required-
|
||||
Specifies the owners of the Tenant. Mandatory.
|
||||
|
||||
priorityClasses <Object>
|
||||
Specifies the allowed priorityClasses assigned to the Tenant. Capsule
|
||||
assures that all pods created in the Tenant can use only one
|
||||
of the allowed priorityClasses. Optional.
|
||||
|
||||
resourceQuotas <Object>
|
||||
Specifies a list of ResourceQuota resources assigned to the Tenant. The
|
||||
assigned values are inherited by any namespace created in the Tenant. The
|
||||
Capsule operator aggregates ResourceQuota at Tenant level, so that the hard
|
||||
quota is never crossed for the given Tenant. This permits the Tenant owner
|
||||
to consume resources in the Tenant regardless of the namespace. Optional.
|
||||
|
||||
serviceOptions <Object>
|
||||
Specifies options for the Service, such as additional metadata or block of
|
||||
certain type of Services. Optional.
|
||||
|
||||
storageClasses <Object>
|
||||
Specifies the allowed StorageClasses assigned to the Tenant. Capsule
|
||||
assures that all PersistentVolumeClaim resources created in the Tenant can
|
||||
use only one of the allowed StorageClasses. Optional.
|
||||
```
|
||||
|
||||
and Tenant status:
|
||||
|
||||
```
|
||||
kubectl explain tenant.status
|
||||
KIND: Tenant
|
||||
VERSION: capsule.clastix.io/v1beta1
|
||||
|
||||
RESOURCE: status <Object>
|
||||
|
||||
DESCRIPTION:
|
||||
Returns the observed state of the Tenant
|
||||
|
||||
FIELDS:
|
||||
namespaces <[]string>
|
||||
List of namespaces assigned to the Tenant.
|
||||
|
||||
size <integer> -required-
|
||||
How many namespaces are assigned to the Tenant.
|
||||
|
||||
state <string> -required-
|
||||
The operational state of the Tenant. Possible values are "Active",
|
||||
"Cordoned".
|
||||
```
|
||||
Capsule operator uses a Custom Resources Definition (CRD) for _Tenants_.
|
||||
Tenants are cluster wide resources, so you need cluster level permissions to work with tenants.
|
||||
You can learn about tenant CRDs in the following [section](./tenant-crd)
|
||||
|
||||
## Capsule Configuration
|
||||
|
||||
@@ -239,4 +105,4 @@ 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
|
||||
```
|
||||
```
|
||||
3378
docs/content/general/tenant-crd.md
Normal file
@@ -2,108 +2,21 @@
|
||||
|
||||
Capsule is a framework to implement multi-tenant and policy-driven scenarios in Kubernetes. In this tutorial, we'll focus on a hypothetical case covering the main features of the Capsule Operator.
|
||||
|
||||
***Acme Corp***, our sample organization, is building a Container as a Service platform (CaaS) to serve multiple lines of business. Each line of business has its team of engineers that are responsible for the development, deployment, and operating of their digital products. We'll work with the following actors:
|
||||
***Acme Corp***, our sample organization, is building a Container as a Service platform (CaaS) to serve multiple lines of business, or departments, e.g. _Oil_, _Gas_, _Solar_, _Wind_, _Water_. Each department has its team of engineers that are responsible for the development, deployment, and operating of their digital products. We'll work with the following actors:
|
||||
|
||||
* ***Bill***: the cluster administrator from the operations department of Acme Corp.
|
||||
* ***Bill***: the cluster administrator from the operations department of _Acme Corp_.
|
||||
|
||||
* ***Alice***: the IT Project Leader in the Oil & Gas Business Units. She is responsible for a team made of different job responsibilities (developers, administrators, SRE engineers, etc.) working in separate multiple departments.
|
||||
* ***Alice***: the project leader in the _Oil_ & _Gas_ departments. She is responsible for a team made of different job responsibilities: e.g. developers, administrators, SRE engineers, etc.
|
||||
|
||||
* ***Joe***:
|
||||
He works at Acme Corp, as a lead developer of a distributed team in Alice's organization.
|
||||
* ***Joe***: works as a lead developer of a distributed team in Alice's organization.
|
||||
|
||||
* ***Bob***:
|
||||
He is the head of Engineering for the Water Business Unit, the main and historical line of business at Acme Corp.
|
||||
* ***Bob***: is the head of engineering for the _Water_ department, the main and historical line of business at _Acme Corp_.
|
||||
|
||||
|
||||
## Assign Tenant ownership
|
||||
|
||||
### Roles assigned to Tenant Owners
|
||||
|
||||
By default, all Tenant Owners will be granted with two ClusterRole resources using the RoleBinding API:
|
||||
|
||||
1. the Kubernetes default one, `admin`, that grants most of the Namespace scoped resources management operations
|
||||
2. a custom one, named `capsule-namespace-deleter`, allowing to delete the created Namespace
|
||||
|
||||
In the example below, assuming Alice create a namespace `oil-production` in Tenant `oil`,getting the tenant owner's Alice default
|
||||
ClusterRoles command:
|
||||
|
||||
```
|
||||
$: kubectl get rolebindings.rbac.authorization.k8s.io -n oil-production
|
||||
NAME ROLE AGE
|
||||
capsule-oil-0-admin ClusterRole/admin 6s
|
||||
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
|
||||
capsule-oil-2-admin ClusterRole/admin 5s
|
||||
capsule-oil-3-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
|
||||
```
|
||||
|
||||
Capsule supports the dynamic management of the assigned ClusterRole resources for each Tenant Owner.
|
||||
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
annotations:
|
||||
clusterrolenames.capsule.clastix.io/user.alice: editor,manager
|
||||
clusterrolenames.capsule.clastix.io/group.sre: readonly
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- kind: User
|
||||
name: alice
|
||||
- kind: Group
|
||||
name: sre
|
||||
```
|
||||
|
||||
For the given configuration, the resulting RoleBinding resources are the following ones:
|
||||
|
||||
```
|
||||
$: kubectl get rolebindings.rbac.authorization.k8s.io
|
||||
NAME ROLE AGE
|
||||
capsule-oil-0-editor ClusterRole/editor 21s
|
||||
capsule-oil-1-manager ClusterRole/manager 19s
|
||||
capsule-oil-2-readonly ClusterRole/readonly 2s
|
||||
```
|
||||
|
||||
> The pattern for the annotation is `clusterrolenames.capsule.clastix.io/${KIND}.${NAME}`.
|
||||
> The placeholders `${KIND}` and `${NAME}` are referring to the Tenant Owner specification fields, both lower-cased.
|
||||
>
|
||||
> In the case of users that are identified using their email address, the symbol `@` wouldn't be supported by the RFC 1123.
|
||||
> For such cases, the `@` symbol can be replaced with the placeholder `__AT__`.
|
||||
>
|
||||
> ```yaml
|
||||
> apiVersion: capsule.clastix.io/v1beta1
|
||||
> kind: Tenant
|
||||
> metadata:
|
||||
> annotations:
|
||||
> clusterrolenames.capsule.clastix.io/alice__AT__clastix.io: editor,manager
|
||||
> spec:
|
||||
> owners:
|
||||
> - kind: User
|
||||
> name: alice@org.tld
|
||||
> - kind: User
|
||||
> name: alice@clastix.io
|
||||
> ```
|
||||
>
|
||||
> Instead, with the resulting annotation key exceeding 63 characters length, the zero-based index of the owner can be specified as follows:
|
||||
>
|
||||
> ```yaml
|
||||
> apiVersion: capsule.clastix.io/v1beta1
|
||||
> kind: Tenant
|
||||
> metadata:
|
||||
> annotations:
|
||||
> clusterrolenames.capsule.clastix.io/1: editor,manager
|
||||
> spec:
|
||||
> owners:
|
||||
> - kind: User
|
||||
> name: alice@org.tld
|
||||
> - kind: User
|
||||
> name: very-long-user-name-that-breaks-rfc-1123@org.tld
|
||||
> ```
|
||||
>
|
||||
> This latter example will assign the roles `editor` and `manager`, assigned to the user `very-long-user-name-that-breaks-rfc-1123@org.tld`.
|
||||
|
||||
### User as tenant owner
|
||||
Bill, the cluster admin, receives a new request from Acme Corp.'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the Acme Corp. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
|
||||
Bill, the cluster admin, receives a new request from _Acme Corp_'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the _Acme Corp_. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
|
||||
|
||||
To keep things simple, we assume that Bill just creates a client certificate for authentication using X.509 Certificate Signing Request, so Alice's certificate has `"/CN=alice/O=capsule.clastix.io"`.
|
||||
|
||||
@@ -226,15 +139,15 @@ metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: system:serviceaccount:default:robot
|
||||
- name: system:serviceaccount:tenant-system:robot
|
||||
kind: ServiceAccount
|
||||
EOF
|
||||
```
|
||||
|
||||
Bill can create a Service Account called `robot`, for example, in the `default` namespace and leave it to act as Tenant Owner of the `oil` tenant
|
||||
Bill can create a Service Account called `robot`, for example, in the `tenant-system` namespace and leave it to act as Tenant Owner of the `oil` tenant
|
||||
|
||||
```
|
||||
kubectl --as system:serviceaccount:default:robot --as-group capsule.clastix.io auth can-i create namespaces
|
||||
kubectl --as system:serviceaccount:tenant-system:robot --as-group capsule.clastix.io auth can-i create namespaces
|
||||
yes
|
||||
```
|
||||
|
||||
@@ -247,7 +160,7 @@ metadata:
|
||||
name: default
|
||||
spec:
|
||||
userGroups:
|
||||
- system:serviceaccounts:default
|
||||
- system:serviceaccounts:tenant-system
|
||||
```
|
||||
|
||||
since each service account in a namespace is a member of following group:
|
||||
@@ -256,21 +169,43 @@ since each service account in a namespace is a member of following group:
|
||||
system:serviceaccounts:{service-account-namespace}
|
||||
```
|
||||
|
||||
You can change the CapsuleConfiguration at install time with a helm parameter:
|
||||
```
|
||||
helm upgrade -i \
|
||||
capsule \
|
||||
clastix/capsule \
|
||||
-n capsule-system \
|
||||
--set manager.options.capsuleUserGroups=system:serviceaccounts:tenant-system \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
Or after installation:
|
||||
```
|
||||
kubectl patch capsuleconfigurations default \
|
||||
--patch '{"spec":{"userGroups":["capsule.clastix.io","system:serviceaccounts:tenant-system"]}}' \
|
||||
--type=merge
|
||||
```
|
||||
|
||||
> Please, pay attention when setting a service account acting as tenant owner. Make sure you're not using the group `system:serviceaccounts` or the group `system:serviceaccounts:{capsule-namespace}` as Capsule group, otherwise you'll create a short-circuit in the Capsule controller, being Capsule itself controlled by a serviceaccount.
|
||||
|
||||
### Roles assigned to Tenant Owners
|
||||
|
||||
## Create namespaces
|
||||
Alice, once logged with her credentials, can create a new namespace in her tenant, as simply issuing:
|
||||
By default, all Tenant Owners will be granted with two ClusterRole resources using the RoleBinding API:
|
||||
|
||||
1. the Kubernetes default one, [`admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles), that grants most of the namespace scoped resources
|
||||
|
||||
2. a custom one, created by Capsule, named `capsule-namespace-deleter`, allowing to delete the created namespaces
|
||||
|
||||
In the example below, assuming the tenant owner creates a namespace `oil-production` in Tenant `oil`, you'll see the Role Bindings giving the tenant owner full permissions on the tenant namespaces:
|
||||
|
||||
```
|
||||
kubectl create ns oil-production
|
||||
$: kubectl get rolebindings -n oil-production
|
||||
NAME ROLE AGE
|
||||
capsule-oil-0-admin ClusterRole/admin 6s
|
||||
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
|
||||
```
|
||||
|
||||
Alice started the name of the namespace prepended by the name of the 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 `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:
|
||||
When Alice creates the namespaces, the Capsule controller assigns to Alice the following permissions, so that Alice can act as the admin of all the tenant namespaces.
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -301,28 +236,191 @@ roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
So Alice is the admin of the namespaces:
|
||||
In some cases, the cluster admin needs to narrow the range of permissions assigned to tenant owners by assigning a Cluster Role with less permissions than above. Capsule supports the dynamic assignment of any ClusterRole resources for each Tenant Owner.
|
||||
|
||||
For example, assign user `Joe` the tenant ownership with only [view](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) permissions on tenant namespaces:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
annotations:
|
||||
clusterrolenames.capsule.clastix.io/user.joe: view
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
- name: joe
|
||||
kind: User
|
||||
EOF
|
||||
```
|
||||
|
||||
you'll see the new Role Bindings assigned to Joe:
|
||||
|
||||
```
|
||||
kubectl get rolebindings -n oil-development
|
||||
kubectl -n oil-production get rolebindings
|
||||
NAME ROLE AGE
|
||||
capsule-oil-0-admin ClusterRole/admin 5s
|
||||
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 4s
|
||||
capsule-oil-0-admin ClusterRole/admin 8d
|
||||
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 8d
|
||||
capsule-oil-2-view ClusterRole/edit 5s
|
||||
```
|
||||
|
||||
The said Role Binding resources are automatically created by Capsule controller when the tenant owner Alice creates a namespace in the tenant.
|
||||
so that Joe can only view resources in the tenant namespaces:
|
||||
|
||||
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).
|
||||
```
|
||||
kubectl --as joe --as-group capsule.clastix.io auth can-i delete pods -n oil-marketing
|
||||
no
|
||||
```
|
||||
|
||||
> Please, note that, despite created with more restricted permissions, a tenant owner can still create namespaces in the tenant because he belongs to the `capsule.clastix.io` group. If you want a user not acting as tenant owner, but still operating in the tenant, you can assign additional `RoleBindings` without assigning him the tenant ownership.
|
||||
|
||||
Custom ClusterRoles are also supported. Assuming the cluster admin creates:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: prometheus-servicemonitors-viewer
|
||||
rules:
|
||||
- apiGroups: ["monitoring.coreos.com"]
|
||||
resources: ["servicemonitors"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
EOF
|
||||
```
|
||||
|
||||
These permissions can be granted to Joe
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
annotations:
|
||||
clusterrolenames.capsule.clastix.io/user.joe: view,prometheus-servicemonitors-viewer
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
- name: joe
|
||||
kind: User
|
||||
EOF
|
||||
```
|
||||
|
||||
For the given configuration, the resulting RoleBinding resources are the following ones:
|
||||
|
||||
```
|
||||
kubectl -n oil-production get rolebindings
|
||||
NAME ROLE AGE
|
||||
capsule-oil-0-admin ClusterRole/admin 8d
|
||||
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 8d
|
||||
capsule-oil-2-view ClusterRole/view 11m
|
||||
capsule-oil-3-prometheus-servicemonitors-viewer ClusterRole/prometheus-servicemonitors-viewer 18s
|
||||
```
|
||||
|
||||
> The pattern for the annotation is `clusterrolenames.capsule.clastix.io/${KIND}.${NAME}`.
|
||||
> The placeholders `${KIND}` and `${NAME}` are referring to the Tenant Owner specification fields, both lower-cased.
|
||||
>
|
||||
> In the case of users that are identified using their email address, the symbol `@` wouldn't be supported by the RFC 1123.
|
||||
> For such cases, the `@` symbol can be replaced with the placeholder `__AT__`.
|
||||
>
|
||||
> ```yaml
|
||||
> apiVersion: capsule.clastix.io/v1beta1
|
||||
> kind: Tenant
|
||||
> metadata:
|
||||
> annotations:
|
||||
> clusterrolenames.capsule.clastix.io/alice__AT__clastix.io: editor,manager
|
||||
> spec:
|
||||
> owners:
|
||||
> - kind: User
|
||||
> name: alice@org.tld
|
||||
> - kind: User
|
||||
> name: alice@clastix.io
|
||||
> ```
|
||||
>
|
||||
> Instead, with the resulting annotation key exceeding 63 characters length, the zero-based index of the owner can be specified as follows:
|
||||
>
|
||||
> ```yaml
|
||||
> apiVersion: capsule.clastix.io/v1beta1
|
||||
> kind: Tenant
|
||||
> metadata:
|
||||
> annotations:
|
||||
> clusterrolenames.capsule.clastix.io/1: editor,manager
|
||||
> spec:
|
||||
> owners:
|
||||
> - kind: User
|
||||
> name: alice@org.tld
|
||||
> - kind: User
|
||||
> name: very-long-user-name-that-breaks-rfc-1123@org.tld
|
||||
> ```
|
||||
>
|
||||
> This latter example will assign the roles `editor` and `manager`, assigned to the user `very-long-user-name-that-breaks-rfc-1123@org.tld`.
|
||||
|
||||
|
||||
### Assign additional Role Bindings
|
||||
The tenant owner acts as admin of tenant namespaces. Other users can operate inside the tenant namespaces with different levels of permissions and authorizations.
|
||||
|
||||
Assuming the cluster admin creates:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: prometheus-servicemonitors-viewer
|
||||
rules:
|
||||
- apiGroups: ["monitoring.coreos.com"]
|
||||
resources: ["servicemonitors"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
EOF
|
||||
```
|
||||
|
||||
These permissions can be granted to a user without giving the role of tenant owner:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
additionalRoleBindings:
|
||||
- clusterRoleName: 'prometheus-servicemonitors-viewer'
|
||||
subjects:
|
||||
- apiGroup: rbac.authorization.k8s.io
|
||||
kind: User
|
||||
name: joe
|
||||
EOF
|
||||
```
|
||||
|
||||
## Create namespaces
|
||||
Alice, once logged with her credentials, can create a new namespace in her tenant, as simply issuing:
|
||||
|
||||
```
|
||||
kubectl create ns oil-production
|
||||
```
|
||||
|
||||
Alice started the name of the namespace prepended by the name of the 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 `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.
|
||||
|
||||
Alice can deploy any resource in any of the namespaces
|
||||
|
||||
```
|
||||
kubectl -n oil-development run nginx --image=docker.io/nginx
|
||||
kubectl -n oil-development get pods
|
||||
```
|
||||
|
||||
Bill, the cluster admin, can control how many namespaces Alice, creates by setting a quota in the tenant manifest `spec.namespaceOptions.quota`
|
||||
The cluster admin, can control how many namespaces Alice, creates by setting a quota in the tenant manifest `spec.namespaceOptions.quota`
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
@@ -333,6 +431,7 @@ spec:
|
||||
kind: User
|
||||
namespaceOptions:
|
||||
quota: 3
|
||||
EOF
|
||||
```
|
||||
|
||||
Alice can create additional namespaces according to the quota:
|
||||
@@ -368,58 +467,6 @@ admission webhook "namespace.capsule.clastix.io" denied the request.
|
||||
```
|
||||
The enforcement on the maximum number of namespaces per Tenant is the responsibility of the Capsule controller via its Dynamic Admission Webhook capability.
|
||||
|
||||
## Assign permissions
|
||||
Alice acts as the tenant admin. Other users can operate inside the tenant with different levels of permissions and authorizations. Alice is responsible for creating additional roles and assigning these roles to other users to work in the same tenant.
|
||||
|
||||
One of the key design principles of the Capsule is self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill does not have to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
|
||||
|
||||
Capsule leaves Alice, and the other tenant owners, the freedom to create RBAC roles at the namespace level, or using the pre-defined cluster roles already available in Kubernetes. Since roles and rolebindings are limited to a namespace scope, Alice can assign the roles to the other users accessing the same tenant only after the namespace is created. This gives Alice the power to administer the tenant without the intervention of the cluster admin.
|
||||
|
||||
From the cluster admin perspective, the only required action for Bill is to provide the other identities, eg. `joe` in the Identity Management system. This task can be done once when onboarding the tenant and the number of users accessing the tenant can be part of the tenant business profile.
|
||||
|
||||
Alice can create Roles and RoleBindings only in the namespaces she owns
|
||||
|
||||
```
|
||||
kubectl auth can-i get roles -n oil-development
|
||||
yes
|
||||
|
||||
kubectl auth can-i get rolebindings -n oil-development
|
||||
yes
|
||||
```
|
||||
|
||||
so she can assign the role of namespace `oil-development` admin to Joe, another user accessing the tenant `oil`
|
||||
|
||||
```yaml
|
||||
kubectl --as alice --as-group capsule.clastix.io apply -f - << EOF
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
name: oil-development:admin
|
||||
namespace: oil-development
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: admin
|
||||
subjects:
|
||||
- apiGroup: rbac.authorization.k8s.io
|
||||
kind: User
|
||||
name: joe
|
||||
EOF
|
||||
```
|
||||
|
||||
Joe now can operate on the namespace `oil-development` as admin but he has no access to the other namespaces `oil-production`, and `oil-test` that are part of the same tenant:
|
||||
|
||||
```
|
||||
kubectl --as joe --as-group capsule.clastix.io auth can-i create pod -n oil-development
|
||||
yes
|
||||
|
||||
kubectl --as joe --as-group capsule.clastix.io auth can-i create pod -n oil-production
|
||||
no
|
||||
```
|
||||
|
||||
> Please, note the user `joe`, in the example above, is not acting as tenant owner. He can just operate in `oil-development` namespace as admin.
|
||||
|
||||
## Assign multiple tenants
|
||||
A single team is likely responsible for multiple lines of business. For example, in our sample organization Acme Corp., Alice is responsible for both the Oil and Gas lines of business. It's more likely that Alice requires two different tenants, for example, `oil` and `gas` to keep things isolated.
|
||||
|
||||
@@ -1085,7 +1132,7 @@ Also, Bill can make sure pods belonging to a tenant namespace cannot access othe
|
||||
Bill can set network policies in the tenant manifest, according to the requirements:
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
@@ -1186,7 +1233,7 @@ Bob, an attacker, could try to schedule a Pod on the same node where Alice is ru
|
||||
To avoid this kind of attack, Bill, the cluster admin, can force Alice, the tenant owner, to start her Pods using only the allowed values for `ImagePullPolicy`, enforcing the `kubelet` to check the authorization first.
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
@@ -1212,7 +1259,7 @@ The spec `containerRegistries` addresses this task and can provide a combination
|
||||
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
@@ -1237,95 +1284,13 @@ A Pod running `internal.registry.foo.tld/capsule:latest` as registry will be all
|
||||
|
||||
Any attempt of Alice to use a not allowed `containerRegistries` value is denied by the Validation Webhook enforcing it.
|
||||
|
||||
|
||||
## Assign Pod Security Policies
|
||||
Bill, the cluster admin, can assign a dedicated Pod Security Policy (PSP) to Alice's tenant. This is likely to be a requirement in a multi-tenancy environment.
|
||||
|
||||
The cluster admin creates a PSP:
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
apiVersion: policy/v1beta1
|
||||
kind: PodSecurityPolicy
|
||||
metadata:
|
||||
name: psp:restricted
|
||||
spec:
|
||||
privileged: false
|
||||
# Required to prevent escalations to root.
|
||||
allowPrivilegeEscalation: false
|
||||
...
|
||||
EOF
|
||||
```
|
||||
|
||||
Then create a _ClusterRole_ using or granting the said item
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: psp:restricted
|
||||
rules:
|
||||
- apiGroups: ['policy']
|
||||
resources: ['podsecuritypolicies']
|
||||
resourceNames: ['psp:restricted']
|
||||
verbs: ['use']
|
||||
EOF
|
||||
```
|
||||
|
||||
Bill can assign this role to all namespaces in the Alice's tenant by setting it in the tenant manifest:
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
additionalRoleBindings:
|
||||
- clusterRoleName: psp:privileged
|
||||
subjects:
|
||||
- kind: "Group"
|
||||
apiGroup: "rbac.authorization.k8s.io"
|
||||
name: "system:authenticated"
|
||||
EOF
|
||||
```
|
||||
|
||||
With the given specification, Capsule will ensure that all Alice's namespaces will contain a _RoleBinding_ for the specified _Cluster Role_.
|
||||
|
||||
For example, in the `oil-production` namespace, Alice will see:
|
||||
|
||||
```yaml
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: 'capsule-oil-psp:privileged'
|
||||
namespace: oil-production
|
||||
labels:
|
||||
capsule.clastix.io/role-binding: a10c4c8c48474963
|
||||
capsule.clastix.io/tenant: oil
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: 'system:authenticated'
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: 'psp:privileged'
|
||||
```
|
||||
|
||||
With the above example, Capsule is forbidding any authenticated user in `oil-production` namespace to run privileged pods and to perform privilege escalation as declared by the Cluster Role `psp:privileged`.
|
||||
|
||||
## Create Custom Resources
|
||||
Capsule grants admin permissions to the tenant owners but is only limited to their namespaces. To achieve that, it assigns the ClusterRole [admin](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) to the tenant owner. This ClusterRole does not permit the installation of custom resources in the namespaces.
|
||||
|
||||
In order to leave the tenant owner to create Custom Resources in their namespaces, the cluster admin defines a proper Cluster Role. For example:
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
@@ -1350,7 +1315,7 @@ EOF
|
||||
Bill can assign this role to any namespace in the Alice's tenant by setting it in the tenant manifest:
|
||||
|
||||
```yaml
|
||||
kubectl -n oil-production apply -f - << EOF
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
@@ -1448,10 +1413,10 @@ spec:
|
||||
|
||||
> This feature is still in an alpha stage and requires a high amount of computing resources due to the dynamic client requests.
|
||||
|
||||
## Taint namespaces
|
||||
With Capsule, Bill can _"taint"_ the namespaces created by Alice with additional labels and/or annotations. There is no specific semantic assigned to these labels and annotations: they just will be assigned to the namespaces in the tenant as they are created by Alice. This can help the cluster admin to implement specific use cases. As it can be used to implement backup as a service for namespaces in the tenant.
|
||||
## Assign Additional Metadata
|
||||
The cluster admin can _"taint"_ the namespaces created by tenant onwers with additional metadata as labels and annotations. There is no specific semantic assigned to these labels and annotations: they will be assigned to the namespaces in the tenant as they are created. This can help the cluster admin to implement specific use cases as, for example, leave only a given tenant to be backuped by a backup service.
|
||||
|
||||
Bill assigns additional labels and annotations to all namespaces created in the `oil` tenant:
|
||||
Assigns additional labels and annotations to all namespaces created in the `oil` tenant:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
@@ -1466,18 +1431,42 @@ spec:
|
||||
namespaceOptions:
|
||||
additionalMetadata:
|
||||
annotations:
|
||||
capsule.clastix.io/backup: "true"
|
||||
storagelocationtype: s3
|
||||
labels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
capsule.clastix.io/backup: "true"
|
||||
EOF
|
||||
```
|
||||
|
||||
When Alice creates a namespace, this will inherit the given label and/or annotation.
|
||||
When the tenant owner creates a namespace, it inherits the given label and/or annotation:
|
||||
|
||||
## Taint services
|
||||
With Capsule, Bill can _"taint"_ the services created by Alice with additional labels and/or annotations. There is no specific semantic assigned to these labels and annotations: they just will be assigned to the services in the tenant as they are created by Alice. This can help the cluster admin to implement specific use cases.
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
annotations:
|
||||
storagelocationtype: s3
|
||||
labels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
kubernetes.io/metadata.name: oil-production
|
||||
name: oil-production
|
||||
capsule.clastix.io/backup: "true"
|
||||
name: oil-production
|
||||
ownerReferences:
|
||||
- apiVersion: capsule.clastix.io/v1beta1
|
||||
blockOwnerDeletion: true
|
||||
controller: true
|
||||
kind: Tenant
|
||||
name: oil
|
||||
spec:
|
||||
finalizers:
|
||||
- kubernetes
|
||||
status:
|
||||
phase: Active
|
||||
```
|
||||
|
||||
Bill assigns additional labels and annotations to all services created in the `oil` tenant:
|
||||
Additionally, the cluster admin can _"taint"_ the services created by the tenant owners with additional metadata as labels and annotations.
|
||||
|
||||
Assigns additional labels and annotations to all services created in the `oil` tenant:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
@@ -1491,14 +1480,30 @@ spec:
|
||||
kind: User
|
||||
serviceOptions:
|
||||
additionalMetadata:
|
||||
annotations:
|
||||
capsule.clastix.io/backup: "true"
|
||||
labels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
capsule.clastix.io/backup: "true"
|
||||
EOF
|
||||
```
|
||||
|
||||
When Alice creates a service in a namespace, this will inherit the given label and/or annotation.
|
||||
When the tenant owner creates a service in a tenant namespace, it inherits the given label and/or annotation:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: oil-production
|
||||
labels:
|
||||
capsule.clastix.io/backup: "true"
|
||||
spec:
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
run: nginx
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
## Cordon a Tenant
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@@ -37,18 +37,18 @@ spec:
|
||||
In example, the cluster admin is supposed to apply this Kustomization, during the cluster bootstrap that i.e. will reconcile also Flux itself.
|
||||
All the remaining Reconciliation resources can be children of this Kustomization.
|
||||
|
||||

|
||||

|
||||
|
||||
### Namespace-as-a-Service
|
||||
|
||||
Tenants could have his own set of Namespaces to operate on but it should be prepared by higher-level roles, like platform admins: the declarations would be part of the platform space.
|
||||
They would be responsible of tenants administration, and each change (e.g. new tenant Namespace) should be a request that would pass through approval.
|
||||
|
||||

|
||||

|
||||
|
||||
What if we would like to provide tenants the ability to manage also their own space the GitOps-way? Enter Capsule.
|
||||
|
||||

|
||||

|
||||
|
||||
## The ingredients of the recipe
|
||||
|
||||
@@ -279,35 +279,6 @@ this is the required set of resources to setup a Tenant:
|
||||
userGroups:
|
||||
- system:serviceaccounts:my-tenant
|
||||
```
|
||||
- Additional `ClusterRole` with related `ClusterRoleBinding` that allows to `PATCH` requests on Namespaces, besides `CREATE`. Flux kustomize controller will `kubectl-apply` resources:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: capsule-namespace-provisioner-gitops
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- patch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: capsule-namespace-provisioner-gitops-my-tenant
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: capsule-namespace-provisioner-gitops
|
||||
subjects:
|
||||
- apiGroup: rbac.authorization.k8s.io
|
||||
kind: Group
|
||||
name: system:serviceaccounts:my-tenant
|
||||
```
|
||||
|
||||
- Additional `ClusterRole` with related `ClusterRoleBinding` that allows the Tenant GitOps Reconciler to impersonate his own `User` (e.g. `system:serviceaccount:my-tenant:gitops-reconciler`):
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
258
docs/content/guides/pod-security.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Pod Security
|
||||
In Kubernetes, by default, workloads run with administrative access, which might be acceptable if there is only a single application running in the cluster or a single user accessing it. This is seldomly required and you’ll consequently suffer a noisy neighbour effect along with large security blast radiuses.
|
||||
|
||||
Many of these concerns were addressed initially by [PodSecurityPolicies](https://kubernetes.io/docs/concepts/security/pod-security-policy) which have been present in the Kubernetes APIs since the very early days.
|
||||
|
||||
The Pod Security Policies are deprecated in Kubernetes 1.21 and removed entirely in 1.25. As replacement, the [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) has been introduced. Capsule support the new standard for tenants under its control as well as the oldest approach.
|
||||
|
||||
## Pod Security Policies
|
||||
As stated in the documentation, *"PodSecurityPolicies enable fine-grained authorization of pod creation and updates. A Pod Security Policy is a cluster-level resource that controls security sensitive aspects of the pod specification. The `PodSecurityPolicy` objects define a set of conditions that a pod must run with in order to be accepted into the system, as well as defaults for the related fields."*
|
||||
|
||||
Using the [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy), the cluster admin can impose limits on pod creation, for example the types of volume that can be consumed, the linux user that the process runs as in order to avoid running things as root, and more. From multi-tenancy point of view, the cluster admin has to control how users run pods in their tenants with a different level of permission on tenant basis.
|
||||
|
||||
Assume the Kubernetes cluster has been configured with [Pod Security Policy Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy) enabled in the APIs server: `--enable-admission-plugins=PodSecurityPolicy`
|
||||
|
||||
The cluster admin creates a `PodSecurityPolicy`:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: policy/v1beta1
|
||||
kind: PodSecurityPolicy
|
||||
metadata:
|
||||
name: psp:restricted
|
||||
spec:
|
||||
privileged: false
|
||||
# Required to prevent escalations to root.
|
||||
allowPrivilegeEscalation: false
|
||||
EOF
|
||||
```
|
||||
|
||||
Then create a _ClusterRole_ using or granting the said item
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: psp:restricted
|
||||
rules:
|
||||
- apiGroups: ['policy']
|
||||
resources: ['podsecuritypolicies']
|
||||
resourceNames: ['psp:restricted']
|
||||
verbs: ['use']
|
||||
EOF
|
||||
```
|
||||
|
||||
He can assign this role to all namespaces in a tenant by setting the tenant manifest:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
additionalRoleBindings:
|
||||
- clusterRoleName: psp:privileged
|
||||
subjects:
|
||||
- kind: "Group"
|
||||
apiGroup: "rbac.authorization.k8s.io"
|
||||
name: "system:authenticated"
|
||||
EOF
|
||||
```
|
||||
|
||||
With the given specification, Capsule will ensure that all tenant namespaces will contain a _RoleBinding_ for the specified _Cluster Role_:
|
||||
|
||||
```yaml
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: 'capsule-oil-psp:privileged'
|
||||
namespace: oil-production
|
||||
labels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: 'system:authenticated'
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: 'psp:privileged'
|
||||
```
|
||||
|
||||
Capsule admission controller forbids the tenant owner to run privileged pods in `oil-production` namespace and perform privilege escalation as declared by the above Cluster Role `psp:privileged`.
|
||||
|
||||
As tenant owner, creates a namespace:
|
||||
|
||||
```
|
||||
kubectl --kubeconfig alice-oil.kubeconfig create ns oil-production
|
||||
```
|
||||
|
||||
and create a pod with privileged permissions:
|
||||
|
||||
```yaml
|
||||
kubectl --kubeconfig alice-oil.kubeconfig apply -f - << EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: oil-production
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
privileged: true
|
||||
EOF
|
||||
```
|
||||
|
||||
Since the assigned `PodSecurityPolicy` explicitly disallows privileged containers, the tenant owner will see her request to be rejected by the Pod Security Policy Admission Controller.
|
||||
|
||||
## Pod Security Standards
|
||||
One of the issues with Pod Secury Policies is that it is difficult to apply restrictive permissions on a granular level, increasing security risk. Also the Pod Security Policies get applied when the request is submitted and there is no way of applying them to pods that are already running. For these, and other reasons, the Kubernetes community decided to deprecate the Pod Secury Policies.
|
||||
|
||||
As the Pod Secury Policies get deprecated and removed, the [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) is used in place. It defines three different policies to broadly cover the security spectrum. These policies are cumulative and range from highly-permissive to highly-restrictive:
|
||||
|
||||
- **Privileged**: unrestricted policy, providing the widest possible level of permissions.
|
||||
- **Baseline**: minimally restrictive policy which prevents known privilege escalations.
|
||||
- **Restricted**: heavily restricted policy, following current Pod hardening best practices.
|
||||
|
||||
Kubernetes provides a built-in [Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecurity) to enforce the Pod Security Standards at either:
|
||||
|
||||
1. cluster level which applies a standard configuration to all namespaces in a cluster
|
||||
2. namespace level, one namespace at a time
|
||||
|
||||
For the first case, the cluster admin has to configure the Admission Controller and pass the configuration to the `kube-apiserver` by mean of the `--admission-control-config-file` extra argument, for example:
|
||||
|
||||
```yaml
|
||||
apiVersion: apiserver.config.k8s.io/v1
|
||||
kind: AdmissionConfiguration
|
||||
plugins:
|
||||
- name: PodSecurity
|
||||
configuration:
|
||||
apiVersion: pod-security.admission.config.k8s.io/v1beta1
|
||||
kind: PodSecurityConfiguration
|
||||
defaults:
|
||||
enforce: "baseline"
|
||||
enforce-version: "latest"
|
||||
warn: "restricted"
|
||||
warn-version: "latest"
|
||||
audit: "restricted"
|
||||
audit-version: "latest"
|
||||
exemptions:
|
||||
usernames: []
|
||||
runtimeClasses: []
|
||||
namespaces: [kube-system]
|
||||
```
|
||||
|
||||
For the second case, he can just assign labels to the specific namespace he wants enforce the policy since the Pod Security Admission Controller is enabled by default starting from Kubernetes 1.23+:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: baseline
|
||||
pod-security.kubernetes.io/warn: restricted
|
||||
pod-security.kubernetes.io/audit: restricted
|
||||
name: development
|
||||
```
|
||||
|
||||
## Pod Security Standards with Capsule
|
||||
According to the regular Kubernetes segregation model, the cluster admin has to operate either at cluster level or at namespace level. Since Capsule introduces a further segregation level (the _Tenant_ abstraction), the cluster admin can implement Pod Security Standards at tenant level by simply forcing specific labels on all the namespaces created in the tenant.
|
||||
|
||||
As cluster admin, create a tenant with additional labels:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f - << EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
namespaceOptions:
|
||||
additionalMetadata:
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: baseline
|
||||
pod-security.kubernetes.io/audit: restricted
|
||||
pod-security.kubernetes.io/warn: restricted
|
||||
owners:
|
||||
- kind: User
|
||||
name: alice
|
||||
EOF
|
||||
```
|
||||
|
||||
All namespaces created by the tenant owner, will inherit the Pod Security labels:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
kubernetes.io/metadata.name: oil-development
|
||||
name: oil-development
|
||||
pod-security.kubernetes.io/enforce: baseline
|
||||
pod-security.kubernetes.io/warn: restricted
|
||||
pod-security.kubernetes.io/audit: restricted
|
||||
name: oil-development
|
||||
ownerReferences:
|
||||
- apiVersion: capsule.clastix.io/v1beta1
|
||||
blockOwnerDeletion: true
|
||||
controller: true
|
||||
kind: Tenant
|
||||
name: oil
|
||||
```
|
||||
|
||||
and the regular Pod Security Admission Controller does the magic:
|
||||
|
||||
```yaml
|
||||
kubectl --kubeconfig alice-oil.kubeconfig apply -f - << EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: oil-production
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
privileged: true
|
||||
EOF
|
||||
```
|
||||
|
||||
The request gets denied:
|
||||
|
||||
```
|
||||
Error from server (Forbidden): error when creating "STDIN":
|
||||
pods "nginx" is forbidden: violates PodSecurity "baseline:latest": privileged
|
||||
(container "nginx" must not set securityContext.privileged=true)
|
||||
```
|
||||
|
||||
If the tenant owner tries to change o delete the above labels, Capsule will reconcile them to the original tenant manifest set by the cluster admin.
|
||||
|
||||
As additional security measure, the cluster admin can also prevent the tenant owner to make an improper usage of the above labels:
|
||||
|
||||
```
|
||||
kubectl annotate tenant oil \
|
||||
capsule.clastix.io/forbidden-namespace-labels-regexp="pod-security.kubernetes.io\/(enforce|warn|audit)"
|
||||
```
|
||||
|
||||
In that case, the tenant owner gets denied if she tries to use the labels:
|
||||
|
||||
```
|
||||
kubectl --kubeconfig alice-oil.kubeconfig label ns oil-production \
|
||||
pod-security.kubernetes.io/enforce=restricted \
|
||||
--overwrite
|
||||
|
||||
Error from server (Label pod-security.kubernetes.io/audit is forbidden for namespaces in the current Tenant ...
|
||||
```
|
||||
@@ -1,23 +1,125 @@
|
||||
# Tenants Backup and Restore with Velero
|
||||
|
||||
[Velero](https://velero.io) is a backup and restore solution that performs disaster recovery and migrates Kubernetes cluster resources and persistent volumes.
|
||||
[Velero](https://velero.io) is a backup and restore solution that performs data protection, disaster recovery and migrates Kubernetes cluster from on-premises to the Cloud or between different Clouds.
|
||||
|
||||
Using Velero in a Kubernetes cluster where Capsule is installed can lead to an incomplete restore of the cluster's Tenants. This is because Velero omits the `ownerReferences` section from the tenant's namespace manifests when backup them.
|
||||
When coming to backup and restore in Kubernetes, we have two main requirements:
|
||||
|
||||
To avoid this problem you can use the script `velero-restore.sh` under the `hack/` folder.
|
||||
- Configurations backup
|
||||
- Data backup
|
||||
|
||||
In case of a data loss, the right thing to do is to restore the cluster with **Velero** at first. Once Velero has finished, you can proceed using the script to complete the restoration.
|
||||
The first requirement aims to backup all the resources stored into `etcd` database, for example: `namespaces`, `pods`, `services`, `deployments`, etc. The second is about how to backup stateful application data as volumes.
|
||||
|
||||
```bash
|
||||
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig restore
|
||||
The main limitation of Velero is the multi tenancy. Currently, Velero does not support multi tenancy meaning it can be only used from admin users and so it cannot provided "as a service" to the users. This means that the cluster admin needs to take care of users' backup.
|
||||
|
||||
Assuming you have multiple tenants managed by Capsule, for example `oil` and `gas`, as cluster admin, you can to take care of scheduling backups for:
|
||||
|
||||
- Tenant cluster resources
|
||||
- Namespaces belonging to each tenant
|
||||
|
||||
## Create backup of a tenant
|
||||
Create a backup of the tenant `oil`. It consists in two different backups:
|
||||
|
||||
- backup of the tenant resource
|
||||
- backup of all the resources belonging to the tenant
|
||||
|
||||
To backup the `oil` tenant selectively, label the tenant as:
|
||||
|
||||
```
|
||||
kubectl label tenant oil capsule.clastix.io/tenant=oil
|
||||
```
|
||||
|
||||
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
|
||||
and create the backup
|
||||
|
||||
Additionally, you can also specify a selected range of tenants to be restored:
|
||||
|
||||
```bash
|
||||
./velero-restore.sh --tenant "gas oil" restore
|
||||
```
|
||||
velero create backup oil-tenant \
|
||||
--include-cluster-resources=true \
|
||||
--include-resources=tenants.capsule.clastix.io \
|
||||
--selector capsule.clastix.io/tenant=oil
|
||||
```
|
||||
|
||||
In this way, only the tenants **gas** and **oil** will be restored.
|
||||
resulting in the following Velero object:
|
||||
|
||||
```yaml
|
||||
apiVersion: velero.io/v1
|
||||
kind: Backup
|
||||
metadata:
|
||||
name: oil-tenant
|
||||
spec:
|
||||
defaultVolumesToRestic: false
|
||||
hooks: {}
|
||||
includeClusterResources: true
|
||||
includedNamespaces:
|
||||
- '*'
|
||||
includedResources:
|
||||
- tenants.capsule.clastix.io
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
capsule.clastix.io/tenant: oil
|
||||
metadata: {}
|
||||
storageLocation: default
|
||||
ttl: 720h0m0s
|
||||
```
|
||||
|
||||
Create a backup of all the resources belonging to the `oil` tenant namespaces:
|
||||
|
||||
```
|
||||
velero create backup oil-namespaces \
|
||||
--include-cluster-resources=false \
|
||||
--include-namespaces oil-production,oil-development,oil-marketing
|
||||
```
|
||||
|
||||
resulting to the following Velero object:
|
||||
|
||||
```yaml
|
||||
apiVersion: velero.io/v1
|
||||
kind: Backup
|
||||
metadata:
|
||||
name: oil-namespaces
|
||||
spec:
|
||||
defaultVolumesToRestic: false
|
||||
hooks: {}
|
||||
includeClusterResources: false
|
||||
includedNamespaces:
|
||||
- oil-production
|
||||
- oil-development
|
||||
- oil-marketing
|
||||
metadata: {}
|
||||
storageLocation: default
|
||||
ttl: 720h0m0s
|
||||
```
|
||||
|
||||
> Velero requires an Object Storage backend where to store backups, you should take care of this requirement before to use Velero.
|
||||
|
||||
## Restore a tenant from the backup
|
||||
To recover the tenant after a disaster, or to migrate it to another cluster, create a restore from the previous backups:
|
||||
|
||||
```
|
||||
velero create restore --from-backup oil-tenant
|
||||
velero create restore --from-backup oil-namespaces
|
||||
```
|
||||
|
||||
Using Velero to restore a Capsule tenant can lead to an incomplete recovery of tenant because the namespaces restored with Velero do not have the `OwnerReference` field used to bind the namespaces to the tenant. For this reason, all restored namespaces are not bound to the tenant:
|
||||
|
||||
```
|
||||
kubectl get tnt
|
||||
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
gas active 9 5 {"pool":"gas"} 34m
|
||||
solar active 9 8 {"pool":"solar"} 33m
|
||||
oil active 9 0 # <<< {"pool":"oil"} 54m
|
||||
```
|
||||
|
||||
To avoid this problem you can use the script `velero-restore.sh` located under the `hack/` folder:
|
||||
|
||||
```
|
||||
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig --tenant "oil" restore
|
||||
```
|
||||
|
||||
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the tenant back.
|
||||
|
||||
```
|
||||
kubectl get tnt
|
||||
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
gas active 9 5 {"pool":"gas"} 44m
|
||||
solar active 9 8 {"pool":"solar"} 43m
|
||||
oil active 9 3 # <<< {"pool":"oil"} 12s
|
||||
```
|
||||
|
||||
@@ -37,6 +37,10 @@ module.exports = function (api) {
|
||||
label: 'References',
|
||||
path: '/docs/general/references'
|
||||
},
|
||||
{
|
||||
label: 'CRDs APIs',
|
||||
path: '/docs/general/tenant-crd'
|
||||
},
|
||||
{
|
||||
label: 'Multi-Tenant Benchmark',
|
||||
path: '/docs/general/mtb'
|
||||
@@ -70,10 +74,18 @@ module.exports = function (api) {
|
||||
label: 'Upgrading Tenant version',
|
||||
path: '/docs/guides/upgrading'
|
||||
},
|
||||
{
|
||||
label: 'Multi-tenant GitOps with Flux',
|
||||
path: '/docs/guides/flux2-capsule'
|
||||
},
|
||||
{
|
||||
label: 'Install on Charmed Kubernetes',
|
||||
path: '/docs/guides/charmed'
|
||||
},
|
||||
{
|
||||
label: 'Control Pod Security',
|
||||
path: '/docs/guides/pod-security'
|
||||
},
|
||||
{
|
||||
title: 'Managed Kubernetes',
|
||||
subItems: [
|
||||
@@ -90,11 +102,7 @@ module.exports = function (api) {
|
||||
path: '/docs/guides/managed-kubernetes/coaks'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Flux and Capsule for multi-tenant GitOps',
|
||||
path: '/docs/guides/flux2-capsule'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
18
docs/package-lock.json
generated
@@ -4472,9 +4472,9 @@
|
||||
}
|
||||
},
|
||||
"defined": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
|
||||
"integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz",
|
||||
"integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==",
|
||||
"dev": true
|
||||
},
|
||||
"delayed-stream": {
|
||||
@@ -8144,9 +8144,9 @@
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -10842,9 +10842,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
|
||||
"version": "8.4.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
|
||||
"integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
|
||||
111
docs/template/reference-cr.tmpl
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
<style>
|
||||
table {
|
||||
border: solid;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
th, td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border: solid;
|
||||
}
|
||||
tr:hover {background-color: coral;}
|
||||
sup {
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
# API Reference
|
||||
|
||||
Packages:
|
||||
{{range .Groups}}
|
||||
- [{{.Group}}/{{.Version}}](#{{ anchorize (printf "%s/%s" .Group .Version) }})
|
||||
{{- end -}}{{/* range .Groups */}}
|
||||
|
||||
{{- range .Groups }}
|
||||
{{- $group := . }}
|
||||
|
||||
# {{.Group}}/{{.Version}}
|
||||
|
||||
Resource Types:
|
||||
{{range .Kinds}}
|
||||
- [{{.Name}}](#{{ anchorize .Name }})
|
||||
{{end}}{{/* range .Kinds */}}
|
||||
|
||||
{{range .Kinds}}
|
||||
{{$kind := .}}
|
||||
## {{.Name}}
|
||||
|
||||
{{range .Types}}
|
||||
|
||||
{{if not .IsTopLevel}}
|
||||
### {{.Name}}
|
||||
{{end}}
|
||||
|
||||
|
||||
{{.Description}}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .IsTopLevel -}}
|
||||
<tr>
|
||||
<td><b>apiVersion</b></td>
|
||||
<td>string</td>
|
||||
<td>{{$group.Group}}/{{$group.Version}}</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>kind</b></td>
|
||||
<td>string</td>
|
||||
<td>{{$kind.Name}}</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b><a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#objectmeta-v1-meta">metadata</a></b></td>
|
||||
<td>object</td>
|
||||
<td>Refer to the Kubernetes API documentation for the fields of the `metadata` field.</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
{{- end -}}
|
||||
{{- range .Fields -}}
|
||||
<tr>
|
||||
<td><b>{{if .TypeKey}}<a href="#{{.TypeKey}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}</b></td>
|
||||
<td>{{.Type}}</td>
|
||||
<td>
|
||||
{{.Description}}<br/>
|
||||
{{- if or .Schema.Format .Schema.Enum .Schema.Default .Schema.Minimum .Schema.Maximum }}
|
||||
<br/>
|
||||
{{- end}}
|
||||
{{- if .Schema.Format }}
|
||||
<i>Format</i>: {{ .Schema.Format }}<br/>
|
||||
{{- end }}
|
||||
{{- if .Schema.Enum }}
|
||||
<i>Enum</i>: {{ .Schema.Enum | toStrings | join ", " }}<br/>
|
||||
{{- end }}
|
||||
{{- if .Schema.Default }}
|
||||
<i>Default</i>: {{ .Schema.Default }}<br/>
|
||||
{{- end }}
|
||||
{{- if .Schema.Minimum }}
|
||||
<i>Minimum</i>: {{ .Schema.Minimum }}<br/>
|
||||
{{- end }}
|
||||
{{- if .Schema.Maximum }}
|
||||
<i>Maximum</i>: {{ .Schema.Maximum }}<br/>
|
||||
{{- end }}
|
||||
</td>
|
||||
<td>{{.Required}}</td>
|
||||
</tr>
|
||||
{{- end -}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{- end}}{{/* range .Types */}}
|
||||
{{- end}}{{/* range .Kinds */}}
|
||||
{{- end}}{{/* range .Groups */}}
|
||||
@@ -2634,7 +2634,7 @@ component-emitter@^1.2.1:
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
concat-stream@^1.5.0:
|
||||
version "1.6.2"
|
||||
@@ -6368,9 +6368,9 @@ minimalistic-crypto-utils@^1.0.1:
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
||||
146
e2e/sa_prevent_privilege_escalation_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
//go:build e2e
|
||||
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
var _ = Describe("trying to escalate from a Tenant Namespace ServiceAccount", func() {
|
||||
tnt := &capsulev1beta1.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sa-privilege-escalation",
|
||||
},
|
||||
Spec: capsulev1beta1.TenantSpec{
|
||||
Owners: capsulev1beta1.OwnerListSpec{
|
||||
{
|
||||
Name: "mario",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
NodeSelector: map[string]string{
|
||||
"kubernetes.io/os": "linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns := NewNamespace("attack")
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should block Namespace changes", func() {
|
||||
role := rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ns-update-role",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"namespaces"},
|
||||
ResourceNames: []string{ns.GetName()},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.Background(), &role)
|
||||
}).Should(Succeed())
|
||||
|
||||
rolebinding := rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "attacker-rolebinding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "attacker",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.Background(), &rolebinding)
|
||||
}).Should(Succeed())
|
||||
|
||||
c, err := config.GetConfig()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
c.Impersonate.Groups = []string{"system:serviceaccounts"}
|
||||
c.Impersonate.UserName = fmt.Sprintf("system:serviceaccount:%s:%s", rolebinding.Subjects[0].Namespace, rolebinding.Subjects[0].Name)
|
||||
saClient, err := kubernetes.NewForConfig(c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Changing Owner Reference is forbidden
|
||||
Consistently(func() error {
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns.OwnerReferences[0].UID = uuid.NewUUID()
|
||||
|
||||
_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
// Removing Owner Reference is forbidden
|
||||
Consistently(func() error {
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns.SetOwnerReferences(nil)
|
||||
|
||||
_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
// Breaking nodeSelector is forbidden
|
||||
Consistently(func() error {
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns.SetAnnotations(map[string]string{
|
||||
"scheduler.alpha.kubernetes.io/node-selector": "kubernetes.io/os=forbidden",
|
||||
})
|
||||
|
||||
_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
|
||||
|
||||
return err
|
||||
}, 10*time.Second, time.Second).ShouldNot(Succeed())
|
||||
})
|
||||
})
|
||||
2
main.go
@@ -205,7 +205,7 @@ func main() {
|
||||
route.Service(service.Handler()),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler()),
|
||||
route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
|
||||
route.OwnerReference(utils.InCapsuleGroups(cfg, namespacewebhook.OwnerReferenceHandler(), ownerreference.Handler(cfg))),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
)
|
||||
|
||||
35
pkg/utils/node_selector.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
NodeSelectorAnnotation = "scheduler.alpha.kubernetes.io/node-selector"
|
||||
)
|
||||
|
||||
func BuildNodeSelector(tnt *capsulev1beta1.Tenant, nsAnnotations map[string]string) map[string]string {
|
||||
if nsAnnotations == nil {
|
||||
nsAnnotations = make(map[string]string)
|
||||
}
|
||||
|
||||
selector := make([]string, 0, len(tnt.Spec.NodeSelector))
|
||||
|
||||
for k, v := range tnt.Spec.NodeSelector {
|
||||
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
// Sorting the resulting slice: iterating over maps is randomized, and we could end-up
|
||||
// in multiple reconciliations upon multiple node selectors.
|
||||
sort.Strings(selector)
|
||||
|
||||
nsAnnotations[NodeSelectorAnnotation] = strings.Join(selector, ",")
|
||||
|
||||
return nsAnnotations
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (r *freezedHandler) OnDelete(c client.Client, _ *admission.Decoder, recorde
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name)
|
||||
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
@@ -101,7 +101,7 @@ func (r *freezedHandler) OnUpdate(c client.Client, decoder *admission.Decoder, r
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(req, r.configuration.UserGroups()) {
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName())
|
||||
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
|
||||
64
pkg/webhook/namespace/owner_reference.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
|
||||
"github.com/clastix/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type ownerReferenceHandler struct{}
|
||||
|
||||
func OwnerReferenceHandler() capsulewebhook.Handler {
|
||||
return &ownerReferenceHandler{}
|
||||
}
|
||||
|
||||
func (r *ownerReferenceHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ownerReferenceHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ownerReferenceHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
oldNs := &corev1.Namespace{}
|
||||
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
newNs := &corev1.Namespace{}
|
||||
if err := decoder.Decode(req, newNs); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(newNs.OwnerReferences) == 0 {
|
||||
response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be removed"))
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if oldNs.GetOwnerReferences()[0].UID != newNs.GetOwnerReferences()[0].UID {
|
||||
response := admission.Errored(http.StatusBadRequest, fmt.Errorf("the OwnerReference cannot be changed"))
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
|
||||
@@ -112,6 +112,25 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
|
||||
}
|
||||
}
|
||||
|
||||
if len(tnt.Spec.NodeSelector) > 0 {
|
||||
v, ok := newNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"]
|
||||
if !ok {
|
||||
response := admission.Denied("the node-selector annotation is enforced, cannot be removed")
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorDeletion", string(response.Result.Reason))
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if v != oldNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"] {
|
||||
response := admission.Denied("the the node-selector annotation is enforced, cannot be updated")
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodeSelectorUpdate", string(response.Result.Reason))
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
var labels, annotations map[string]string
|
||||
|
||||
for key, value := range newNs.GetLabels() {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
|
||||
|
||||
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.setOwnerRef(ctx, req, client, decoder, recorder)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(req, h.configuration.UserGroups()) {
|
||||
if tnt.IsCordoned() && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
|
||||
|
||||
response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
|
||||
|
||||
@@ -28,7 +28,7 @@ type handler struct {
|
||||
|
||||
func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, rec
|
||||
|
||||
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, rec
|
||||
|
||||
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(req, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
"github.com/clastix/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
func IsCapsuleUser(req admission.Request, userGroups []string) bool {
|
||||
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string) bool {
|
||||
groupList := utils.NewUserGroupList(req.UserInfo.Groups)
|
||||
// if the user is a ServiceAccount belonging to the kube-system namespace, definitely, it's not a Capsule user
|
||||
// and we can skip the check in case of Capsule user group assigned to system:authenticated
|
||||
@@ -14,6 +21,23 @@ func IsCapsuleUser(req admission.Request, userGroups []string) bool {
|
||||
if groupList.Find("system:serviceaccounts:kube-system") {
|
||||
return false
|
||||
}
|
||||
// nolint:nestif
|
||||
if sets.NewString(req.UserInfo.Groups...).Has("system:serviceaccounts") {
|
||||
parts := strings.Split(req.UserInfo.Username, ":")
|
||||
|
||||
targetNamespace := parts[2]
|
||||
|
||||
if len(targetNamespace) > 0 {
|
||||
tl := &capsulev1beta1.TenantList{}
|
||||
if err := clt.List(ctx, tl, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(".status.namespaces", targetNamespace)}); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(tl.Items) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range userGroups {
|
||||
if groupList.Find(group) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
## Reference: https://github.com/norwoodj/helm-docs
|
||||
set -eux
|
||||
CHART_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
echo "$CHART_DIR"
|
||||
|
||||
echo "Running Helm-Docs"
|
||||
docker run \
|
||||
-v "$CHART_DIR:/helm-docs" \
|
||||
-u $(id -u) \
|
||||
jnorwood/helm-docs:latest
|
||||