Compare commits

...

19 Commits

Author SHA1 Message Date
Dario Tranchitella
eba072c88d chore(helm): release v0.1.3 2022-12-02 15:43:41 +01:00
Dario Tranchitella
9d02fb39eb chore(kustomize): release v0.1.3 2022-12-02 15:43:41 +01:00
Dario Tranchitella
1df430e71b test: preventing serviceaccount privilege escalation 2022-12-02 15:19:06 +01:00
Dario Tranchitella
75525ac192 fix: preventing serviceaccount privilege escalation 2022-12-02 15:19:06 +01:00
Dario Tranchitella
132ffd57ea chore(maintainers): welcome oliver 2022-11-21 18:56:44 +01:00
dependabot[bot]
7602114835 build(deps): bump minimatch from 3.0.4 to 3.1.2 in /docs
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-15 18:43:26 +01:00
Dario Tranchitella
82996c1c83 fix(docs): typo in the velero guide 2022-11-15 10:58:19 +01:00
Alessio Pragliola
ede96f5cf4 fix: service controller not skipping sentinel errs 2022-10-14 20:21:32 +02:00
Oliver Bähler
2fc1be8bfe feat: improve local development experience
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2022-10-13 16:02:23 +02:00
Lajos Papp
e0dbf47723 fix(docs): clarifying serviceaccount as tenant owner 2022-09-27 14:15:38 +02:00
Dario Tranchitella
fb68795e90 chore: adding code of conduct 2022-09-13 15:19:19 +02:00
Oliver Bähler
a026e2f00c feat: add bedag as adopters 2022-08-30 15:02:00 +02:00
Massimiliano Giovagnoli
413208e7fe docs(capsule-flux): remove no longer needed clusterrole for ns patch
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-30 10:34:21 +02:00
Oliver Bähler
2771b63c18 feat: add helm-docs check 2022-08-30 10:33:18 +02:00
Oliver Bähler
9a1520ff66 feat: add github config, correct slack link 2022-08-30 10:33:18 +02:00
Oliver Bähler
c304fb2438 feat: chart testing and docs improvements 2022-08-30 10:33:18 +02:00
Dario Tranchitella
6d56237e23 style(docs): removing parent link for crds docs 2022-08-17 18:49:57 +02:00
Dario Tranchitella
c32166ba45 chore(github): ensuring autogenerated crds docs is committed 2022-08-17 18:49:57 +02:00
Michele Mastrogiovanni
e4ecbe30d1 docs: autogeneration for crds documentation
Co-authored-by: mastrogiovanni <mastrogiovanni@localhost>
2022-08-17 18:36:22 +02:00
43 changed files with 4159 additions and 262 deletions

View File

@@ -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
View 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

View File

@@ -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
View 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
View 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

View File

@@ -16,3 +16,8 @@
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

View File

@@ -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

View File

@@ -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
View File

@@ -30,3 +30,5 @@ bin
.DS_Store
*.tgz
capsule

View File

@@ -3,3 +3,6 @@
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/)
![Bedag](https://www.bedag.ch/wGlobal/wGlobal/layout/images/logo.svg)

128
CODE_OF_CONDUCT.md Normal file
View 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.

View File

@@ -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

View File

@@ -72,12 +72,20 @@ 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

View File

@@ -21,8 +21,8 @@ sources:
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.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

View File

@@ -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;\
}

View File

@@ -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"` | |

View File

@@ -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

View File

@@ -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:

View File

@@ -7,4 +7,4 @@ kind: Kustomization
images:
- name: controller
newName: clastix/capsule
newTag: v0.1.2
newTag: v0.1.3

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
```
```

File diff suppressed because it is too large Load Diff

View File

@@ -139,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
```
@@ -160,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:
@@ -169,6 +169,23 @@ 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

View File

@@ -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

View File

@@ -68,7 +68,7 @@ velero create backup oil-namespaces \
--include-namespaces oil-production,oil-development,oil-marketing
```
resulting ti the following Velero object:
resulting to the following Velero object:
```yaml
apiVersion: velero.io/v1
@@ -122,4 +122,4 @@ 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
```
```

View File

@@ -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'

18
docs/package-lock.json generated
View File

@@ -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
View 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 */}}

View File

@@ -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"

View 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())
})
})

View File

@@ -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))),
)

View 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
}

View File

@@ -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")

View 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
}
}

View File

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

View File

@@ -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() {

View File

@@ -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
}
}

View File

@@ -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()))

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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