Compare commits
109 Commits
v0.1.1-rc0
...
v0.1.2-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb0858fae | ||
|
|
1af56b736b | ||
|
|
3c9228d1aa | ||
|
|
bf6760fbd0 | ||
|
|
23564f8e40 | ||
|
|
a8b84c8cb3 | ||
|
|
8c0c8c653d | ||
|
|
ec89f5dd26 | ||
|
|
68956a075a | ||
|
|
c036feeefc | ||
|
|
9f6883d309 | ||
|
|
e7227d24e9 | ||
|
|
f168137407 | ||
|
|
49e76f7f93 | ||
|
|
9d69770888 | ||
|
|
f4ac85dfed | ||
|
|
cb4289d45b | ||
|
|
01197892a4 | ||
|
|
345836630c | ||
|
|
69a6394e59 | ||
|
|
a3495cf614 | ||
|
|
7662c3dc6a | ||
|
|
137b0f083b | ||
|
|
9fd18db5a5 | ||
|
|
364adf7d9e | ||
|
|
cb3ce372b9 | ||
|
|
59d81c2002 | ||
|
|
85861ee5dc | ||
|
|
ed88606031 | ||
|
|
afae361627 | ||
|
|
535ef7412c | ||
|
|
f373debf54 | ||
|
|
569d803e95 | ||
|
|
7b3b0d6504 | ||
|
|
0bfca6b60e | ||
|
|
fdc1b3fe39 | ||
|
|
f7bc2e24cc | ||
|
|
d3021633cd | ||
|
|
7fefe4f6de | ||
|
|
302bb19707 | ||
|
|
27a7792c31 | ||
|
|
1a60e83772 | ||
|
|
632268dd68 | ||
|
|
4e07de37c4 | ||
|
|
1d10bcab1e | ||
|
|
d4a5f3beca | ||
|
|
cd56eab119 | ||
|
|
6cee5b73af | ||
|
|
8e7325aecb | ||
|
|
be26783424 | ||
|
|
0b199f4136 | ||
|
|
1bbaebbc90 | ||
|
|
4b8d8b2a7c | ||
|
|
3fb4c41daf | ||
|
|
055791966a | ||
|
|
c9af9c18e4 | ||
|
|
fef381d2b4 | ||
|
|
19aff8c882 | ||
|
|
8da7e22cb2 | ||
|
|
47c37a3d5d | ||
|
|
677175b3ed | ||
|
|
c95e3a2068 | ||
|
|
0be3be4480 | ||
|
|
6ad434fcfb | ||
|
|
e53911942d | ||
|
|
a179645f26 | ||
|
|
778fb4bcc2 | ||
|
|
bc23324fe7 | ||
|
|
4a6fd49554 | ||
|
|
d7baf18bf9 | ||
|
|
5c7804e1bf | ||
|
|
c4481f26f7 | ||
|
|
ec715d2e8f | ||
|
|
0aeaf89cb7 | ||
|
|
3d31ddb4e3 | ||
|
|
e83f344cdc | ||
|
|
da83a8711a | ||
|
|
43a944ace0 | ||
|
|
0acc2d2ef1 | ||
|
|
14f9686bbb | ||
|
|
6ba9826c51 | ||
|
|
bd58084ded | ||
|
|
3a5e50886d | ||
|
|
e2768dad83 | ||
|
|
b97c23176d | ||
|
|
fa8e805842 | ||
|
|
8df66fc232 | ||
|
|
c2218912eb | ||
|
|
e361e2d424 | ||
|
|
260b60d263 | ||
|
|
e0d5e6feb2 | ||
|
|
0784dc7177 | ||
|
|
b17c6c4636 | ||
|
|
52cf597041 | ||
|
|
b8dcded882 | ||
|
|
6a175e9017 | ||
|
|
3c609f84db | ||
|
|
7c3a59c4e4 | ||
|
|
d3e3b8a881 | ||
|
|
7a8148bd58 | ||
|
|
405d3ac52d | ||
|
|
f92acf9a9d | ||
|
|
bbb7b850d6 | ||
|
|
0f7284d190 | ||
|
|
7db263b2b6 | ||
|
|
0a8f50f761 | ||
|
|
7a66e8ea93 | ||
|
|
b5eb03ea76 | ||
|
|
681b514516 |
4
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2.3.0
|
||||
with:
|
||||
version: latest
|
||||
version: v1.45.2
|
||||
only-new-issues: false
|
||||
args: --timeout 2m --config .golangci.yml
|
||||
diff:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '1.18'
|
||||
- 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
|
||||
|
||||
27
.github/workflows/e2e.yml
vendored
@@ -3,15 +3,33 @@ name: e2e
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
paths:
|
||||
- '.github/workflows/e2e.yml'
|
||||
- 'api/**'
|
||||
- 'controllers/**'
|
||||
- 'e2e/*'
|
||||
- 'Dockerfile'
|
||||
- 'go.*'
|
||||
- 'main.go'
|
||||
- 'Makefile'
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
paths:
|
||||
- '.github/workflows/e2e.yml'
|
||||
- 'api/**'
|
||||
- 'controllers/**'
|
||||
- 'e2e/*'
|
||||
- 'Dockerfile'
|
||||
- 'go.*'
|
||||
- 'main.go'
|
||||
- 'Makefile'
|
||||
|
||||
jobs:
|
||||
kind:
|
||||
name: Kubernetes
|
||||
strategy:
|
||||
matrix:
|
||||
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.0']
|
||||
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.4', 'v1.23.0']
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -19,17 +37,12 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '1.18'
|
||||
- run: make manifests
|
||||
- name: Checking if manifests are disaligned
|
||||
run: test -z "$(git diff 2> /dev/null)"
|
||||
- name: Checking if manifests generated untracked files
|
||||
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
|
||||
- name: Installing Ginkgo
|
||||
run: go get github.com/onsi/ginkgo/ginkgo
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
- uses: engineerd/setup-kind@v0.5.0
|
||||
with:
|
||||
skipClusterCreation: true
|
||||
|
||||
3
.github/workflows/helm.yml
vendored
@@ -3,10 +3,9 @@ name: Helm Chart
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
tags: [ "helm-v*" ]
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
create:
|
||||
branches: [ "*" ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
1
.gitignore
vendored
@@ -22,6 +22,7 @@ bin
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode
|
||||
|
||||
**/*.kubeconfig
|
||||
**/*.crt
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
golint:
|
||||
min-confidence: 0
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goimports:
|
||||
local-prefixes: github.com/clastix/capsule
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
cyclop:
|
||||
max-complexity: 27
|
||||
gocognit:
|
||||
min-complexity: 50
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/clastix/capsule)
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- goconst
|
||||
- gocritic
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- misspell
|
||||
- nolintlint
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
enable-all: true
|
||||
disable:
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- lll
|
||||
- exhaustivestruct
|
||||
- maligned
|
||||
- interfacer
|
||||
- scopelint
|
||||
- golint
|
||||
- gochecknoglobals
|
||||
- goerr113
|
||||
- gomnd
|
||||
- paralleltest
|
||||
- ireturn
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.16 as builder
|
||||
FROM golang:1.18 as builder
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG GIT_HEAD_COMMIT
|
||||
|
||||
94
Makefile
@@ -40,12 +40,12 @@ test: generate manifests
|
||||
go test ./... -coverprofile cover.out
|
||||
|
||||
# Build manager binary
|
||||
manager: generate fmt vet
|
||||
go build -o bin/manager main.go
|
||||
manager: generate golint
|
||||
go build -o bin/manager
|
||||
|
||||
# Run against the configured Kubernetes cluster in ~/.kube/config
|
||||
run: generate manifests
|
||||
go run ./main.go
|
||||
go run .
|
||||
|
||||
# Creates the single file to install Capsule without any external dependency
|
||||
installer: manifests kustomize
|
||||
@@ -78,6 +78,58 @@ manifests: controller-gen
|
||||
generate: controller-gen
|
||||
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
|
||||
|
||||
# Setup development env
|
||||
# Usage:
|
||||
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
|
||||
# For example:
|
||||
# LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
||||
define TLS_CNF
|
||||
[ req ]
|
||||
default_bits = 4096
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
[ req_distinguished_name ]
|
||||
countryName = SG
|
||||
stateOrProvinceName = SG
|
||||
localityName = SG
|
||||
organizationName = CAPSULE
|
||||
commonName = CAPSULE
|
||||
[ req_ext ]
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = $(LAPTOP_HOST_IP)
|
||||
endef
|
||||
export TLS_CNF
|
||||
dev-setup:
|
||||
kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
|
||||
mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||
echo "$${TLS_CNF}" > _tls.cnf
|
||||
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
||||
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
|
||||
-extensions req_ext \
|
||||
-config _tls.cnf \
|
||||
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
|
||||
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
|
||||
rm -f _tls.cnf
|
||||
export WEBHOOK_URL="https://$${LAPTOP_HOST_IP}:9443"; \
|
||||
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
|
||||
kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]" && \
|
||||
kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/cordoning\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/ingresses\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/namespaces\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/networkpolicies\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/nodes\",'caBundle':\"$${CA_BUNDLE}\"}}\
|
||||
]";
|
||||
|
||||
# Build the docker image
|
||||
docker-build: test
|
||||
docker build . -t ${IMG} --build-arg GIT_HEAD_COMMIT=$(GIT_HEAD_COMMIT) \
|
||||
@@ -93,23 +145,33 @@ docker-push:
|
||||
|
||||
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
|
||||
controller-gen: ## Download controller-gen locally if necessary.
|
||||
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0)
|
||||
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0)
|
||||
|
||||
GINKGO = $(shell pwd)/bin/ginkgo
|
||||
ginkgo: ## Download ginkgo locally if necessary.
|
||||
$(call go-install-tool,$(KUSTOMIZE),github.com/onsi/ginkgo/ginkgo@v1.16.5)
|
||||
|
||||
KUSTOMIZE = $(shell pwd)/bin/kustomize
|
||||
kustomize: ## Download kustomize locally if necessary.
|
||||
$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7)
|
||||
$(call install-kustomize,$(KUSTOMIZE),3.8.7)
|
||||
|
||||
# go-get-tool will 'go get' any package $2 and install it to $1.
|
||||
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
define go-get-tool
|
||||
define install-kustomize
|
||||
@[ -f $(1) ] || { \
|
||||
set -e ;\
|
||||
TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
echo "Downloading $(2)" ;\
|
||||
GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
|
||||
rm -rf $$TMP_DIR ;\
|
||||
echo "Installing v$(2)" ;\
|
||||
cd bin ;\
|
||||
wget "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" ;\
|
||||
bash ./install_kustomize.sh $(2) ;\
|
||||
}
|
||||
endef
|
||||
|
||||
# go-install-tool will 'go install' any package $2 and install it to $1.
|
||||
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
define go-install-tool
|
||||
@[ -f $(1) ] || { \
|
||||
set -e ;\
|
||||
echo "Installing $(2)" ;\
|
||||
GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
|
||||
}
|
||||
endef
|
||||
|
||||
@@ -135,7 +197,7 @@ golint:
|
||||
|
||||
# Running e2e tests in a KinD instance
|
||||
.PHONY: e2e
|
||||
e2e/%:
|
||||
e2e/%: ginkgo
|
||||
kind create cluster --name capsule --image=kindest/node:$*
|
||||
make docker-build
|
||||
kind load docker-image --nodes capsule-control-plane --name capsule $(IMG)
|
||||
@@ -151,5 +213,5 @@ e2e/%:
|
||||
--set 'manager.readinessProbe.failureThreshold=10' \
|
||||
capsule \
|
||||
./charts/capsule
|
||||
ginkgo -v -tags e2e ./e2e
|
||||
$(GINKGO) -v -tags e2e ./e2e
|
||||
kind delete cluster --name capsule
|
||||
|
||||
144
README.md
@@ -14,161 +14,71 @@
|
||||
---
|
||||
|
||||
# Kubernetes multi-tenancy made easy
|
||||
**Capsule** helps to implement a multi-tenancy and policy-based environment in your Kubernetes cluster. It is not intended to be yet another _PaaS_, instead, it has been designed as a micro-services-based ecosystem with the minimalist approach, leveraging only on upstream Kubernetes.
|
||||
|
||||
**Capsule** implements a multi-tenant and policy-based environment in your Kubernetes cluster. It is designed as a micro-services-based ecosystem with the minimalist approach, leveraging only on upstream Kubernetes.
|
||||
|
||||
# What's the problem with the current status?
|
||||
|
||||
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well know phenomena of the _clusters sprawl_.
|
||||
|
||||
|
||||
# Entering Capsule
|
||||
Capsule takes a different approach. In a single cluster, the Capsule Controller aggregates multiple namespaces in a lightweight abstraction called _Tenant_, basically a grouping of Kubernetes Namespaces. Within each tenant, users are free to create their namespaces and share all the assigned resources while the Capsule Policy Engine keeps the different tenants isolated from each other.
|
||||
|
||||
The _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, _RBAC_, and other policies defined at the tenant level are automatically inherited by all the namespaces in the tenant. Then users are free to operate their tenants in autonomy, without the intervention of the cluster administrator. Take a look at following diagram:
|
||||
Capsule takes a different approach. In a single cluster, the Capsule Controller aggregates multiple namespaces in a lightweight abstraction called _Tenant_, basically a grouping of Kubernetes Namespaces. Within each tenant, users are free to create their namespaces and share all the assigned resources.
|
||||
|
||||
<p align="center" style="padding: 60px 20px">
|
||||
<img src="assets/capsule-operator.svg" />
|
||||
</p>
|
||||
On the other side, the Capsule Policy Engine keeps the different tenants isolated from each other. _Network and Security Policies_, _Resource Quota_, _Limit Ranges_, _RBAC_, and other policies defined at the tenant level are automatically inherited by all the namespaces in the tenant. Then users are free to operate their tenants in autonomy, without the intervention of the cluster administrator.
|
||||
|
||||
# Features
|
||||
|
||||
## Self-Service
|
||||
Leave to developers the freedom to self-provision their cluster resources according to the assigned boundaries.
|
||||
|
||||
Leave developers the freedom to self-provision their cluster resources according to the assigned boundaries.
|
||||
|
||||
## Preventing Clusters Sprawl
|
||||
|
||||
Share a single cluster with multiple teams, groups of users, or departments by saving operational and management efforts.
|
||||
|
||||
## Governance
|
||||
Leverage Kubernetes Admission Controllers to enforce the industry security best practices and meet legal requirements.
|
||||
|
||||
Leverage Kubernetes Admission Controllers to enforce the industry security best practices and meet policy requirements.
|
||||
|
||||
## Resources Control
|
||||
|
||||
Take control of the resources consumed by users while preventing them to overtake.
|
||||
|
||||
## Native Experience
|
||||
|
||||
Provide multi-tenancy with a native Kubernetes experience without introducing additional management layers, plugins, or customized binaries.
|
||||
|
||||
## GitOps ready
|
||||
|
||||
Capsule is completely declarative and GitOps ready.
|
||||
|
||||
## Bring your own device (BYOD)
|
||||
|
||||
Assign to tenants a dedicated set of compute, storage, and network resources and avoid the noisy neighbors' effect.
|
||||
|
||||
# Common use cases for Capsule
|
||||
Please, refer to the corresponding [section](./docs/operator/use-cases/overview.md) in the project documentation for a detailed list of common use cases that Capsule can address.
|
||||
|
||||
# Installation
|
||||
Make sure you have access to a Kubernetes cluster as administrator.
|
||||
|
||||
There are two ways to install Capsule:
|
||||
|
||||
* Use the Helm Chart available [here](./charts/capsule/README.md)
|
||||
* Use the [single YAML file installer](./config/install.yaml)
|
||||
|
||||
## Install with the single YAML file installer
|
||||
|
||||
Ensure you have `kubectl` installed in your `PATH`.
|
||||
|
||||
Clone this repository and move to the repo folder:
|
||||
|
||||
```
|
||||
$ kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/install.yaml
|
||||
```
|
||||
|
||||
It will install the Capsule controller in a dedicated namespace `capsule-system`.
|
||||
|
||||
## How to create Tenants
|
||||
Use the scaffold [Tenant](config/samples/capsule_v1beta1_tenant.yaml) and simply apply as cluster admin.
|
||||
|
||||
```
|
||||
$ kubectl apply -f config/samples/capsule_v1beta1_tenant.yaml
|
||||
tenant.capsule.clastix.io/gas created
|
||||
```
|
||||
|
||||
You can check the tenant just created as
|
||||
|
||||
```
|
||||
$ kubectl get tenants
|
||||
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
gas Active 3 0 {"kubernetes.io/os":"linux"} 25s
|
||||
```
|
||||
|
||||
## Tenant owners
|
||||
Each tenant comes with a delegated user or group of users acting as the tenant admin. In the Capsule jargon, this is called the _Tenant Owner_. Other users can operate inside a tenant with different levels of permissions and authorizations assigned directly by the Tenant Owner.
|
||||
|
||||
Capsule does not care about the authentication strategy used in the cluster and all the Kubernetes methods of [authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) are supported. The only requirement to use Capsule is to assign tenant users to the the group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
|
||||
|
||||
Assignment to a group depends on the authentication strategy in your cluster.
|
||||
|
||||
For example, if you are using `capsule.clastix.io`, users authenticated through a _X.509_ certificate must have `capsule.clastix.io` as _Organization_: `-subj "/CN=${USER}/O=capsule.clastix.io"`
|
||||
|
||||
Users authenticated through an _OIDC token_ must have in their token:
|
||||
|
||||
```json
|
||||
...
|
||||
"users_groups": [
|
||||
"capsule.clastix.io",
|
||||
"other_group"
|
||||
]
|
||||
```
|
||||
|
||||
The [hack/create-user.sh](hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `bob` user acting as owner of a tenant called `gas`
|
||||
|
||||
```bash
|
||||
./hack/create-user.sh bob gas
|
||||
...
|
||||
certificatesigningrequest.certificates.k8s.io/bob-gas created
|
||||
certificatesigningrequest.certificates.k8s.io/bob-gas approved
|
||||
kubeconfig file is: bob-gas.kubeconfig
|
||||
to use it as bob export KUBECONFIG=bob-gas.kubeconfig
|
||||
```
|
||||
|
||||
## Working with Tenants
|
||||
Log in to the Kubernetes cluster as `bob` tenant owner
|
||||
|
||||
```
|
||||
$ export KUBECONFIG=bob-gas.kubeconfig
|
||||
```
|
||||
|
||||
and create a couple of new namespaces
|
||||
|
||||
```
|
||||
$ kubectl create namespace gas-production
|
||||
$ kubectl create namespace gas-development
|
||||
```
|
||||
|
||||
As user `bob` you can operate with fully admin permissions:
|
||||
|
||||
```
|
||||
$ kubectl -n gas-development run nginx --image=docker.io/nginx
|
||||
$ kubectl -n gas-development get pods
|
||||
```
|
||||
|
||||
but limited to only your own namespaces:
|
||||
|
||||
```
|
||||
$ kubectl -n kube-system get pods
|
||||
Error from server (Forbidden): pods is forbidden:
|
||||
User "bob" cannot list resource "pods" in API group "" in the namespace "kube-system"
|
||||
```
|
||||
|
||||
# Documentation
|
||||
Please, check the project [documentation](./docs/index.md) for more cool things you can do with Capsule.
|
||||
|
||||
# Removal
|
||||
Similar to `deploy`, you can get rid of Capsule using the `remove` target.
|
||||
Please, check the project [documentation](https://capsule.clastix.io) for the cool things you can do with Capsule.
|
||||
|
||||
```
|
||||
$ make remove
|
||||
```
|
||||
# Contributions
|
||||
|
||||
Capsule is Open Source with Apache 2 license and any contribution is welcome.
|
||||
|
||||
## 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).
|
||||
|
||||
# Governance
|
||||
|
||||
You can find how the Capsule project is governed [here](https://capsule.clastix.io/docs/contributing/governance).
|
||||
|
||||
# FAQ
|
||||
|
||||
- Q. How to pronounce Capsule?
|
||||
|
||||
A. It should be pronounced as `/ˈkæpsjuːl/`.
|
||||
|
||||
- Q. Can I contribute?
|
||||
|
||||
A. Absolutely! Capsule is Open Source with Apache 2 license and any contribution is welcome. Please refer to the corresponding [section](./docs/operator/contributing.md) in the documentation.
|
||||
|
||||
- Q. Is it production grade?
|
||||
|
||||
A. Although under frequent development and improvements, Capsule is ready to be used in production environments as currently, people are using it in public and private deployments. Check out the [release](https://github.com/clastix/capsule/releases) page for a detailed list of available versions.
|
||||
|
||||
@@ -19,9 +19,12 @@ func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
|
||||
sort.SliceStable(in.Exact, func(i, j int) bool {
|
||||
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
|
||||
})
|
||||
|
||||
i := sort.SearchStrings(in.Exact, value)
|
||||
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,5 +32,6 @@ func (in AllowedListSpec) RegexMatch(value string) (ok bool) {
|
||||
if len(in.Regex) > 0 {
|
||||
ok = regexp.MustCompile(in.Regex).MatchString(value)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestAllowedListSpec_ExactMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{
|
||||
[]string{"foo", "bar", "bizz", "buzz"},
|
||||
@@ -35,9 +36,11 @@ func TestAllowedListSpec_ExactMatch(t *testing.T) {
|
||||
a := AllowedListSpec{
|
||||
Exact: tc.In,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.ExactMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.ExactMatch(ko))
|
||||
}
|
||||
@@ -50,6 +53,7 @@ func TestAllowedListSpec_RegexMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
|
||||
{``, nil, []string{"any", "value"}},
|
||||
@@ -57,9 +61,11 @@ func TestAllowedListSpec_RegexMatch(t *testing.T) {
|
||||
a := AllowedListSpec{
|
||||
Regex: tc.Regex,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.RegexMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.RegexMatch(ko))
|
||||
}
|
||||
|
||||
12
api/v1alpha1/capsuleconfiguration_annotations.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package v1alpha1
|
||||
|
||||
const (
|
||||
ForbiddenNodeLabelsAnnotation = "capsule.clastix.io/forbidden-node-labels"
|
||||
ForbiddenNodeLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-node-labels-regexp"
|
||||
ForbiddenNodeAnnotationsAnnotation = "capsule.clastix.io/forbidden-node-annotations"
|
||||
ForbiddenNodeAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-node-annotations-regexp"
|
||||
CASecretNameAnnotation = "capsule.clastix.io/ca-secret-name"
|
||||
TLSSecretNameAnnotation = "capsule.clastix.io/tls-secret-name"
|
||||
MutatingWebhookConfigurationName = "capsule.clastix.io/mutating-webhook-configuration-name"
|
||||
ValidatingWebhookConfigurationName = "capsule.clastix.io/validating-webhook-configuration-name"
|
||||
)
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// CapsuleConfigurationSpec defines the Capsule configuration
|
||||
// CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
type CapsuleConfigurationSpec struct {
|
||||
// Names of the groups for Capsule users.
|
||||
// +kubebuilder:default={capsule.clastix.io}
|
||||
@@ -23,7 +23,7 @@ type CapsuleConfigurationSpec struct {
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// CapsuleConfiguration is the Schema for the Capsule configuration API
|
||||
// CapsuleConfiguration is the Schema for the Capsule configuration API.
|
||||
type CapsuleConfiguration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -33,7 +33,7 @@ type CapsuleConfiguration struct {
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// CapsuleConfigurationList contains a list of CapsuleConfiguration
|
||||
// CapsuleConfigurationList contains a list of CapsuleConfiguration.
|
||||
type CapsuleConfigurationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -49,13 +49,13 @@ const (
|
||||
)
|
||||
|
||||
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
var serviceKindToAnnotationMap = map[capsulev1beta1.ProxyServiceKind][]string{
|
||||
serviceKindToAnnotationMap := map[capsulev1beta1.ProxyServiceKind][]string{
|
||||
capsulev1beta1.NodesProxy: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
|
||||
capsulev1beta1.StorageClassesProxy: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
|
||||
capsulev1beta1.IngressClassesProxy: {enableIngressClassListingAnnotation, enableIngressClassUpdateAnnotation, enableIngressClassDeletionAnnotation},
|
||||
capsulev1beta1.PriorityClassesProxy: {enablePriorityClassListingAnnotation, enablePriorityClassUpdateAnnotation, enablePriorityClassDeletionAnnotation},
|
||||
}
|
||||
var annotationToOperationMap = map[string]capsulev1beta1.ProxyOperation{
|
||||
annotationToOperationMap := map[string]capsulev1beta1.ProxyOperation{
|
||||
enableNodeListingAnnotation: capsulev1beta1.ListOperation,
|
||||
enableNodeUpdateAnnotation: capsulev1beta1.UpdateOperation,
|
||||
enableNodeDeletionAnnotation: capsulev1beta1.DeleteOperation,
|
||||
@@ -69,14 +69,15 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
enablePriorityClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
|
||||
enablePriorityClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
|
||||
}
|
||||
var annotationToOwnerKindMap = map[string]capsulev1beta1.OwnerKind{
|
||||
annotationToOwnerKindMap := map[string]capsulev1beta1.OwnerKind{
|
||||
ownerUsersAnnotation: capsulev1beta1.UserOwner,
|
||||
ownerGroupsAnnotation: capsulev1beta1.GroupOwner,
|
||||
ownerServiceAccountAnnotation: capsulev1beta1.ServiceAccountOwner,
|
||||
}
|
||||
|
||||
annotations := t.GetAnnotations()
|
||||
|
||||
var operations = make(map[string]map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
|
||||
operations := make(map[string]map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
|
||||
|
||||
for serviceKind, operationAnnotations := range serviceKindToAnnotationMap {
|
||||
for _, operationAnnotation := range operationAnnotations {
|
||||
@@ -86,6 +87,7 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
if _, exists := operations[owner]; !exists {
|
||||
operations[owner] = make(map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
|
||||
}
|
||||
|
||||
operations[owner][serviceKind] = append(operations[owner][serviceKind], annotationToOperationMap[operationAnnotation])
|
||||
}
|
||||
}
|
||||
@@ -94,7 +96,7 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
|
||||
var owners capsulev1beta1.OwnerListSpec
|
||||
|
||||
var getProxySettingsForOwner = func(ownerName string) (settings []capsulev1beta1.ProxySettings) {
|
||||
getProxySettingsForOwner := func(ownerName string) (settings []capsulev1beta1.ProxySettings) {
|
||||
ownerOperations, ok := operations[ownerName]
|
||||
if ok {
|
||||
for k, v := range ownerOperations {
|
||||
@@ -104,6 +106,7 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,8 +132,13 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
|
||||
return owners
|
||||
}
|
||||
|
||||
// nolint:gocognit,gocyclo,cyclop,maintidx
|
||||
func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
dst := dstRaw.(*capsulev1beta1.Tenant)
|
||||
dst, ok := dstRaw.(*capsulev1beta1.Tenant)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected type *capsulev1beta1.Tenant, got %T", dst)
|
||||
}
|
||||
|
||||
annotations := t.GetAnnotations()
|
||||
|
||||
// ObjectMeta
|
||||
@@ -141,6 +149,7 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
if dst.Spec.NamespaceOptions == nil {
|
||||
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
|
||||
}
|
||||
|
||||
dst.Spec.NamespaceOptions.Quota = t.Spec.NamespaceQuota
|
||||
}
|
||||
|
||||
@@ -152,11 +161,13 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
if dst.Spec.NamespaceOptions == nil {
|
||||
dst.Spec.NamespaceOptions = &capsulev1beta1.NamespaceOptions{}
|
||||
}
|
||||
|
||||
dst.Spec.NamespaceOptions.AdditionalMetadata = &capsulev1beta1.AdditionalMetadataSpec{
|
||||
Labels: t.Spec.NamespacesMetadata.AdditionalLabels,
|
||||
Annotations: t.Spec.NamespacesMetadata.AdditionalAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.ServicesMetadata != nil {
|
||||
if dst.Spec.ServiceOptions == nil {
|
||||
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
|
||||
@@ -167,13 +178,15 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.StorageClasses != nil {
|
||||
dst.Spec.StorageClasses = &capsulev1beta1.AllowedListSpec{
|
||||
Exact: t.Spec.StorageClasses.Exact,
|
||||
Regex: t.Spec.StorageClasses.Regex,
|
||||
}
|
||||
}
|
||||
if v, ok := t.Annotations[ingressHostnameCollisionScope]; ok {
|
||||
|
||||
if v, annotationOk := t.Annotations[ingressHostnameCollisionScope]; annotationOk {
|
||||
switch v {
|
||||
case string(capsulev1beta1.HostnameCollisionScopeCluster), string(capsulev1beta1.HostnameCollisionScopeTenant), string(capsulev1beta1.HostnameCollisionScopeNamespace):
|
||||
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScope(v)
|
||||
@@ -181,38 +194,44 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
dst.Spec.IngressOptions.HostnameCollisionScope = capsulev1beta1.HostnameCollisionScopeDisabled
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.IngressClasses != nil {
|
||||
dst.Spec.IngressOptions.AllowedClasses = &capsulev1beta1.AllowedListSpec{
|
||||
Exact: t.Spec.IngressClasses.Exact,
|
||||
Regex: t.Spec.IngressClasses.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.IngressHostnames != nil {
|
||||
dst.Spec.IngressOptions.AllowedHostnames = &capsulev1beta1.AllowedListSpec{
|
||||
Exact: t.Spec.IngressHostnames.Exact,
|
||||
Regex: t.Spec.IngressHostnames.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.ContainerRegistries != nil {
|
||||
dst.Spec.ContainerRegistries = &capsulev1beta1.AllowedListSpec{
|
||||
Exact: t.Spec.ContainerRegistries.Exact,
|
||||
Regex: t.Spec.ContainerRegistries.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Spec.NetworkPolicies) > 0 {
|
||||
dst.Spec.NetworkPolicies = &capsulev1beta1.NetworkPolicySpec{
|
||||
dst.Spec.NetworkPolicies = capsulev1beta1.NetworkPolicySpec{
|
||||
Items: t.Spec.NetworkPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Spec.LimitRanges) > 0 {
|
||||
dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{
|
||||
dst.Spec.LimitRanges = capsulev1beta1.LimitRangesSpec{
|
||||
Items: t.Spec.LimitRanges,
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Spec.ResourceQuota) > 0 {
|
||||
dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{
|
||||
dst.Spec.ResourceQuota = capsulev1beta1.ResourceQuotaSpec{
|
||||
Scope: func() capsulev1beta1.ResourceQuotaScope {
|
||||
if v, ok := t.GetAnnotations()[resourceQuotaScopeAnnotation]; ok {
|
||||
if v, annotationOk := t.GetAnnotations()[resourceQuotaScopeAnnotation]; annotationOk {
|
||||
switch v {
|
||||
case string(capsulev1beta1.ResourceQuotaScopeNamespace):
|
||||
return capsulev1beta1.ResourceQuotaScopeNamespace
|
||||
@@ -220,11 +239,13 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
return capsulev1beta1.ResourceQuotaScopeTenant
|
||||
}
|
||||
}
|
||||
|
||||
return capsulev1beta1.ResourceQuotaScopeTenant
|
||||
}(),
|
||||
Items: t.Spec.ResourceQuota,
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Spec.AdditionalRoleBindings) > 0 {
|
||||
for _, rb := range t.Spec.AdditionalRoleBindings {
|
||||
dst.Spec.AdditionalRoleBindings = append(dst.Spec.AdditionalRoleBindings, capsulev1beta1.AdditionalRoleBindingsSpec{
|
||||
@@ -233,10 +254,12 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if t.Spec.ExternalServiceIPs != nil {
|
||||
if dst.Spec.ServiceOptions == nil {
|
||||
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
|
||||
}
|
||||
|
||||
dst.Spec.ServiceOptions.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
|
||||
Allowed: make([]capsulev1beta1.AllowedIP, len(t.Spec.ExternalServiceIPs.Allowed)),
|
||||
}
|
||||
@@ -256,10 +279,13 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
priorityClasses := capsulev1beta1.AllowedListSpec{}
|
||||
|
||||
priorityClassAllowed, ok := annotations[podPriorityAllowedAnnotation]
|
||||
|
||||
if ok {
|
||||
priorityClasses.Exact = strings.Split(priorityClassAllowed, ",")
|
||||
}
|
||||
|
||||
priorityClassesRegexp, ok := annotations[podPriorityAllowedRegexAnnotation]
|
||||
|
||||
if ok {
|
||||
priorityClasses.Regex = priorityClassesRegexp
|
||||
}
|
||||
@@ -274,12 +300,15 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName()))
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions == nil {
|
||||
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions.AllowedServices == nil {
|
||||
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
|
||||
}
|
||||
|
||||
dst.Spec.ServiceOptions.AllowedServices.NodePort = pointer.BoolPtr(val)
|
||||
}
|
||||
|
||||
@@ -289,12 +318,15 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, t.GetName()))
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions == nil {
|
||||
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions.AllowedServices == nil {
|
||||
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
|
||||
}
|
||||
|
||||
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
|
||||
}
|
||||
|
||||
@@ -304,21 +336,22 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableLoadBalancerAnnotation, t.GetName()))
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions == nil {
|
||||
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
|
||||
}
|
||||
|
||||
if dst.Spec.ServiceOptions.AllowedServices == nil {
|
||||
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
|
||||
}
|
||||
|
||||
dst.Spec.ServiceOptions.AllowedServices.LoadBalancer = pointer.BoolPtr(val)
|
||||
}
|
||||
|
||||
// Status
|
||||
dst.Status = capsulev1beta1.TenantStatus{
|
||||
Size: t.Status.Size,
|
||||
Namespaces: t.Status.Namespaces,
|
||||
}
|
||||
|
||||
// Remove unneeded annotations
|
||||
delete(dst.ObjectMeta.Annotations, podAllowedImagePullPolicyAnnotation)
|
||||
delete(dst.ObjectMeta.Annotations, podPriorityAllowedAnnotation)
|
||||
@@ -347,14 +380,15 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:gocognit,gocyclo,cyclop
|
||||
func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
|
||||
var ownersAnnotations = map[string][]string{
|
||||
ownersAnnotations := map[string][]string{
|
||||
ownerGroupsAnnotation: nil,
|
||||
ownerUsersAnnotation: nil,
|
||||
ownerServiceAccountAnnotation: nil,
|
||||
}
|
||||
|
||||
var proxyAnnotations = map[string][]string{
|
||||
proxyAnnotations := map[string][]string{
|
||||
enableNodeListingAnnotation: nil,
|
||||
enableNodeUpdateAnnotation: nil,
|
||||
enableNodeDeletionAnnotation: nil,
|
||||
@@ -382,6 +416,7 @@ func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
|
||||
ownersAnnotations[ownerServiceAccountAnnotation] = append(ownersAnnotations[ownerServiceAccountAnnotation], owner.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, setting := range owner.ProxyOperations {
|
||||
switch setting.Kind {
|
||||
case capsulev1beta1.NodesProxy:
|
||||
@@ -437,6 +472,7 @@ func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
|
||||
t.Annotations[k] = strings.Join(v, ",")
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range proxyAnnotations {
|
||||
if len(v) > 0 {
|
||||
t.Annotations[k] = strings.Join(v, ",")
|
||||
@@ -444,8 +480,12 @@ func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gocyclo,cyclop
|
||||
func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
src := srcRaw.(*capsulev1beta1.Tenant)
|
||||
src, ok := srcRaw.(*capsulev1beta1.Tenant)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected *capsulev1beta1.Tenant, got %T", srcRaw)
|
||||
}
|
||||
|
||||
// ObjectMeta
|
||||
t.ObjectMeta = src.ObjectMeta
|
||||
@@ -469,47 +509,57 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
AdditionalAnnotations: src.Spec.NamespaceOptions.AdditionalMetadata.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
|
||||
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
|
||||
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.Labels,
|
||||
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.StorageClasses != nil {
|
||||
t.Spec.StorageClasses = &AllowedListSpec{
|
||||
Exact: src.Spec.StorageClasses.Exact,
|
||||
Regex: src.Spec.StorageClasses.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
t.Annotations[ingressHostnameCollisionScope] = string(src.Spec.IngressOptions.HostnameCollisionScope)
|
||||
|
||||
if src.Spec.IngressOptions.AllowedClasses != nil {
|
||||
t.Spec.IngressClasses = &AllowedListSpec{
|
||||
Exact: src.Spec.IngressOptions.AllowedClasses.Exact,
|
||||
Regex: src.Spec.IngressOptions.AllowedClasses.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.IngressOptions.AllowedHostnames != nil {
|
||||
t.Spec.IngressHostnames = &AllowedListSpec{
|
||||
Exact: src.Spec.IngressOptions.AllowedHostnames.Exact,
|
||||
Regex: src.Spec.IngressOptions.AllowedHostnames.Regex,
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.ContainerRegistries != nil {
|
||||
t.Spec.ContainerRegistries = &AllowedListSpec{
|
||||
Exact: src.Spec.ContainerRegistries.Exact,
|
||||
Regex: src.Spec.ContainerRegistries.Regex,
|
||||
}
|
||||
}
|
||||
if src.Spec.NetworkPolicies != nil {
|
||||
|
||||
if len(src.Spec.NetworkPolicies.Items) > 0 {
|
||||
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
|
||||
}
|
||||
if src.Spec.LimitRanges != nil {
|
||||
|
||||
if len(src.Spec.LimitRanges.Items) > 0 {
|
||||
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
|
||||
}
|
||||
if src.Spec.ResourceQuota != nil {
|
||||
|
||||
if len(src.Spec.ResourceQuota.Items) > 0 {
|
||||
t.Annotations[resourceQuotaScopeAnnotation] = string(src.Spec.ResourceQuota.Scope)
|
||||
t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items
|
||||
}
|
||||
|
||||
if len(src.Spec.AdditionalRoleBindings) > 0 {
|
||||
for _, rb := range src.Spec.AdditionalRoleBindings {
|
||||
t.Spec.AdditionalRoleBindings = append(t.Spec.AdditionalRoleBindings, AdditionalRoleBindingsSpec{
|
||||
@@ -518,6 +568,7 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.ExternalServiceIPs != nil {
|
||||
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
|
||||
Allowed: make([]AllowedIP, len(src.Spec.ServiceOptions.ExternalServiceIPs.Allowed)),
|
||||
@@ -527,11 +578,14 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
t.Spec.ExternalServiceIPs.Allowed[i] = AllowedIP(IP)
|
||||
}
|
||||
}
|
||||
|
||||
if len(src.Spec.ImagePullPolicies) != 0 {
|
||||
var pullPolicies []string
|
||||
|
||||
for _, policy := range src.Spec.ImagePullPolicies {
|
||||
pullPolicies = append(pullPolicies, string(policy))
|
||||
}
|
||||
|
||||
t.Annotations[podAllowedImagePullPolicyAnnotation] = strings.Join(pullPolicies, ",")
|
||||
}
|
||||
|
||||
@@ -539,15 +593,24 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
|
||||
if len(src.Spec.PriorityClasses.Exact) != 0 {
|
||||
t.Annotations[podPriorityAllowedAnnotation] = strings.Join(src.Spec.PriorityClasses.Exact, ",")
|
||||
}
|
||||
|
||||
if src.Spec.PriorityClasses.Regex != "" {
|
||||
t.Annotations[podPriorityAllowedRegexAnnotation] = src.Spec.PriorityClasses.Regex
|
||||
}
|
||||
}
|
||||
|
||||
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AllowedServices != nil {
|
||||
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
|
||||
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
|
||||
t.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
|
||||
if src.Spec.ServiceOptions.AllowedServices.NodePort != nil {
|
||||
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
|
||||
}
|
||||
|
||||
if src.Spec.ServiceOptions.AllowedServices.ExternalName != nil {
|
||||
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
|
||||
}
|
||||
|
||||
if src.Spec.ServiceOptions.AllowedServices.LoadBalancer != nil {
|
||||
t.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
// nolint:maintidx
|
||||
func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
var namespaceQuota int32 = 5
|
||||
var nodeSelector = map[string]string{
|
||||
|
||||
nodeSelector := map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
var v1alpha1AdditionalMetadataSpec = &AdditionalMetadataSpec{
|
||||
v1alpha1AdditionalMetadataSpec := &AdditionalMetadataSpec{
|
||||
AdditionalLabels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
@@ -31,11 +33,11 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
var v1alpha1AllowedListSpec = &AllowedListSpec{
|
||||
v1alpha1AllowedListSpec := &AllowedListSpec{
|
||||
Exact: []string{"foo", "bar"},
|
||||
Regex: "^foo*",
|
||||
}
|
||||
var v1beta1AdditionalMetadataSpec = &capsulev1beta1.AdditionalMetadataSpec{
|
||||
v1beta1AdditionalMetadataSpec := &capsulev1beta1.AdditionalMetadataSpec{
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
@@ -43,11 +45,11 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
var v1beta1NamespaceOptions = &capsulev1beta1.NamespaceOptions{
|
||||
v1beta1NamespaceOptions := &capsulev1beta1.NamespaceOptions{
|
||||
Quota: &namespaceQuota,
|
||||
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
|
||||
}
|
||||
var v1beta1ServiceOptions = &capsulev1beta1.ServiceOptions{
|
||||
v1beta1ServiceOptions := &capsulev1beta1.ServiceOptions{
|
||||
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
|
||||
AllowedServices: &capsulev1beta1.AllowedServices{
|
||||
NodePort: pointer.BoolPtr(false),
|
||||
@@ -58,11 +60,11 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
|
||||
},
|
||||
}
|
||||
var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{
|
||||
v1beta1AllowedListSpec := &capsulev1beta1.AllowedListSpec{
|
||||
Exact: []string{"foo", "bar"},
|
||||
Regex: "^foo*",
|
||||
}
|
||||
var networkPolicies = []networkingv1.NetworkPolicySpec{
|
||||
networkPolicies := []networkingv1.NetworkPolicySpec{
|
||||
{
|
||||
Ingress: []networkingv1.NetworkPolicyIngressRule{
|
||||
{
|
||||
@@ -87,7 +89,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var limitRanges = []corev1.LimitRangeSpec{
|
||||
limitRanges := []corev1.LimitRangeSpec{
|
||||
{
|
||||
Limits: []corev1.LimitRangeItem{
|
||||
{
|
||||
@@ -104,7 +106,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var resourceQuotas = []corev1.ResourceQuotaSpec{
|
||||
resourceQuotas := []corev1.ResourceQuotaSpec{
|
||||
{
|
||||
Hard: map[corev1.ResourceName]resource.Quantity{
|
||||
corev1.ResourceLimitsCPU: resource.MustParse("8"),
|
||||
@@ -118,7 +120,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
}
|
||||
|
||||
var v1beta1Tnt = capsulev1beta1.Tenant{
|
||||
v1beta1Tnt := capsulev1beta1.Tenant{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "alice",
|
||||
@@ -240,13 +242,13 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
ContainerRegistries: v1beta1AllowedListSpec,
|
||||
NodeSelector: nodeSelector,
|
||||
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{
|
||||
NetworkPolicies: capsulev1beta1.NetworkPolicySpec{
|
||||
Items: networkPolicies,
|
||||
},
|
||||
LimitRanges: &capsulev1beta1.LimitRangesSpec{
|
||||
LimitRanges: capsulev1beta1.LimitRangesSpec{
|
||||
Items: limitRanges,
|
||||
},
|
||||
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{
|
||||
ResourceQuota: capsulev1beta1.ResourceQuotaSpec{
|
||||
Scope: capsulev1beta1.ResourceQuotaScopeNamespace,
|
||||
Items: resourceQuotas,
|
||||
},
|
||||
@@ -256,7 +258,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "Group",
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: "system:authenticated",
|
||||
},
|
||||
},
|
||||
@@ -274,7 +276,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
},
|
||||
}
|
||||
|
||||
var v1alpha1Tnt = Tenant{
|
||||
v1alpha1Tnt := Tenant{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "alice",
|
||||
@@ -327,7 +329,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "Group",
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: "system:authenticated",
|
||||
},
|
||||
},
|
||||
@@ -347,10 +349,11 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
|
||||
}
|
||||
|
||||
func TestConversionHub_ConvertTo(t *testing.T) {
|
||||
var v1beta1ConvertedTnt = capsulev1beta1.Tenant{}
|
||||
v1beta1ConvertedTnt := capsulev1beta1.Tenant{}
|
||||
|
||||
v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs()
|
||||
err := v1alpha1Tnt.ConvertTo(&v1beta1ConvertedTnt)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
sort.Slice(v1beta1tnt.Spec.Owners, func(i, j int) bool {
|
||||
return v1beta1tnt.Spec.Owners[i].Name < v1beta1tnt.Spec.Owners[j].Name
|
||||
@@ -364,17 +367,20 @@ func TestConversionHub_ConvertTo(t *testing.T) {
|
||||
return owner.ProxyOperations[i].Kind < owner.ProxyOperations[j].Kind
|
||||
})
|
||||
}
|
||||
|
||||
for _, owner := range v1beta1ConvertedTnt.Spec.Owners {
|
||||
sort.Slice(owner.ProxyOperations, func(i, j int) bool {
|
||||
return owner.ProxyOperations[i].Kind < owner.ProxyOperations[j].Kind
|
||||
})
|
||||
}
|
||||
|
||||
assert.Equal(t, v1beta1tnt, v1beta1ConvertedTnt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversionHub_ConvertFrom(t *testing.T) {
|
||||
var v1alpha1ConvertedTnt = Tenant{}
|
||||
v1alpha1ConvertedTnt := Tenant{}
|
||||
|
||||
v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs()
|
||||
|
||||
err := v1alpha1ConvertedTnt.ConvertFrom(&v1beta1tnt)
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
// GroupVersion is group version used to register these objects.
|
||||
GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1alpha1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// OwnerSpec defines tenant owner name and kind
|
||||
// OwnerSpec defines tenant owner name and kind.
|
||||
type OwnerSpec struct {
|
||||
Name string `json:"name"`
|
||||
Kind Kind `json:"kind"`
|
||||
|
||||
@@ -13,6 +13,7 @@ func (t *Tenant) IsCordoned() bool {
|
||||
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,16 +22,19 @@ func (t *Tenant) IsFull() bool {
|
||||
if t.Spec.NamespaceQuota == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
|
||||
}
|
||||
|
||||
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
|
||||
var l []string
|
||||
|
||||
for _, ns := range namespaces {
|
||||
if ns.Status.Phase == corev1.NamespaceActive {
|
||||
l = append(l, ns.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(l)
|
||||
|
||||
t.Status.Namespaces = l
|
||||
|
||||
@@ -27,5 +27,6 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
|
||||
default:
|
||||
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// TenantSpec defines the desired state of Tenant
|
||||
// TenantSpec defines the desired state of Tenant.
|
||||
type TenantSpec struct {
|
||||
Owner OwnerSpec `json:"owner"`
|
||||
|
||||
@@ -29,7 +29,7 @@ type TenantSpec struct {
|
||||
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
|
||||
}
|
||||
|
||||
// TenantStatus defines the observed state of Tenant
|
||||
// TenantStatus defines the observed state of Tenant.
|
||||
type TenantStatus struct {
|
||||
Size uint `json:"size"`
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
@@ -45,7 +45,7 @@ type TenantStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// Tenant is the Schema for the tenants API
|
||||
// Tenant is the Schema for the tenants API.
|
||||
type Tenant struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -56,7 +56,7 @@ type Tenant struct {
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// TenantList contains a list of Tenant
|
||||
// TenantList contains a list of Tenant.
|
||||
type TenantList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
|
||||
@@ -19,9 +19,12 @@ func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
|
||||
sort.SliceStable(in.Exact, func(i, j int) bool {
|
||||
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
|
||||
})
|
||||
|
||||
i := sort.SearchStrings(in.Exact, value)
|
||||
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,5 +32,6 @@ func (in AllowedListSpec) RegexMatch(value string) (ok bool) {
|
||||
if len(in.Regex) > 0 {
|
||||
ok = regexp.MustCompile(in.Regex).MatchString(value)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestAllowedListSpec_ExactMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{
|
||||
[]string{"foo", "bar", "bizz", "buzz"},
|
||||
@@ -35,9 +36,11 @@ func TestAllowedListSpec_ExactMatch(t *testing.T) {
|
||||
a := AllowedListSpec{
|
||||
Exact: tc.In,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.ExactMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.ExactMatch(ko))
|
||||
}
|
||||
@@ -50,6 +53,7 @@ func TestAllowedListSpec_RegexMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
|
||||
{``, nil, []string{"any", "value"}},
|
||||
@@ -57,9 +61,11 @@ func TestAllowedListSpec_RegexMatch(t *testing.T) {
|
||||
a := AllowedListSpec{
|
||||
Regex: tc.Regex,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.RegexMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.RegexMatch(ko))
|
||||
}
|
||||
|
||||
59
api/v1beta1/custom_resource_quota.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceQuotaAnnotationPrefix = "quota.resources.capsule.clastix.io"
|
||||
ResourceUsedAnnotationPrefix = "used.resources.capsule.clastix.io"
|
||||
)
|
||||
|
||||
func UsedAnnotationForResource(kindGroup string) string {
|
||||
return fmt.Sprintf("%s/%s", ResourceUsedAnnotationPrefix, kindGroup)
|
||||
}
|
||||
|
||||
func LimitAnnotationForResource(kindGroup string) string {
|
||||
return fmt.Sprintf("%s/%s", ResourceQuotaAnnotationPrefix, kindGroup)
|
||||
}
|
||||
|
||||
func GetUsedResourceFromTenant(tenant Tenant, kindGroup string) (int64, error) {
|
||||
usedStr, ok := tenant.GetAnnotations()[UsedAnnotationForResource(kindGroup)]
|
||||
if !ok {
|
||||
usedStr = "0"
|
||||
}
|
||||
|
||||
used, _ := strconv.ParseInt(usedStr, 10, 10)
|
||||
|
||||
return used, nil
|
||||
}
|
||||
|
||||
type NonLimitedResourceError struct {
|
||||
kindGroup string
|
||||
}
|
||||
|
||||
func NewNonLimitedResourceError(kindGroup string) *NonLimitedResourceError {
|
||||
return &NonLimitedResourceError{kindGroup: kindGroup}
|
||||
}
|
||||
|
||||
func (n NonLimitedResourceError) Error() string {
|
||||
return fmt.Sprintf("resource %s is not limited for the current tenant", n.kindGroup)
|
||||
}
|
||||
|
||||
func GetLimitResourceFromTenant(tenant Tenant, kindGroup string) (int64, error) {
|
||||
limitStr, ok := tenant.GetAnnotations()[LimitAnnotationForResource(kindGroup)]
|
||||
if !ok {
|
||||
return 0, NewNonLimitedResourceError(kindGroup)
|
||||
}
|
||||
|
||||
limit, err := strconv.ParseInt(limitStr, 10, 10)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("resource %s limit cannot be parsed, %w", kindGroup, err)
|
||||
}
|
||||
|
||||
return limit, nil
|
||||
}
|
||||
@@ -11,5 +11,6 @@ func (t *Tenant) IsWildcardDenied() bool {
|
||||
if v, ok := t.Annotations[denyWildcard]; ok && v == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@ func (in *ForbiddenListSpec) ExactMatch(value string) (ok bool) {
|
||||
sort.SliceStable(in.Exact, func(i, j int) bool {
|
||||
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
|
||||
})
|
||||
|
||||
i := sort.SearchStrings(in.Exact, value)
|
||||
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,5 +32,6 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
|
||||
if len(in.Regex) > 0 {
|
||||
ok = regexp.MustCompile(in.Regex).MatchString(value)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestForbiddenListSpec_ExactMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{
|
||||
[]string{"foo", "bar", "bizz", "buzz"},
|
||||
@@ -35,9 +36,11 @@ func TestForbiddenListSpec_ExactMatch(t *testing.T) {
|
||||
a := ForbiddenListSpec{
|
||||
Exact: tc.In,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.ExactMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.ExactMatch(ko))
|
||||
}
|
||||
@@ -50,6 +53,7 @@ func TestForbiddenListSpec_RegexMatch(t *testing.T) {
|
||||
True []string
|
||||
False []string
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{`first-\w+-pattern`, []string{"first-date-pattern", "first-year-pattern"}, []string{"broken", "first-year", "second-date-pattern"}},
|
||||
{``, nil, []string{"any", "value"}},
|
||||
@@ -57,9 +61,11 @@ func TestForbiddenListSpec_RegexMatch(t *testing.T) {
|
||||
a := ForbiddenListSpec{
|
||||
Regex: tc.Regex,
|
||||
}
|
||||
|
||||
for _, ok := range tc.True {
|
||||
assert.True(t, a.RegexMatch(ok))
|
||||
}
|
||||
|
||||
for _, ko := range tc.False {
|
||||
assert.False(t, a.RegexMatch(ko))
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
// GroupVersion is group version used to register these objects.
|
||||
GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1beta1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
|
||||
@@ -14,9 +14,11 @@ func (t *Tenant) hasForbiddenNamespaceLabelsAnnotations() bool {
|
||||
if _, ok := t.Annotations[ForbiddenNamespaceLabelsAnnotation]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -24,9 +26,11 @@ func (t *Tenant) hasForbiddenNamespaceAnnotationsAnnotations() bool {
|
||||
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsAnnotation]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -34,6 +38,7 @@ func (t *Tenant) ForbiddenUserNamespaceLabels() *ForbiddenListSpec {
|
||||
if !t.hasForbiddenNamespaceLabelsAnnotations() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ForbiddenListSpec{
|
||||
Exact: strings.Split(t.Annotations[ForbiddenNamespaceLabelsAnnotation], ","),
|
||||
Regex: t.Annotations[ForbiddenNamespaceLabelsRegexpAnnotation],
|
||||
@@ -44,6 +49,7 @@ func (t *Tenant) ForbiddenUserNamespaceAnnotations() *ForbiddenListSpec {
|
||||
if !t.hasForbiddenNamespaceAnnotationsAnnotations() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ForbiddenListSpec{
|
||||
Exact: strings.Split(t.Annotations[ForbiddenNamespaceAnnotationsAnnotation], ","),
|
||||
Regex: t.Annotations[ForbiddenNamespaceAnnotationsRegexpAnnotation],
|
||||
|
||||
@@ -15,6 +15,7 @@ func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
|
||||
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
|
||||
return o[i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,12 +24,15 @@ type ByKindAndName OwnerListSpec
|
||||
func (b ByKindAndName) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func (b ByKindAndName) Less(i, j int) bool {
|
||||
if b[i].Kind.String() != b[j].Kind.String() {
|
||||
return b[i].Kind.String() < b[j].Kind.String()
|
||||
}
|
||||
|
||||
return b[i].Name < b[j].Name
|
||||
}
|
||||
|
||||
func (b ByKindAndName) Swap(i, j int) {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
var bla = OwnerSpec{
|
||||
bla := OwnerSpec{
|
||||
Kind: UserOwner,
|
||||
Name: "bla",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -17,7 +17,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var bar = OwnerSpec{
|
||||
bar := OwnerSpec{
|
||||
Kind: GroupOwner,
|
||||
Name: "bar",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -27,7 +27,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var baz = OwnerSpec{
|
||||
baz := OwnerSpec{
|
||||
Kind: UserOwner,
|
||||
Name: "baz",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -37,7 +37,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var fim = OwnerSpec{
|
||||
fim := OwnerSpec{
|
||||
Kind: ServiceAccountOwner,
|
||||
Name: "fim",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -47,7 +47,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var bom = OwnerSpec{
|
||||
bom := OwnerSpec{
|
||||
Kind: GroupOwner,
|
||||
Name: "bom",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -61,7 +61,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var qip = OwnerSpec{
|
||||
qip := OwnerSpec{
|
||||
Kind: ServiceAccountOwner,
|
||||
Name: "qip",
|
||||
ProxyOperations: []ProxySettings{
|
||||
@@ -71,7 +71,7 @@ func TestOwnerListSpec_FindOwner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
var owners = OwnerListSpec{bom, qip, bla, bar, baz, fim}
|
||||
owners := OwnerListSpec{bom, qip, bla, bar, baz, fim}
|
||||
|
||||
assert.Equal(t, owners.FindOwner("bom", GroupOwner), bom)
|
||||
assert.Equal(t, owners.FindOwner("qip", ServiceAccountOwner), qip)
|
||||
|
||||
23
api/v1beta1/owner_role.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ClusterRoleNamesAnnotation = "clusterrolenames.capsule.clastix.io"
|
||||
)
|
||||
|
||||
func (in OwnerSpec) GetRoles(tenant Tenant) []string {
|
||||
for key, value := range tenant.GetAnnotations() {
|
||||
if key == fmt.Sprintf("%s/%s.%s", ClusterRoleNamesAnnotation, strings.ToLower(in.Kind.String()), strings.ToLower(in.Name)) {
|
||||
return strings.Split(value, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return []string{"admin", "capsule-namespace-deleter"}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package v1beta1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,12 +19,13 @@ const (
|
||||
ForbiddenNamespaceLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-labels-regexp"
|
||||
ForbiddenNamespaceAnnotationsAnnotation = "capsule.clastix.io/forbidden-namespace-annotations"
|
||||
ForbiddenNamespaceAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-annotations-regexp"
|
||||
ProtectedTenantAnnotation = "capsule.clastix.io/protected"
|
||||
)
|
||||
|
||||
func UsedQuotaFor(resource fmt.Stringer) string {
|
||||
return "quota.capsule.clastix.io/used-" + resource.String()
|
||||
return "quota.capsule.clastix.io/used-" + strings.ReplaceAll(resource.String(), "/", "_")
|
||||
}
|
||||
|
||||
func HardQuotaFor(resource fmt.Stringer) string {
|
||||
return "quota.capsule.clastix.io/hard-" + resource.String()
|
||||
return "quota.capsule.clastix.io/hard-" + strings.ReplaceAll(resource.String(), "/", "_")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func (t *Tenant) IsCordoned() bool {
|
||||
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,16 +22,19 @@ func (t *Tenant) IsFull() bool {
|
||||
if t.Spec.NamespaceOptions == nil || t.Spec.NamespaceOptions.Quota == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceOptions.Quota)
|
||||
}
|
||||
|
||||
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
|
||||
var l []string
|
||||
|
||||
for _, ns := range namespaces {
|
||||
if ns.Status.Phase == corev1.NamespaceActive {
|
||||
l = append(l, ns.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(l)
|
||||
|
||||
t.Status.Namespaces = l
|
||||
|
||||
@@ -27,5 +27,6 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
|
||||
default:
|
||||
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const (
|
||||
TenantStateCordoned tenantState = "Cordoned"
|
||||
)
|
||||
|
||||
// Returns the observed state of the Tenant
|
||||
// Returns the observed state of the Tenant.
|
||||
type TenantStatus struct {
|
||||
//+kubebuilder:default=Active
|
||||
// The operational state of the Tenant. Possible values are "Active", "Cordoned".
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// TenantSpec defines the desired state of Tenant
|
||||
// TenantSpec defines the desired state of Tenant.
|
||||
type TenantSpec struct {
|
||||
// Specifies the owners of the Tenant. Mandatory.
|
||||
Owners OwnerListSpec `json:"owners"`
|
||||
@@ -21,14 +21,14 @@ type TenantSpec struct {
|
||||
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
|
||||
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
|
||||
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
|
||||
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
NetworkPolicies *NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
LimitRanges *LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
NetworkPolicies NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
LimitRanges LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
|
||||
ResourceQuota *ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
ResourceQuota ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
|
||||
// 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.
|
||||
@@ -47,7 +47,7 @@ type TenantSpec struct {
|
||||
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// Tenant is the Schema for the tenants API
|
||||
// Tenant is the Schema for the tenants API.
|
||||
type Tenant struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@@ -60,7 +60,7 @@ func (t *Tenant) Hub() {}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
// TenantList contains a list of Tenant
|
||||
// TenantList contains a list of Tenant.
|
||||
type TenantList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
@@ -480,21 +481,9 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.NetworkPolicies != nil {
|
||||
in, out := &in.NetworkPolicies, &out.NetworkPolicies
|
||||
*out = new(NetworkPolicySpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.LimitRanges != nil {
|
||||
in, out := &in.LimitRanges, &out.LimitRanges
|
||||
*out = new(LimitRangesSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ResourceQuota != nil {
|
||||
in, out := &in.ResourceQuota, &out.ResourceQuota
|
||||
*out = new(ResourceQuotaSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
|
||||
in.LimitRanges.DeepCopyInto(&out.LimitRanges)
|
||||
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
|
||||
if in.AdditionalRoleBindings != nil {
|
||||
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
|
||||
*out = make([]AdditionalRoleBindingsSpec, len(*in))
|
||||
|
||||
@@ -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.1
|
||||
version: 0.1.9
|
||||
|
||||
# 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.0
|
||||
appVersion: 0.1.1
|
||||
|
||||
@@ -24,23 +24,19 @@ The Capsule Operator Chart can be used to instantly deploy the Capsule Operator
|
||||
|
||||
$ helm repo add clastix https://clastix.github.io/charts
|
||||
|
||||
2. Create the Namespace:
|
||||
2. Install the Chart:
|
||||
|
||||
$ kubectl create namespace capsule-system
|
||||
$ helm install capsule clastix/capsule -n capsule-system --create-namespace
|
||||
|
||||
3. Install the Chart:
|
||||
|
||||
$ helm install capsule clastix/capsule -n capsule-system
|
||||
|
||||
4. Show the status:
|
||||
3. Show the status:
|
||||
|
||||
$ helm status capsule -n capsule-system
|
||||
|
||||
5. Upgrade the Chart
|
||||
4. Upgrade the Chart
|
||||
|
||||
$ helm upgrade capsule clastix/capsule -n capsule-system
|
||||
|
||||
6. Uninstall the Chart
|
||||
5. Uninstall the Chart
|
||||
|
||||
$ helm uninstall capsule -n capsule-system
|
||||
|
||||
@@ -62,44 +58,46 @@ If you only need to make minor customizations, you can specify them on the comma
|
||||
|
||||
Here the values you can override:
|
||||
|
||||
Parameter | Description | Default
|
||||
--- | --- | ---
|
||||
`manager.hostNetwork` | Specifies if the container should be started in `hostNetwork` mode. | `false`
|
||||
`manager.options.logLevel` | Set the log verbosity of the controller with a value from 1 to 10.| `4`
|
||||
Parameter | Description | Default
|
||||
--- |-----------------------------------------------------------------------------------------------------------------------------------------| ---
|
||||
`manager.hostNetwork` | Specifies if the container should be started in `hostNetwork` mode. | `false`
|
||||
`manager.options.logLevel` | Set the log verbosity of the controller with a value from 1 to 10. | `4`
|
||||
`manager.options.forceTenantPrefix` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash | `false`
|
||||
`manager.options.capsuleUserGroups` | Override the Capsule user groups | `[capsule.clastix.io]`
|
||||
`manager.options.protectedNamespaceRegex` | If specified, disallows creation of namespaces matching the passed regexp | `null`
|
||||
`manager.image.repository` | Set the image repository of the controller. | `quay.io/clastix/capsule`
|
||||
`manager.image.tag` | Overrides the image tag whose default is the chart. `appVersion` | `null`
|
||||
`manager.image.pullPolicy` | Set the image pull policy. | `IfNotPresent`
|
||||
`manager.livenessProbe` | Configure the liveness probe using Deployment probe spec | `GET :10080/healthz`
|
||||
`manager.readinessProbe` | Configure the readiness probe using Deployment probe spec | `GET :10080/readyz`
|
||||
`manager.resources.requests/cpu` | Set the CPU requests assigned to the controller. | `200m`
|
||||
`manager.resources.requests/memory` | Set the memory requests assigned to the controller. | `128Mi`
|
||||
`manager.resources.limits/cpu` | Set the CPU limits assigned to the controller. | `200m`
|
||||
`manager.resources.limits/cpu` | Set the memory limits assigned to the controller. | `128Mi`
|
||||
`mutatingWebhooksTimeoutSeconds` | Timeout in seconds for mutating webhooks. | `30`
|
||||
`validatingWebhooksTimeoutSeconds` | Timeout in seconds for validating webhooks. | `30`
|
||||
`imagePullSecrets` | Configuration for `imagePullSecrets` so that you can use a private images registry. | `[]`
|
||||
`serviceAccount.create` | Specifies whether a service account should be created. | `true`
|
||||
`serviceAccount.annotations` | Annotations to add to the service account. | `{}`
|
||||
`serviceAccount.name` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template | `capsule`
|
||||
`podAnnotations` | Annotations to add to the Capsule pod. | `{}`
|
||||
`priorityClassName` | Set the priority class name of the Capsule pod. | `null`
|
||||
`nodeSelector` | Set the node selector for the Capsule pod. | `{}`
|
||||
`tolerations` | Set list of tolerations for the Capsule pod. | `[]`
|
||||
`replicaCount` | Set the replica count for Capsule pod. | `1`
|
||||
`affinity` | Set affinity rules for the Capsule pod. | `{}`
|
||||
`podSecurityPolicy.enabled` | Specify if a Pod Security Policy must be created. | `false`
|
||||
`serviceMonitor.enabled` | Specifies if a service monitor must be created. | `false`
|
||||
`serviceMonitor.labels` | Additional labels which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.annotations` | Additional annotations which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.matchLabels` | Additional matchLabels which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.serviceAccount.name` | Specifies service account name for metrics scrape. | `capsule`
|
||||
`serviceMonitor.serviceAccount.namespace` | Specifies service account namespace for metrics scrape. | `capsule-system`
|
||||
`customLabels` | Additional labels which will be added to all resources created by Capsule helm chart . | `{}`
|
||||
`customAnnotations` | Additional annotations which will be added to all resources created by Capsule helm chart . | `{}`
|
||||
|
||||
`manager.options.capsuleUserGroups` | Override the Capsule user groups | `[capsule.clastix.io]`
|
||||
`manager.options.protectedNamespaceRegex` | If specified, disallows creation of namespaces matching the passed regexp | `null`
|
||||
`manager.options.enableSecretController` | Boolean, enables apsule secret controller which reconciles TLS and CA secrets for capsule webhooks. | `true`
|
||||
`manager.image.repository` | Set the image repository of the controller. | `quay.io/clastix/capsule`
|
||||
`manager.image.tag` | Overrides the image tag whose default is the chart. `appVersion` | `null`
|
||||
`manager.image.pullPolicy` | Set the image pull policy. | `IfNotPresent`
|
||||
`manager.livenessProbe` | Configure the liveness probe using Deployment probe spec | `GET :10080/healthz`
|
||||
`manager.readinessProbe` | Configure the readiness probe using Deployment probe spec | `GET :10080/readyz`
|
||||
`manager.resources.requests/cpu` | Set the CPU requests assigned to the controller. | `200m`
|
||||
`manager.resources.requests/memory` | Set the memory requests assigned to the controller. | `128Mi`
|
||||
`manager.resources.limits/cpu` | Set the CPU limits assigned to the controller. | `200m`
|
||||
`manager.resources.limits/cpu` | Set the memory limits assigned to the controller. | `128Mi`
|
||||
`mutatingWebhooksTimeoutSeconds` | Timeout in seconds for mutating webhooks. | `30`
|
||||
`validatingWebhooksTimeoutSeconds` | Timeout in seconds for validating webhooks. | `30`
|
||||
`webhooks` | Additional configuration for capsule webhooks. |
|
||||
`imagePullSecrets` | Configuration for `imagePullSecrets` so that you can use a private images registry. | `[]`
|
||||
`serviceAccount.create` | Specifies whether a service account should be created. | `true`
|
||||
`serviceAccount.annotations` | Annotations to add to the service account. | `{}`
|
||||
`serviceAccount.name` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template | `capsule`
|
||||
`podAnnotations` | Annotations to add to the Capsule pod. | `{}`
|
||||
`priorityClassName` | Set the priority class name of the Capsule pod. | `null`
|
||||
`nodeSelector` | Set the node selector for the Capsule pod. | `{}`
|
||||
`tolerations` | Set list of tolerations for the Capsule pod. | `[]`
|
||||
`replicaCount` | Set the replica count for Capsule pod. | `1`
|
||||
`affinity` | Set affinity rules for the Capsule pod. | `{}`
|
||||
`podSecurityPolicy.enabled` | Specify if a Pod Security Policy must be created. | `false`
|
||||
`serviceMonitor.enabled` | Specifies if a service monitor must be created. | `false`
|
||||
`serviceMonitor.labels` | Additional labels which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.annotations` | Additional annotations which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.matchLabels` | Additional matchLabels which will be added to service monitor. | `{}`
|
||||
`serviceMonitor.serviceAccount.name` | Specifies service account name for metrics scrape. | `capsule`
|
||||
`serviceMonitor.serviceAccount.namespace` | Specifies service account namespace for metrics scrape. | `capsule-system`
|
||||
`customLabels` | Additional labels which will be added to all resources created by Capsule helm chart . | `{}`
|
||||
`customAnnotations` | Additional annotations which will be added to all resources created by Capsule helm chart . | `{}`
|
||||
`certManager.generateCertificates` | Specifies whether capsule webhooks certificates should be generated using cert-manager. | `false`
|
||||
## Created resources
|
||||
|
||||
This Helm Chart creates the following Kubernetes resources in the release namespace:
|
||||
@@ -110,6 +108,7 @@ This Helm Chart creates the following Kubernetes resources in the release namesp
|
||||
* CA Secret
|
||||
* Certificate Secret
|
||||
* Tenant Custom Resource Definition
|
||||
* CapsuleConfiguration Custom Resource Definition
|
||||
* MutatingWebHookConfiguration
|
||||
* ValidatingWebHookConfiguration
|
||||
* RBAC Cluster Roles
|
||||
@@ -129,4 +128,4 @@ Capsule, as many other add-ons, defines its own set of Custom Resource Definitio
|
||||
|
||||
## More
|
||||
|
||||
See Capsule [use cases](https://github.com/clastix/capsule/blob/master/use_cases.md) for more information about how to use Capsule.
|
||||
See Capsule [tutorial](https://github.com/clastix/capsule/blob/master/docs/content/general/tutorial.md) for more information about how to use Capsule.
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
properties:
|
||||
forceTenantPrefix:
|
||||
default: false
|
||||
|
||||
@@ -7,7 +7,17 @@ metadata:
|
||||
name: tenants.capsule.clastix.io
|
||||
spec:
|
||||
conversion:
|
||||
strategy: None
|
||||
strategy: Webhook
|
||||
webhook:
|
||||
clientConfig:
|
||||
service:
|
||||
name: capsule-webhook-service
|
||||
namespace: capsule-system
|
||||
path: /convert
|
||||
port: 443
|
||||
conversionReviewVersions:
|
||||
- v1alpha1
|
||||
- v1beta1
|
||||
group: capsule.clastix.io
|
||||
names:
|
||||
kind: Tenant
|
||||
@@ -46,7 +56,7 @@ spec:
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -57,7 +67,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
items:
|
||||
@@ -475,7 +485,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
owner:
|
||||
description: OwnerSpec defines tenant owner name and kind
|
||||
description: OwnerSpec defines tenant owner name and kind.
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -558,7 +568,7 @@ spec:
|
||||
- owner
|
||||
type: object
|
||||
status:
|
||||
description: TenantStatus defines the observed state of Tenant
|
||||
description: TenantStatus defines the observed state of Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
items:
|
||||
@@ -598,7 +608,7 @@ spec:
|
||||
name: v1beta1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -609,7 +619,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
description: Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
@@ -697,7 +707,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
limitRanges:
|
||||
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
description: Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -1055,7 +1065,7 @@ spec:
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
type: object
|
||||
owners:
|
||||
description: Specifies the owners of the Tenant. Mandatory.
|
||||
@@ -1224,7 +1234,7 @@ spec:
|
||||
- owners
|
||||
type: object
|
||||
status:
|
||||
description: Returns the observed state of the Tenant
|
||||
description: Returns the observed state of the Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
description: List of namespaces assigned to the Tenant.
|
||||
|
||||
@@ -66,6 +66,19 @@ ServiceAccount annotations
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Webhook annotations
|
||||
*/}}
|
||||
{{- define "capsule.webhookAnnotations" -}}
|
||||
{{- if .Values.certManager.generateCertificates -}}
|
||||
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
|
||||
{{- end }}
|
||||
{{- if .Values.customAnnotations }}
|
||||
{{ toYaml .Values.customAnnotations }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
@@ -91,11 +104,26 @@ Create the proxy fully-qualified Docker image to use
|
||||
{{- printf "%s:%s" .Values.proxy.image.repository .Values.proxy.image.tag -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Determine the Kubernetes version to use for jobsFullyQualifiedDockerImage tag
|
||||
*/}}
|
||||
{{- define "capsule.jobsTagKubeVersion" -}}
|
||||
{{- if contains "-eks-" .Capabilities.KubeVersion.GitVersion }}
|
||||
{{- print "v" .Capabilities.KubeVersion.Major "." (.Capabilities.KubeVersion.Minor | replace "+" "") -}}
|
||||
{{- else }}
|
||||
{{- print "v" .Capabilities.KubeVersion.Major "." .Capabilities.KubeVersion.Minor -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the jobs fully-qualified Docker image to use
|
||||
*/}}
|
||||
{{- define "capsule.jobsFullyQualifiedDockerImage" -}}
|
||||
{{- if .Values.jobs.image.tag }}
|
||||
{{- printf "%s:%s" .Values.jobs.image.repository .Values.jobs.image.tag -}}
|
||||
{{- else }}
|
||||
{{- printf "%s:%s" .Values.jobs.image.repository (include "capsule.jobsTagKubeVersion" .) -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
@@ -109,8 +137,12 @@ Create the Capsule Deployment name to use
|
||||
Create the Capsule CA Secret name to use
|
||||
*/}}
|
||||
{{- define "capsule.secretCaName" -}}
|
||||
{{- if .Values.certManager.generateCertificates }}
|
||||
{{- printf "%s-tls" (include "capsule.fullname" .) -}}
|
||||
{{- else }}
|
||||
{{- printf "%s-ca" (include "capsule.fullname" .) -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the Capsule TLS Secret name to use
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -8,4 +9,4 @@ metadata:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
name: {{ include "capsule.secretCaName" . }}
|
||||
data:
|
||||
{{- end }}
|
||||
|
||||
33
charts/capsule/templates/certificate.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
{{- if .Values.certManager.generateCertificates }}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-selfsigned
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selfSigned: {}
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-cert
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
dnsNames:
|
||||
- {{ include "capsule.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc
|
||||
- {{ include "capsule.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: {{ include "capsule.fullname" . }}-webhook-selfsigned
|
||||
secretName: {{ include "capsule.fullname" . }}-tls
|
||||
{{- end }}
|
||||
@@ -8,4 +8,3 @@ metadata:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
name: {{ include "capsule.secretTlsName" . }}
|
||||
data:
|
||||
|
||||
@@ -4,8 +4,12 @@ metadata:
|
||||
name: default
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
annotations:
|
||||
capsule.clastix.io/ca-secret-name: {{ include "capsule.secretCaName" . }}
|
||||
capsule.clastix.io/mutating-webhook-configuration-name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
|
||||
capsule.clastix.io/tls-secret-name: {{ include "capsule.secretTlsName" . }}
|
||||
capsule.clastix.io/validating-webhook-configuration-name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
|
||||
{{- with .Values.customAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
|
||||
@@ -56,6 +56,7 @@ spec:
|
||||
- --enable-leader-election
|
||||
- --zap-log-level={{ default 4 .Values.manager.options.logLevel }}
|
||||
- --configuration-name=default
|
||||
- --enable-secret-controller={{ .Values.manager.options.enableSecretController }}
|
||||
image: {{ include "capsule.managerFullyQualifiedDockerImage" . }}
|
||||
imagePullPolicy: {{ .Values.manager.image.pullPolicy }}
|
||||
env:
|
||||
|
||||
@@ -4,16 +4,18 @@ metadata:
|
||||
name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
{{- if or (.Values.certManager.generateCertificates) (.Values.customAnnotations) }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- include "capsule.webhookAnnotations" . | nindent 4 }}
|
||||
{{- end }}
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- $cmd := "while [ -z $$(kubectl -n $NAMESPACE get secret capsule-tls -o jsonpath='{.data.tls\\\\.crt}') ];" -}}
|
||||
{{- $cmd := printf "while [ -z $$(kubectl -n $NAMESPACE get secret %s -o jsonpath='{.data.tls\\\\.crt}') ];" (include "capsule.secretCaName" .) -}}
|
||||
{{- $cmd = printf "%s do echo 'waiting Capsule to be up and running...' && sleep 5;" $cmd -}}
|
||||
{{- $cmd = printf "%s done" $cmd -}}
|
||||
apiVersion: batch/v1
|
||||
@@ -25,6 +25,14 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name | quote }}
|
||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: post-install-job
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{{- $cmd := printf "kubectl scale deployment -n $NAMESPACE %s --replicas 0 &&" (include "capsule.deploymentName" .) -}}
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
{{- $cmd = printf "%s kubectl delete secret -n $NAMESPACE %s %s --ignore-not-found &&" $cmd (include "capsule.secretTlsName" .) (include "capsule.secretCaName" .) -}}
|
||||
{{- end }}
|
||||
{{- $cmd = printf "%s kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found &&" $cmd -}}
|
||||
{{- $cmd = printf "%s kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found" $cmd -}}
|
||||
apiVersion: batch/v1
|
||||
@@ -26,6 +28,14 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name | quote }}
|
||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: pre-delete-job
|
||||
|
||||
@@ -4,16 +4,18 @@ metadata:
|
||||
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
{{- if or (.Values.certManager.generateCertificates) (.Values.customAnnotations) }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- include "capsule.webhookAnnotations" . | nindent 4 }}
|
||||
{{- end }}
|
||||
webhooks:
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -23,7 +25,7 @@ webhooks:
|
||||
matchPolicy: Equivalent
|
||||
name: cordoning.tenant.capsule.clastix.io
|
||||
namespaceSelector:
|
||||
{{- toYaml .Values.webhooks.cordoning.namespaceSelector | nindent 4}}
|
||||
{{- toYaml .Values.webhooks.cordoning.namespaceSelector | nindent 4}}
|
||||
objectSelector: {}
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -43,7 +45,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -53,10 +57,7 @@ webhooks:
|
||||
matchPolicy: Equivalent
|
||||
name: ingress.capsule.clastix.io
|
||||
namespaceSelector:
|
||||
{{- toYaml .Values.webhooks.ingresses.namespaceSelector | nindent 4}}
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
{{- toYaml .Values.webhooks.ingresses.namespaceSelector | nindent 4}}
|
||||
objectSelector: {}
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -77,7 +78,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -106,7 +109,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -135,7 +140,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -163,10 +170,12 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: capsule-system
|
||||
namespace: {{ .Release.Namespace }}
|
||||
path: /persistentvolumeclaims
|
||||
failurePolicy: {{ .Values.webhooks.persistentvolumeclaims.failurePolicy }}
|
||||
name: pvc.capsule.clastix.io
|
||||
@@ -189,7 +198,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -218,7 +229,9 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -243,3 +256,31 @@ webhooks:
|
||||
scope: '*'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- if not .Values.certManager.generateCertificates }}
|
||||
caBundle: Cg==
|
||||
{{- end }}
|
||||
service:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ .Release.Namespace }}
|
||||
path: /nodes
|
||||
port: 443
|
||||
failurePolicy: {{ .Values.webhooks.nodes.failurePolicy }}
|
||||
name: nodes.capsule.clastix.io
|
||||
matchPolicy: Exact
|
||||
namespaceSelector: {}
|
||||
objectSelector: {}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- UPDATE
|
||||
resources:
|
||||
- nodes
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ .Values.validatingWebhooksTimeoutSeconds }}
|
||||
|
||||
@@ -21,6 +21,7 @@ manager:
|
||||
forceTenantPrefix: false
|
||||
capsuleUserGroups: ["capsule.clastix.io"]
|
||||
protectedNamespaceRegex: ""
|
||||
enableSecretController: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
@@ -41,7 +42,7 @@ jobs:
|
||||
image:
|
||||
repository: quay.io/clastix/kubectl
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "v1.20.7"
|
||||
tag: ""
|
||||
imagePullSecrets: []
|
||||
serviceAccount:
|
||||
create: true
|
||||
@@ -61,6 +62,9 @@ affinity: {}
|
||||
podSecurityPolicy:
|
||||
enabled: false
|
||||
|
||||
certManager:
|
||||
generateCertificates: false
|
||||
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
# Install the ServiceMonitor into a different Namespace, as the monitoring stack one (default: the release one)
|
||||
@@ -123,5 +127,7 @@ webhooks:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
nodes:
|
||||
failurePolicy: Fail
|
||||
mutatingWebhooksTimeoutSeconds: 30
|
||||
validatingWebhooksTimeoutSeconds: 30
|
||||
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -30,7 +30,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
properties:
|
||||
forceTenantPrefix:
|
||||
default: false
|
||||
|
||||
@@ -46,7 +46,7 @@ spec:
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -57,7 +57,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
items:
|
||||
@@ -475,7 +475,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
owner:
|
||||
description: OwnerSpec defines tenant owner name and kind
|
||||
description: OwnerSpec defines tenant owner name and kind.
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -558,7 +558,7 @@ spec:
|
||||
- owner
|
||||
type: object
|
||||
status:
|
||||
description: TenantStatus defines the observed state of Tenant
|
||||
description: TenantStatus defines the observed state of Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
items:
|
||||
@@ -598,7 +598,7 @@ spec:
|
||||
name: v1beta1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -609,7 +609,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
description: Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
@@ -697,7 +697,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
limitRanges:
|
||||
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
description: Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -1055,7 +1055,7 @@ spec:
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
type: object
|
||||
owners:
|
||||
description: Specifies the owners of the Tenant. Mandatory.
|
||||
@@ -1224,7 +1224,7 @@ spec:
|
||||
- owners
|
||||
type: object
|
||||
status:
|
||||
description: Returns the observed state of the Tenant
|
||||
description: Returns the observed state of the Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
description: List of namespaces assigned to the Tenant.
|
||||
|
||||
@@ -24,7 +24,7 @@ spec:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API
|
||||
description: CapsuleConfiguration is the Schema for the Capsule configuration API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -35,7 +35,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
properties:
|
||||
forceTenantPrefix:
|
||||
default: false
|
||||
@@ -118,7 +118,7 @@ spec:
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -129,7 +129,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
items:
|
||||
@@ -547,7 +547,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
owner:
|
||||
description: OwnerSpec defines tenant owner name and kind
|
||||
description: OwnerSpec defines tenant owner name and kind.
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -630,7 +630,7 @@ spec:
|
||||
- owner
|
||||
type: object
|
||||
status:
|
||||
description: TenantStatus defines the observed state of Tenant
|
||||
description: TenantStatus defines the observed state of Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
items:
|
||||
@@ -670,7 +670,7 @@ spec:
|
||||
name: v1beta1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Tenant is the Schema for the tenants API
|
||||
description: Tenant is the Schema for the tenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
@@ -681,7 +681,7 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TenantSpec defines the desired state of Tenant
|
||||
description: TenantSpec defines the desired state of Tenant.
|
||||
properties:
|
||||
additionalRoleBindings:
|
||||
description: Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
|
||||
@@ -769,7 +769,7 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
limitRanges:
|
||||
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
description: Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -1127,7 +1127,7 @@ spec:
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
type: object
|
||||
owners:
|
||||
description: Specifies the owners of the Tenant. Mandatory.
|
||||
@@ -1296,7 +1296,7 @@ spec:
|
||||
- owners
|
||||
type: object
|
||||
status:
|
||||
description: Returns the observed state of the Tenant
|
||||
description: Returns the observed state of the Tenant.
|
||||
properties:
|
||||
namespaces:
|
||||
description: List of namespaces assigned to the Tenant.
|
||||
@@ -1411,7 +1411,7 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
image: quay.io/clastix/capsule:v0.1.0
|
||||
image: quay.io/clastix/capsule:v0.1.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: manager
|
||||
ports:
|
||||
@@ -1582,6 +1582,29 @@ webhooks:
|
||||
- networkpolicies
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: capsule-webhook-service
|
||||
namespace: capsule-system
|
||||
path: /nodes
|
||||
failurePolicy: Fail
|
||||
name: nodes.capsule.clastix.io
|
||||
namespaceSelector:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- UPDATE
|
||||
resources:
|
||||
- nodes
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
|
||||
@@ -7,4 +7,4 @@ kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: quay.io/clastix/capsule
|
||||
newTag: v0.1.0
|
||||
newTag: v0.1.1
|
||||
|
||||
@@ -118,6 +118,25 @@ webhooks:
|
||||
resources:
|
||||
- networkpolicies
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
path: /nodes
|
||||
failurePolicy: Fail
|
||||
name: nodes.capsule.clastix.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- UPDATE
|
||||
resources:
|
||||
- nodes
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
- op: add
|
||||
path: /webhooks/7/namespaceSelector
|
||||
value:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
- op: add
|
||||
path: /webhooks/0/rules/0/scope
|
||||
value: Namespaced
|
||||
@@ -43,12 +49,12 @@
|
||||
- op: add
|
||||
path: /webhooks/3/rules/0/scope
|
||||
value: Namespaced
|
||||
- op: add
|
||||
path: /webhooks/4/rules/0/scope
|
||||
value: Namespaced
|
||||
- op: add
|
||||
path: /webhooks/5/rules/0/scope
|
||||
value: Namespaced
|
||||
- op: add
|
||||
path: /webhooks/6/rules/0/scope
|
||||
value: Namespaced
|
||||
- op: add
|
||||
path: /webhooks/7/rules/0/scope
|
||||
value: Namespaced
|
||||
|
||||
@@ -9,13 +9,11 @@ import (
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/controllers/utils"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
@@ -24,44 +22,23 @@ type Manager struct {
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
// InjectClient injects the Client interface, required by the Runnable interface
|
||||
// InjectClient injects the Client interface, required by the Runnable interface.
|
||||
func (c *Manager) InjectClient(client client.Client) error {
|
||||
c.Client = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterByName(objName, desired string) bool {
|
||||
return objName == desired
|
||||
}
|
||||
|
||||
func forOptionPerInstanceName(instanceName string) builder.ForOption {
|
||||
return builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) bool {
|
||||
return filterByName(event.Object.GetName(), instanceName)
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return filterByName(deleteEvent.Object.GetName(), instanceName)
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return filterByName(updateEvent.ObjectNew.GetName(), instanceName)
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return filterByName(genericEvent.Object.GetName(), instanceName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
|
||||
For(&capsulev1alpha1.CapsuleConfiguration{}, utils.NamesMatchingPredicate(configurationName)).
|
||||
Complete(c)
|
||||
}
|
||||
|
||||
func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
|
||||
c.Log.Info("CapsuleConfiguration reconciliation started", "request.name", request.Name)
|
||||
|
||||
cfg := configuration.NewCapsuleConfiguration(c.Client, request.Name)
|
||||
cfg := configuration.NewCapsuleConfiguration(ctx, c.Client, request.Name)
|
||||
// Validating the Capsule Configuration options
|
||||
if _, err = cfg.ProtectedNamespaceRegexp(); err != nil {
|
||||
panic(errors.Wrap(err, "Invalid configuration for protected Namespace regex"))
|
||||
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
Kind: "ClusterRole",
|
||||
Name: ProvisionerRoleName,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
APIGroup: rbacv1.GroupName,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,20 +10,19 @@ import (
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
|
||||
"github.com/clastix/capsule/controllers/utils"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
@@ -33,65 +32,40 @@ type Manager struct {
|
||||
Configuration configuration.Configuration
|
||||
}
|
||||
|
||||
// InjectClient injects the Client interface, required by the Runnable interface
|
||||
// InjectClient injects the Client interface, required by the Runnable interface.
|
||||
func (r *Manager) InjectClient(c client.Client) error {
|
||||
r.Client = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Manager) filterByNames(name string) bool {
|
||||
return name == ProvisionerRoleName || name == DeleterRoleName
|
||||
}
|
||||
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, configurationName string) (err error) {
|
||||
namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName)
|
||||
|
||||
//nolint:dupl
|
||||
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) (err error) {
|
||||
crErr := ctrl.NewControllerManagedBy(mgr).
|
||||
For(&rbacv1.ClusterRole{}, builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) bool {
|
||||
return r.filterByNames(event.Object.GetName())
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return r.filterByNames(deleteEvent.Object.GetName())
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return r.filterByNames(updateEvent.ObjectNew.GetName())
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return r.filterByNames(genericEvent.Object.GetName())
|
||||
},
|
||||
})).
|
||||
For(&rbacv1.ClusterRole{}, namesPredicate).
|
||||
Complete(r)
|
||||
if crErr != nil {
|
||||
err = multierror.Append(err, crErr)
|
||||
}
|
||||
|
||||
crbErr := ctrl.NewControllerManagedBy(mgr).
|
||||
For(&rbacv1.ClusterRoleBinding{}, builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) bool {
|
||||
return r.filterByNames(event.Object.GetName())
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return r.filterByNames(deleteEvent.Object.GetName())
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return r.filterByNames(updateEvent.ObjectNew.GetName())
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return r.filterByNames(genericEvent.Object.GetName())
|
||||
},
|
||||
})).
|
||||
For(&rbacv1.ClusterRoleBinding{}, namesPredicate).
|
||||
Watches(source.NewKindWithCache(&capsulev1alpha1.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent, limitingInterface workqueue.RateLimitingInterface) {
|
||||
if updateEvent.ObjectNew.GetName() == configurationName {
|
||||
if crbErr := r.EnsureClusterRoleBindings(); crbErr != nil {
|
||||
if crbErr := r.EnsureClusterRoleBindings(ctx); crbErr != nil {
|
||||
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
|
||||
}
|
||||
}
|
||||
},
|
||||
}).
|
||||
Complete(r)
|
||||
|
||||
if crbErr != nil {
|
||||
err = multierror.Append(err, crbErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,18 +74,19 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) (
|
||||
func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
|
||||
switch request.Name {
|
||||
case ProvisionerRoleName:
|
||||
if err = r.EnsureClusterRole(ProvisionerRoleName); err != nil {
|
||||
if err = r.EnsureClusterRole(ctx, ProvisionerRoleName); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ProvisionerRoleName)
|
||||
|
||||
break
|
||||
}
|
||||
if err = r.EnsureClusterRoleBindings(); err != nil {
|
||||
|
||||
if err = r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed")
|
||||
|
||||
break
|
||||
}
|
||||
case DeleterRoleName:
|
||||
if err = r.EnsureClusterRole(DeleterRoleName); err != nil {
|
||||
if err = r.EnsureClusterRole(ctx, DeleterRoleName); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", DeleterRoleName)
|
||||
}
|
||||
}
|
||||
@@ -119,14 +94,14 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRoleBindings() (err error) {
|
||||
func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) {
|
||||
crb := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ProvisionerRoleName,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, crb, func() (err error) {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() (err error) {
|
||||
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
|
||||
|
||||
crb.Subjects = []rbacv1.Subject{}
|
||||
@@ -144,7 +119,7 @@ func (r *Manager) EnsureClusterRoleBindings() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRole(roleName string) (err error) {
|
||||
func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) {
|
||||
role, ok := clusterRoles[roleName]
|
||||
if !ok {
|
||||
return fmt.Errorf("clusterRole %s is not mapped", roleName)
|
||||
@@ -156,8 +131,9 @@ func (r *Manager) EnsureClusterRole(roleName string) (err error) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, clusterRole, func() error {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, clusterRole, func() error {
|
||||
clusterRole.Rules = role.Rules
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -170,8 +146,9 @@ func (r *Manager) EnsureClusterRole(roleName string) (err error) {
|
||||
func (r *Manager) Start(ctx context.Context) error {
|
||||
for roleName := range clusterRoles {
|
||||
r.Log.Info("setting up ClusterRoles", "ClusterRole", roleName)
|
||||
if err := r.EnsureClusterRole(roleName); err != nil {
|
||||
if errors.IsAlreadyExists(err) {
|
||||
|
||||
if err := r.EnsureClusterRole(ctx, roleName); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -180,8 +157,9 @@ func (r *Manager) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
r.Log.Info("setting up ClusterRoleBindings")
|
||||
if err := r.EnsureClusterRoleBindings(); err != nil {
|
||||
if errors.IsAlreadyExists(err) {
|
||||
|
||||
if err := r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,38 +19,62 @@ import (
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
type CAReconciler struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Namespace string
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Namespace string
|
||||
Configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
enqueueFn := handler.EnqueueRequestsFromMapFunc(func(client.Object) []reconcile.Request {
|
||||
return []reconcile.Request{
|
||||
{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: r.Namespace,
|
||||
Name: r.Configuration.CASecretName(),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.Secret{}, forOptionPerInstanceName(caSecretName)).
|
||||
For(&corev1.Secret{}).
|
||||
Watches(source.NewKindWithCache(&admissionregistrationv1.ValidatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == r.Configuration.ValidatingWebhookConfigurationName()
|
||||
}))).
|
||||
Watches(source.NewKindWithCache(&admissionregistrationv1.MutatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == r.Configuration.MutatingWebhookConfigurationName()
|
||||
}))).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// By default helm doesn't allow to use templates in CRD (https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you).
|
||||
// In order to overcome this, we are setting conversion strategy in helm chart to None, and then update it with CA and namespace information.
|
||||
func (r *CAReconciler) UpdateCustomResourceDefinition(caBundle []byte) error {
|
||||
func (r *CAReconciler) UpdateCustomResourceDefinition(ctx context.Context, caBundle []byte) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
crd := &apiextensionsv1.CustomResourceDefinition{}
|
||||
err = r.Get(context.TODO(), types.NamespacedName{Name: "tenants.capsule.clastix.io"}, crd)
|
||||
err = r.Get(ctx, types.NamespacedName{Name: "tenants.capsule.clastix.io"}, crd)
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot retrieve CustomResourceDefinition")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, crd, func() error {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, crd, func() error {
|
||||
crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
|
||||
Strategy: "Webhook",
|
||||
Webhook: &apiextensionsv1.WebhookConversion{
|
||||
@@ -75,12 +99,13 @@ func (r *CAReconciler) UpdateCustomResourceDefinition(caBundle []byte) error {
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) error {
|
||||
func (r CAReconciler) UpdateValidatingWebhookConfiguration(ctx context.Context, caBundle []byte) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
vw := &admissionregistrationv1.ValidatingWebhookConfiguration{}
|
||||
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-validating-webhook-configuration"}, vw)
|
||||
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.ValidatingWebhookConfigurationName()}, vw)
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
|
||||
|
||||
return err
|
||||
}
|
||||
for i, w := range vw.Webhooks {
|
||||
@@ -89,17 +114,19 @@ func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) erro
|
||||
vw.Webhooks[i].ClientConfig.CABundle = caBundle
|
||||
}
|
||||
}
|
||||
return r.Update(context.TODO(), vw, &client.UpdateOptions{})
|
||||
|
||||
return r.Update(ctx, vw, &client.UpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (r CAReconciler) UpdateMutatingWebhookConfiguration(caBundle []byte) error {
|
||||
func (r CAReconciler) UpdateMutatingWebhookConfiguration(ctx context.Context, caBundle []byte) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
mw := &admissionregistrationv1.MutatingWebhookConfiguration{}
|
||||
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-mutating-webhook-configuration"}, mw)
|
||||
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.MutatingWebhookConfigurationName()}, mw)
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
|
||||
|
||||
return err
|
||||
}
|
||||
for i, w := range mw.Webhooks {
|
||||
@@ -108,27 +135,34 @@ func (r CAReconciler) UpdateMutatingWebhookConfiguration(caBundle []byte) error
|
||||
mw.Webhooks[i].ClientConfig.CABundle = caBundle
|
||||
}
|
||||
}
|
||||
return r.Update(context.TODO(), mw, &client.UpdateOptions{})
|
||||
|
||||
return r.Update(ctx, mw, &client.UpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
|
||||
var err error
|
||||
|
||||
if request.Name != r.Configuration.CASecretName() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
r.Log.Info("Reconciling CA Secret")
|
||||
|
||||
// Fetch the CA instance
|
||||
instance := &corev1.Secret{}
|
||||
err = r.Client.Get(context.TODO(), request.NamespacedName, instance)
|
||||
if err != nil {
|
||||
|
||||
if err = r.Client.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var ca cert.CA
|
||||
|
||||
var rq time.Duration
|
||||
ca, err = getCertificateAuthority(r.Client, r.Namespace)
|
||||
|
||||
ca, err = getCertificateAuthority(ctx, r.Client, r.Namespace, r.Configuration.CASecretName())
|
||||
if err != nil && errors.Is(err, MissingCaError{}) {
|
||||
ca, err = cert.GenerateCertificateAuthority()
|
||||
if err != nil {
|
||||
@@ -143,6 +177,7 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
|
||||
rq, err = ca.ExpiresIn(time.Now())
|
||||
if err != nil {
|
||||
r.Log.Info("CA is expired, cleaning to obtain a new one")
|
||||
|
||||
instance.Data = map[string][]byte{}
|
||||
} else {
|
||||
r.Log.Info("Updating CA secret with new PEM and RSA")
|
||||
@@ -153,19 +188,19 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
|
||||
key, _ = ca.CAPrivateKeyPem()
|
||||
|
||||
instance.Data = map[string][]byte{
|
||||
certSecretKey: crt.Bytes(),
|
||||
privateKeySecretKey: key.Bytes(),
|
||||
corev1.TLSCertKey: crt.Bytes(),
|
||||
corev1.TLSPrivateKeyKey: key.Bytes(),
|
||||
}
|
||||
|
||||
group := new(errgroup.Group)
|
||||
group.Go(func() error {
|
||||
return r.UpdateMutatingWebhookConfiguration(crt.Bytes())
|
||||
return r.UpdateMutatingWebhookConfiguration(ctx, crt.Bytes())
|
||||
})
|
||||
group.Go(func() error {
|
||||
return r.UpdateValidatingWebhookConfiguration(crt.Bytes())
|
||||
return r.UpdateValidatingWebhookConfiguration(ctx, crt.Bytes())
|
||||
})
|
||||
group.Go(func() error {
|
||||
return r.UpdateCustomResourceDefinition(crt.Bytes())
|
||||
return r.UpdateCustomResourceDefinition(ctx, crt.Bytes())
|
||||
})
|
||||
|
||||
if err = group.Wait(); err != nil {
|
||||
@@ -174,39 +209,50 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
|
||||
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() error {
|
||||
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, t, func() error {
|
||||
t.Data = instance.Data
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot update Capsule TLS")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if res == controllerutil.OperationResultUpdated {
|
||||
r.Log.Info("Capsule CA has been updated, we need to trigger TLS update too")
|
||||
|
||||
tls := &corev1.Secret{}
|
||||
err = r.Get(ctx, types.NamespacedName{
|
||||
Namespace: r.Namespace,
|
||||
Name: tlsSecretName,
|
||||
Name: r.Configuration.TLSSecretName(),
|
||||
}, tls)
|
||||
|
||||
if err != nil {
|
||||
r.Log.Error(err, "Capsule TLS Secret missing")
|
||||
}
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tls, func() error {
|
||||
tls.Data = map[string][]byte{}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
r.Log.Error(err, "Cannot clean Capsule TLS Secret due to CA update")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020-2021 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package secret
|
||||
|
||||
const (
|
||||
certSecretKey = "tls.crt"
|
||||
privateKeySecretKey = "tls.key"
|
||||
|
||||
caSecretName = "capsule-ca"
|
||||
tlsSecretName = "capsule-tls"
|
||||
)
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package secret
|
||||
|
||||
type MissingCaError struct {
|
||||
}
|
||||
type MissingCaError struct{}
|
||||
|
||||
func (MissingCaError) Error() string {
|
||||
return "CA has not been created yet, please generate a new"
|
||||
|
||||
@@ -9,54 +9,26 @@ import (
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
)
|
||||
|
||||
func getCertificateAuthority(client client.Client, namespace string) (ca cert.CA, err error) {
|
||||
func getCertificateAuthority(ctx context.Context, client client.Client, namespace, name string) (ca cert.CA, err error) {
|
||||
instance := &corev1.Secret{}
|
||||
|
||||
err = client.Get(context.TODO(), types.NamespacedName{
|
||||
Namespace: namespace,
|
||||
Name: caSecretName,
|
||||
}, instance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing secret %s, cannot reconcile", caSecretName)
|
||||
if err = client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, instance); err != nil {
|
||||
return nil, fmt.Errorf("missing secret %s, cannot reconcile", name)
|
||||
}
|
||||
|
||||
if instance.Data == nil {
|
||||
return nil, MissingCaError{}
|
||||
}
|
||||
|
||||
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[certSecretKey], instance.Data[privateKeySecretKey])
|
||||
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[corev1.TLSCertKey], instance.Data[corev1.TLSPrivateKeyKey])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func forOptionPerInstanceName(instanceName string) builder.ForOption {
|
||||
return builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) bool {
|
||||
return filterByName(event.Object.GetName(), instanceName)
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return filterByName(deleteEvent.Object.GetName(), instanceName)
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return filterByName(updateEvent.ObjectNew.GetName(), instanceName)
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return filterByName(genericEvent.Object.GetName(), instanceName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func filterByName(objName, desired string) bool {
|
||||
return objName == desired
|
||||
}
|
||||
|
||||
@@ -22,73 +22,86 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
"github.com/clastix/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
type TLSReconciler struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Namespace string
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Namespace string
|
||||
Configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func (r *TLSReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.Secret{}, forOptionPerInstanceName(tlsSecretName)).
|
||||
For(&corev1.Secret{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
|
||||
var err error
|
||||
|
||||
if request.Name != r.Configuration.TLSSecretName() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
r.Log.Info("Reconciling TLS Secret")
|
||||
|
||||
// Fetch the Secret instance
|
||||
instance := &corev1.Secret{}
|
||||
err = r.Get(ctx, request.NamespacedName, instance)
|
||||
if err != nil {
|
||||
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var ca cert.CA
|
||||
|
||||
var rq time.Duration
|
||||
|
||||
ca, err = getCertificateAuthority(r.Client, r.Namespace)
|
||||
ca, err = getCertificateAuthority(ctx, r.Client, r.Namespace, r.Configuration.CASecretName())
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var shouldCreate bool
|
||||
for _, key := range []string{certSecretKey, privateKeySecretKey} {
|
||||
|
||||
for _, key := range []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey} {
|
||||
if _, ok := instance.Data[key]; !ok {
|
||||
shouldCreate = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
r.Log.Info("Missing Capsule TLS certificate")
|
||||
|
||||
rq = 6 * 30 * 24 * time.Hour
|
||||
|
||||
opts := cert.NewCertOpts(time.Now().Add(rq), fmt.Sprintf("capsule-webhook-service.%s.svc", r.Namespace))
|
||||
|
||||
var crt, key *bytes.Buffer
|
||||
crt, key, err = ca.GenerateCertificate(opts)
|
||||
if err != nil {
|
||||
|
||||
if crt, key, err = ca.GenerateCertificate(opts); err != nil {
|
||||
r.Log.Error(err, "Cannot generate new TLS certificate")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
instance.Data = map[string][]byte{
|
||||
certSecretKey: crt.Bytes(),
|
||||
privateKeySecretKey: key.Bytes(),
|
||||
corev1.TLSCertKey: crt.Bytes(),
|
||||
corev1.TLSPrivateKeyKey: key.Bytes(),
|
||||
}
|
||||
} else {
|
||||
var c *x509.Certificate
|
||||
var b *pem.Block
|
||||
b, _ = pem.Decode(instance.Data[certSecretKey])
|
||||
b, _ = pem.Decode(instance.Data[corev1.TLSCertKey])
|
||||
c, err = x509.ParseCertificate(b.Bytes)
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot parse Capsule TLS")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
@@ -102,21 +115,27 @@ func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctr
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
|
||||
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
|
||||
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, t, func() error {
|
||||
t.Data = instance.Data
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot update Capsule TLS")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if instance.Name == tlsSecretName && res == controllerutil.OperationResultUpdated {
|
||||
// nolint:nestif
|
||||
if instance.Name == r.Configuration.TLSSecretName() && res == controllerutil.OperationResultUpdated {
|
||||
r.Log.Info("Capsule TLS certificates has been updated, Controller pods must be restarted to load new certificate")
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
leaderPod := &corev1.Pod{}
|
||||
|
||||
if err = r.Client.Get(ctx, types.NamespacedName{Namespace: os.Getenv("NAMESPACE"), Name: hostname}, leaderPod); err != nil {
|
||||
r.Log.Error(err, "cannot retrieve the leader Pod, probably running in out of the cluster mode")
|
||||
|
||||
@@ -148,5 +167,6 @@ func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctr
|
||||
}
|
||||
|
||||
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -16,7 +18,6 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
@@ -32,29 +33,35 @@ type abstractServiceLabelsReconciler struct {
|
||||
|
||||
func (r *abstractServiceLabelsReconciler) InjectClient(c client.Client) error {
|
||||
r.client = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
switch err.(type) {
|
||||
case *NonTenantObject, *NoServicesMetadata:
|
||||
if errors.As(err, &NonTenantObjectError{}) || errors.As(err, &NoServicesMetadataError{}) {
|
||||
return reconcile.Result{}, nil
|
||||
default:
|
||||
r.log.Error(err, fmt.Sprintf("Cannot sync %t labels", r.obj))
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.log.Error(err, fmt.Sprintf("Cannot sync %T %s/%s labels", r.obj, r.obj.GetNamespace(), r.obj.GetName()))
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
err = r.client.Get(ctx, request.NamespacedName, r.obj)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.client, r.obj, func() (err error) {
|
||||
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServiceOptions.AdditionalMetadata.Labels))
|
||||
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServiceOptions.AdditionalMetadata.Annotations))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -97,32 +104,23 @@ func (r *abstractServiceLabelsReconciler) sync(available map[string]string, tena
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
func (r *abstractServiceLabelsReconciler) forOptionPerInstanceName() builder.ForOption {
|
||||
return builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) bool {
|
||||
return r.IsNamespaceInTenant(event.Object.GetNamespace())
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return r.IsNamespaceInTenant(deleteEvent.Object.GetNamespace())
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return r.IsNamespaceInTenant(updateEvent.ObjectNew.GetNamespace())
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return r.IsNamespaceInTenant(genericEvent.Object.GetNamespace())
|
||||
},
|
||||
})
|
||||
func (r *abstractServiceLabelsReconciler) forOptionPerInstanceName(ctx context.Context) builder.ForOption {
|
||||
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return r.IsNamespaceInTenant(ctx, object.GetNamespace())
|
||||
}))
|
||||
}
|
||||
|
||||
func (r *abstractServiceLabelsReconciler) IsNamespaceInTenant(namespace string) bool {
|
||||
func (r *abstractServiceLabelsReconciler) IsNamespaceInTenant(ctx context.Context, namespace string) bool {
|
||||
tl := &capsulev1beta1.TenantList{}
|
||||
if err := r.client.List(context.Background(), tl, client.MatchingFieldsSelector{
|
||||
if err := r.client.List(ctx, tl, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
|
||||
}); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(tl.Items) > 0
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package servicelabels
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -15,14 +17,13 @@ type EndpointsLabelsReconciler struct {
|
||||
Log logr.Logger
|
||||
}
|
||||
|
||||
func (r *EndpointsLabelsReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *EndpointsLabelsReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
|
||||
r.abstractServiceLabelsReconciler = abstractServiceLabelsReconciler{
|
||||
obj: &corev1.Endpoints{},
|
||||
scheme: mgr.GetScheme(),
|
||||
log: r.Log,
|
||||
obj: &corev1.Endpoints{},
|
||||
log: r.Log,
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(r.abstractServiceLabelsReconciler.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName()).
|
||||
For(r.abstractServiceLabelsReconciler.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName(ctx)).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package servicelabels
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
|
||||
@@ -14,20 +16,19 @@ type EndpointSlicesLabelsReconciler struct {
|
||||
abstractServiceLabelsReconciler
|
||||
|
||||
Log logr.Logger
|
||||
VersionMinor int
|
||||
VersionMajor int
|
||||
VersionMinor uint
|
||||
VersionMajor uint
|
||||
}
|
||||
|
||||
func (r *EndpointSlicesLabelsReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
r.scheme = mgr.GetScheme()
|
||||
func (r *EndpointSlicesLabelsReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
|
||||
r.abstractServiceLabelsReconciler = abstractServiceLabelsReconciler{
|
||||
scheme: mgr.GetScheme(),
|
||||
log: r.Log,
|
||||
log: r.Log,
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.VersionMajor == 1 && r.VersionMinor <= 16:
|
||||
r.Log.Info("Skipping controller setup, as EndpointSlices are not supported on current kubernetes version", "VersionMajor", r.VersionMajor, "VersionMinor", r.VersionMinor)
|
||||
|
||||
return nil
|
||||
case r.VersionMajor == 1 && r.VersionMinor >= 21:
|
||||
r.abstractServiceLabelsReconciler.obj = &discoveryv1.EndpointSlice{}
|
||||
@@ -36,6 +37,6 @@ func (r *EndpointSlicesLabelsReconciler) SetupWithManager(mgr ctrl.Manager) erro
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(r.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName()).
|
||||
For(r.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName(ctx)).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -5,26 +5,26 @@ package servicelabels
|
||||
|
||||
import "fmt"
|
||||
|
||||
type NonTenantObject struct {
|
||||
type NonTenantObjectError struct {
|
||||
objectName string
|
||||
}
|
||||
|
||||
func NewNonTenantObject(objectName string) error {
|
||||
return &NonTenantObject{objectName: objectName}
|
||||
return &NonTenantObjectError{objectName: objectName}
|
||||
}
|
||||
|
||||
func (n NonTenantObject) Error() string {
|
||||
func (n NonTenantObjectError) Error() string {
|
||||
return fmt.Sprintf("Skipping labels sync for %s as it doesn't belong to tenant", n.objectName)
|
||||
}
|
||||
|
||||
type NoServicesMetadata struct {
|
||||
type NoServicesMetadataError struct {
|
||||
objectName string
|
||||
}
|
||||
|
||||
func NewNoServicesMetadata(objectName string) error {
|
||||
return &NoServicesMetadata{objectName: objectName}
|
||||
return &NoServicesMetadataError{objectName: objectName}
|
||||
}
|
||||
|
||||
func (n NoServicesMetadata) Error() string {
|
||||
func (n NoServicesMetadataError) Error() string {
|
||||
return fmt.Sprintf("Skipping labels sync for %s because no AdditionalLabels or AdditionalAnnotations presents in Tenant spec", n.objectName)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package servicelabels
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -15,13 +17,13 @@ type ServicesLabelsReconciler struct {
|
||||
Log logr.Logger
|
||||
}
|
||||
|
||||
func (r *ServicesLabelsReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *ServicesLabelsReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
|
||||
r.abstractServiceLabelsReconciler = abstractServiceLabelsReconciler{
|
||||
obj: &corev1.Service{},
|
||||
scheme: mgr.GetScheme(),
|
||||
log: r.Log,
|
||||
obj: &corev1.Service{},
|
||||
log: r.Log,
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(r.abstractServiceLabelsReconciler.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName()).
|
||||
For(r.abstractServiceLabelsReconciler.obj, r.abstractServiceLabelsReconciler.forOptionPerInstanceName(ctx)).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
// nolint:dupl
|
||||
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
|
||||
func (r *Manager) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
|
||||
func (r *Manager) syncLimitRanges(ctx context.Context, tenant *capsulev1beta1.Tenant) error {
|
||||
// getting requested LimitRange keys
|
||||
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items))
|
||||
|
||||
@@ -28,26 +29,27 @@ func (r *Manager) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
|
||||
namespace := ns
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncLimitRange(tenant, namespace, keys)
|
||||
return r.syncLimitRange(ctx, tenant, namespace, keys)
|
||||
})
|
||||
}
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (r *Manager) syncLimitRange(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
func (r *Manager) syncLimitRange(ctx context.Context, tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
// getting LimitRange labels for the mutateFn
|
||||
var tenantLabel, limitRangeLabel string
|
||||
|
||||
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
|
||||
return
|
||||
}
|
||||
if limitRangeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.LimitRange{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.pruningResources(namespace, keys, &corev1.LimitRange{}); err != nil {
|
||||
return
|
||||
if limitRangeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.LimitRange{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.pruningResources(ctx, namespace, keys, &corev1.LimitRange{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, spec := range tenant.Spec.LimitRanges.Items {
|
||||
@@ -59,22 +61,24 @@ func (r *Manager) syncLimitRange(tenant *capsulev1beta1.Tenant, namespace string
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() (err error) {
|
||||
target.ObjectMeta.Labels = map[string]string{
|
||||
tenantLabel: tenant.Name,
|
||||
limitRangeLabel: strconv.Itoa(i),
|
||||
}
|
||||
target.Spec = spec
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
|
||||
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme())
|
||||
})
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", target.GetName()), err)
|
||||
|
||||
r.Log.Info("LimitRange sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
type Manager struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
Log logr.Logger
|
||||
Recorder record.EventRecorder
|
||||
RESTConfig *rest.Config
|
||||
}
|
||||
|
||||
func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
@@ -38,83 +38,96 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
|
||||
r.Log = r.Log.WithValues("Request.Name", request.Name)
|
||||
|
||||
// Fetch the Tenant instance
|
||||
instance := &capsulev1beta1.Tenant{}
|
||||
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if apierrors.IsNotFound(err) {
|
||||
r.Log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
r.Log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring the Tenant Status
|
||||
if err = r.updateTenantStatus(instance); err != nil {
|
||||
if err = r.updateTenantStatus(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot update Tenant status")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring ResourceQuota
|
||||
r.Log.Info("Ensuring limit resources count is updated")
|
||||
|
||||
if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot count limited resources")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring all namespaces are collected
|
||||
r.Log.Info("Ensuring all Namespaces are collected")
|
||||
if err = r.collectNamespaces(instance); err != nil {
|
||||
|
||||
if err = r.collectNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot collect Namespace resources")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Ensuring Namespace metadata
|
||||
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
|
||||
if err = r.syncNamespaces(instance); err != nil {
|
||||
|
||||
if err = r.syncNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace items")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring NetworkPolicy resources
|
||||
r.Log.Info("Starting processing of Network Policies")
|
||||
|
||||
if instance.Spec.NetworkPolicies != nil {
|
||||
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies.Items))
|
||||
if err = r.syncNetworkPolicies(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = r.syncNetworkPolicies(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
|
||||
if instance.Spec.LimitRanges != nil {
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
if err = r.syncLimitRanges(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if instance.Spec.ResourceQuota != nil {
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
if err = r.syncResourceQuotas(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.Log.Info("Ensuring additional RoleBindings for owner")
|
||||
if err = r.syncAdditionalRoleBindings(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync additional RoleBindings items")
|
||||
return
|
||||
}
|
||||
// Ensuring LimitRange resources
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
|
||||
if err = r.syncLimitRanges(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
|
||||
r.Log.Info("Ensuring RoleBinding for owner")
|
||||
if err = r.ownerRoleBinding(instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync owner RoleBinding")
|
||||
return
|
||||
}
|
||||
// Ensuring ResourceQuota resources
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
|
||||
if err = r.syncResourceQuotas(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring RoleBinding resources
|
||||
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")
|
||||
|
||||
if err = r.syncRoleBindings(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync RoleBindings items")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring Namespace count
|
||||
r.Log.Info("Ensuring Namespace count")
|
||||
if err = r.ensureNamespaceCount(instance); err != nil {
|
||||
|
||||
if err = r.ensureNamespaceCount(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace count")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.Log.Info("Tenant reconciling completed")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
func (r *Manager) updateTenantStatus(tnt *capsulev1beta1.Tenant) error {
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta1.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if tnt.IsCordoned() {
|
||||
tnt.Status.State = capsulev1beta1.TenantStateCordoned
|
||||
@@ -122,6 +135,6 @@ func (r *Manager) updateTenantStatus(tnt *capsulev1beta1.Tenant) error {
|
||||
tnt.Status.State = capsulev1beta1.TenantStateActive
|
||||
}
|
||||
|
||||
return r.Client.Status().Update(context.Background(), tnt)
|
||||
return r.Client.Status().Update(ctx, tnt)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,37 +20,39 @@ import (
|
||||
)
|
||||
|
||||
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
||||
func (r *Manager) syncNamespaces(tenant *capsulev1beta1.Tenant) (err error) {
|
||||
func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
|
||||
group := new(errgroup.Group)
|
||||
|
||||
for _, item := range tenant.Status.Namespaces {
|
||||
namespace := item
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncNamespaceMetadata(namespace, tenant)
|
||||
return r.syncNamespaceMetadata(ctx, namespace, tenant)
|
||||
})
|
||||
}
|
||||
|
||||
if err = group.Wait(); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespaces")
|
||||
|
||||
err = fmt.Errorf("cannot sync Namespaces: %s", err.Error())
|
||||
err = fmt.Errorf("cannot sync Namespaces: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Tenant) (err error) {
|
||||
// nolint:gocognit
|
||||
func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, tnt *capsulev1beta1.Tenant) (err error) {
|
||||
var res controllerutil.OperationResult
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
||||
ns := &corev1.Namespace{}
|
||||
if conflictErr = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
if conflictErr = r.Client.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
|
||||
|
||||
res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
|
||||
res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
|
||||
annotations := make(map[string]string)
|
||||
labels := map[string]string{
|
||||
"name": namespace,
|
||||
@@ -144,28 +146,28 @@ func (r *Manager) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Te
|
||||
|
||||
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Manager) ensureNamespaceCount(tenant *capsulev1beta1.Tenant) error {
|
||||
func (r *Manager) ensureNamespaceCount(ctx context.Context, tenant *capsulev1beta1.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
found := &capsulev1beta1.Tenant{}
|
||||
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
|
||||
if err := r.Client.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found.Status.Size = tenant.Status.Size
|
||||
|
||||
return r.Client.Status().Update(context.TODO(), found, &client.UpdateOptions{})
|
||||
return r.Client.Status().Update(ctx, found, &client.UpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(tenant *capsulev1beta1.Tenant) error {
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta1.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
err = r.Client.List(context.TODO(), list, client.MatchingFieldsSelector{
|
||||
err = r.Client.List(ctx, list, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
|
||||
})
|
||||
|
||||
@@ -173,11 +175,12 @@ func (r *Manager) collectNamespaces(tenant *capsulev1beta1.Tenant) error {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, tenant.DeepCopy(), func() error {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tenant.DeepCopy(), func() error {
|
||||
tenant.AssignNamespaces(list.Items)
|
||||
|
||||
return r.Client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
|
||||
return r.Client.Status().Update(ctx, tenant, &client.UpdateOptions{})
|
||||
})
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
// nolint:dupl
|
||||
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
|
||||
func (r *Manager) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
|
||||
func (r *Manager) syncNetworkPolicies(ctx context.Context, tenant *capsulev1beta1.Tenant) error {
|
||||
// getting requested NetworkPolicy keys
|
||||
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items))
|
||||
|
||||
@@ -28,26 +29,26 @@ func (r *Manager) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
|
||||
namespace := ns
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncNetworkPolicy(tenant, namespace, keys)
|
||||
return r.syncNetworkPolicy(ctx, tenant, namespace, keys)
|
||||
})
|
||||
}
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (r *Manager) syncNetworkPolicy(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
if err = r.pruningResources(namespace, keys, &networkingv1.NetworkPolicy{}); err != nil {
|
||||
return
|
||||
func (r *Manager) syncNetworkPolicy(ctx context.Context, tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
if err = r.pruningResources(ctx, namespace, keys, &networkingv1.NetworkPolicy{}); err != nil {
|
||||
return err
|
||||
}
|
||||
// getting NetworkPolicy labels for the mutateFn
|
||||
var tenantLabel, networkPolicyLabel string
|
||||
|
||||
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if networkPolicyLabel, err = capsulev1beta1.GetTypeLabel(&networkingv1.NetworkPolicy{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
for i, spec := range tenant.Spec.NetworkPolicies.Items {
|
||||
@@ -59,14 +60,14 @@ func (r *Manager) syncNetworkPolicy(tenant *capsulev1beta1.Tenant, namespace str
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() (err error) {
|
||||
target.SetLabels(map[string]string{
|
||||
tenantLabel: tenant.Name,
|
||||
networkPolicyLabel: strconv.Itoa(i),
|
||||
})
|
||||
target.Spec = spec
|
||||
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme())
|
||||
})
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", target.GetName()), err)
|
||||
@@ -74,9 +75,9 @@ func (r *Manager) syncNetworkPolicy(tenant *capsulev1beta1.Tenant, namespace str
|
||||
r.Log.Info("Network Policy sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ import (
|
||||
// the mutateFn along with the CreateOrUpdate to don't perform the update since resources are identical.
|
||||
//
|
||||
// In case of Namespace-scoped Resource Budget, we're just replicating the resources across all registered Namespaces.
|
||||
func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error) {
|
||||
// nolint:gocognit
|
||||
func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
|
||||
@@ -42,7 +43,7 @@ func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error)
|
||||
if typeLabel, err = capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint:nestif
|
||||
if tenant.Spec.ResourceQuota.Scope == capsulev1beta1.ResourceQuotaScopeTenant {
|
||||
group := new(errgroup.Group)
|
||||
|
||||
@@ -67,8 +68,9 @@ func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error)
|
||||
// These are required since Capsule is going to sum all the used quota to
|
||||
// sum them and get the Tenant one.
|
||||
list := &corev1.ResourceQuotaList{}
|
||||
if scopeErr = r.List(context.TODO(), list, &client.ListOptions{LabelSelector: labels.NewSelector().Add(*tntRequirement).Add(*indexRequirement)}); scopeErr != nil {
|
||||
if scopeErr = r.List(ctx, list, &client.ListOptions{LabelSelector: labels.NewSelector().Add(*tntRequirement).Add(*indexRequirement)}); scopeErr != nil {
|
||||
r.Log.Error(scopeErr, "Cannot list ResourceQuota", "tenantFilter", tntRequirement.String(), "indexFilter", indexRequirement.String())
|
||||
|
||||
return
|
||||
}
|
||||
// Iterating over all the options declared for the ResourceQuota,
|
||||
@@ -116,11 +118,13 @@ func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error)
|
||||
list.Items[item].Spec.Hard[name] = resourceQuota.Hard[name]
|
||||
}
|
||||
}
|
||||
if scopeErr = r.resourceQuotasUpdate(name, quantity, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
|
||||
if scopeErr = r.resourceQuotasUpdate(ctx, name, quantity, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
|
||||
r.Log.Error(scopeErr, "cannot proceed with outer ResourceQuota")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
@@ -142,14 +146,14 @@ func (r *Manager) syncResourceQuotas(tenant *capsulev1beta1.Tenant) (err error)
|
||||
namespace := ns
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncResourceQuota(tenant, namespace, keys)
|
||||
return r.syncResourceQuota(ctx, tenant, namespace, keys)
|
||||
})
|
||||
}
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta1.Tenant, namespace string, keys []string) (err error) {
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
|
||||
@@ -161,7 +165,7 @@ func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace str
|
||||
return err
|
||||
}
|
||||
// Pruning resource of non-requested resources
|
||||
if err = r.pruningResources(namespace, keys, &corev1.ResourceQuota{}); err != nil {
|
||||
if err = r.pruningResources(ctx, namespace, keys, &corev1.ResourceQuota{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -174,8 +178,9 @@ func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace str
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
res, retryErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
|
||||
res, retryErr = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() (err error) {
|
||||
target.SetLabels(map[string]string{
|
||||
tenantLabel: tenant.Name,
|
||||
typeLabel: strconv.Itoa(index),
|
||||
@@ -187,7 +192,7 @@ func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace str
|
||||
target.Spec.Hard = resQuota.Hard
|
||||
}
|
||||
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme())
|
||||
})
|
||||
|
||||
return retryErr
|
||||
@@ -208,7 +213,7 @@ func (r *Manager) syncResourceQuota(tenant *capsulev1beta1.Tenant, namespace str
|
||||
// Serial ResourceQuota processing is expensive: using Go routines we can speed it up.
|
||||
// In case of multiple errors these are logged properly, returning a generic error since we have to repush back the
|
||||
// reconciliation loop.
|
||||
func (r *Manager) resourceQuotasUpdate(resourceName corev1.ResourceName, actual, limit resource.Quantity, list ...corev1.ResourceQuota) (err error) {
|
||||
func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.ResourceName, actual, limit resource.Quantity, list ...corev1.ResourceQuota) (err error) {
|
||||
group := new(errgroup.Group)
|
||||
|
||||
for _, item := range list {
|
||||
@@ -216,12 +221,12 @@ func (r *Manager) resourceQuotasUpdate(resourceName corev1.ResourceName, actual,
|
||||
|
||||
group.Go(func() (err error) {
|
||||
found := &corev1.ResourceQuota{}
|
||||
if err = r.Get(context.TODO(), types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
|
||||
if err = r.Get(ctx, types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
_, retryErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, found, func() error {
|
||||
_, retryErr = controllerutil.CreateOrUpdate(ctx, r.Client, found, func() error {
|
||||
// Ensuring annotation map is there to avoid uninitialized map error and
|
||||
// assigning the overall usage
|
||||
if found.Annotations == nil {
|
||||
@@ -232,6 +237,7 @@ func (r *Manager) resourceQuotasUpdate(resourceName corev1.ResourceName, actual,
|
||||
found.Annotations[capsulev1beta1.HardQuotaFor(resourceName)] = limit.String()
|
||||
// Updating the Resource according to the actual.Cmp result
|
||||
found.Spec.Hard = rq.Spec.Hard
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -244,7 +250,7 @@ func (r *Manager) resourceQuotasUpdate(resourceName corev1.ResourceName, actual,
|
||||
// We had an error and we mark the whole transaction as failed
|
||||
// to process it another time according to the Tenant controller back-off factor.
|
||||
r.Log.Error(err, "Cannot update outer ResourceQuotas", "resourceName", resourceName.String())
|
||||
err = fmt.Errorf("update of outer ResourceQuota items has failed: %s", err.Error())
|
||||
err = fmt.Errorf("update of outer ResourceQuota items has failed: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
122
controllers/tenant/resourcequotas_quota.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
)
|
||||
|
||||
func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *capsulev1beta1.Tenant) error {
|
||||
type resource struct {
|
||||
kind string
|
||||
group string
|
||||
version string
|
||||
}
|
||||
// nolint:prealloc
|
||||
var resourceList []resource
|
||||
|
||||
for k := range tenant.GetAnnotations() {
|
||||
if !strings.HasPrefix(k, capsulev1beta1.ResourceQuotaAnnotationPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(k, "/")
|
||||
if len(parts) != 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
parts = strings.Split(parts[1], "_")
|
||||
|
||||
if len(parts) != 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation, cannot retrieve version", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
groupKindParts := strings.Split(parts[0], ".")
|
||||
if len(groupKindParts) < 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation, cannot retrieve kind and group", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
resourceList = append(resourceList, resource{
|
||||
kind: groupKindParts[0],
|
||||
group: strings.Join(groupKindParts[1:], "."),
|
||||
version: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
errGroup := new(errgroup.Group)
|
||||
|
||||
usedMap := make(map[string]int)
|
||||
|
||||
defer func() {
|
||||
for gvk, used := range usedMap {
|
||||
err := retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
tnt := &capsulev1beta1.Tenant{}
|
||||
if retryErr = r.Client.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, tnt); retryErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tnt.GetAnnotations() == nil {
|
||||
tnt.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
tnt.Annotations[capsulev1beta1.UsedAnnotationForResource(gvk)] = fmt.Sprintf("%d", used)
|
||||
|
||||
return r.Client.Update(ctx, tnt)
|
||||
})
|
||||
if err != nil {
|
||||
r.Log.Error(err, "cannot update custom Resource Quota", "GVK", gvk)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, item := range resourceList {
|
||||
res := item
|
||||
|
||||
errGroup.Go(func() (scopeErr error) {
|
||||
dynamicClient := dynamic.NewForConfigOrDie(r.RESTConfig)
|
||||
|
||||
for _, ns := range tenant.Status.Namespaces {
|
||||
var list *unstructured.UnstructuredList
|
||||
|
||||
list, scopeErr = dynamicClient.Resource(schema.GroupVersionResource{Group: res.group, Version: res.version, Resource: res.kind}).List(ctx, metav1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.namespace==%s", ns),
|
||||
})
|
||||
if scopeErr != nil {
|
||||
return scopeErr
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s.%s_%s", res.kind, res.group, res.version)
|
||||
|
||||
if _, ok := usedMap[key]; !ok {
|
||||
usedMap[key] = 0
|
||||
}
|
||||
|
||||
usedMap[key] += len(list.Items)
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
if err := errGroup.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,16 +9,43 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
|
||||
"github.com/clastix/capsule/controllers/rbac"
|
||||
)
|
||||
|
||||
// Additional Role Bindings can be used in many ways: applying Pod Security Policies or giving
|
||||
// access to CRDs or specific API groups.
|
||||
func (r *Manager) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err error) {
|
||||
// ownerClusterRoleBindings generates a Capsule AdditionalRoleBinding object for the Owner dynamic clusterrole in order
|
||||
// to take advantage of the additional role binding feature.
|
||||
func (r *Manager) ownerClusterRoleBindings(owner capsulev1beta1.OwnerSpec, clusterRole string) capsulev1beta1.AdditionalRoleBindingsSpec {
|
||||
var subject rbacv1.Subject
|
||||
|
||||
if owner.Kind == "ServiceAccount" {
|
||||
splitName := strings.Split(owner.Name, ":")
|
||||
|
||||
subject = rbacv1.Subject{
|
||||
Kind: owner.Kind.String(),
|
||||
Name: splitName[len(splitName)-1],
|
||||
Namespace: splitName[len(splitName)-2],
|
||||
}
|
||||
} else {
|
||||
subject = rbacv1.Subject{
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: owner.Kind.String(),
|
||||
Name: owner.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return capsulev1beta1.AdditionalRoleBindingsSpec{
|
||||
ClusterRoleName: clusterRole,
|
||||
Subjects: []rbacv1.Subject{
|
||||
subject,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Sync the dynamic Tenant Owner specific cluster-roles and additional Role Bindings, which can be used in many ways:
|
||||
// applying Pod Security Policies or giving access to CRDs or specific API groups.
|
||||
func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
|
||||
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
|
||||
hashFn := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
|
||||
h := fnv.New64a()
|
||||
@@ -32,7 +59,16 @@ func (r *Manager) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err
|
||||
return fmt.Sprintf("%x", h.Sum64())
|
||||
}
|
||||
// getting requested Role Binding keys
|
||||
var keys []string
|
||||
keys := make([]string, 0, len(tenant.Spec.Owners))
|
||||
// Generating for dynamic tenant owners cluster roles
|
||||
for _, owner := range tenant.Spec.Owners {
|
||||
for _, clusterRoleName := range owner.GetRoles(*tenant) {
|
||||
cr := r.ownerClusterRoleBindings(owner, clusterRoleName)
|
||||
|
||||
keys = append(keys, hashFn(cr))
|
||||
}
|
||||
}
|
||||
// Generating hash of additional role bindings
|
||||
for _, i := range tenant.Spec.AdditionalRoleBindings {
|
||||
keys = append(keys, hashFn(i))
|
||||
}
|
||||
@@ -43,14 +79,14 @@ func (r *Manager) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err
|
||||
namespace := ns
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncAdditionalRoleBinding(tenant, namespace, keys, hashFn)
|
||||
return r.syncAdditionalRoleBinding(ctx, tenant, namespace, keys, hashFn)
|
||||
})
|
||||
}
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (r *Manager) syncAdditionalRoleBinding(tenant *capsulev1beta1.Tenant, ns string, keys []string, hashFn func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string) (err error) {
|
||||
func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsulev1beta1.Tenant, ns string, keys []string, hashFn func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string) (err error) {
|
||||
var tenantLabel, roleBindingLabel string
|
||||
|
||||
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
|
||||
@@ -61,11 +97,21 @@ func (r *Manager) syncAdditionalRoleBinding(tenant *capsulev1beta1.Tenant, ns st
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.pruningResources(ns, keys, &rbacv1.RoleBinding{}); err != nil {
|
||||
if err = r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i, roleBinding := range tenant.Spec.AdditionalRoleBindings {
|
||||
var roleBindings []capsulev1beta1.AdditionalRoleBindingsSpec
|
||||
|
||||
for _, owner := range tenant.Spec.Owners {
|
||||
for _, clusterRoleName := range owner.GetRoles(*tenant) {
|
||||
roleBindings = append(roleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName))
|
||||
}
|
||||
}
|
||||
|
||||
roleBindings = append(roleBindings, tenant.Spec.AdditionalRoleBindings...)
|
||||
|
||||
for i, roleBinding := range roleBindings {
|
||||
roleBindingHashLabel := hashFn(roleBinding)
|
||||
|
||||
target := &rbacv1.RoleBinding{
|
||||
@@ -76,27 +122,29 @@ func (r *Manager) syncAdditionalRoleBinding(tenant *capsulev1beta1.Tenant, ns st
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() error {
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error {
|
||||
target.ObjectMeta.Labels = map[string]string{
|
||||
tenantLabel: tenant.Name,
|
||||
roleBindingLabel: roleBindingHashLabel,
|
||||
}
|
||||
target.RoleRef = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: roleBinding.ClusterRoleName,
|
||||
}
|
||||
target.Subjects = roleBinding.Subjects
|
||||
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme())
|
||||
})
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", target.GetName()), err)
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring RoleBinding %s", target.GetName()), err)
|
||||
|
||||
if err != nil {
|
||||
r.Log.Error(err, "Cannot sync Additional RoleBinding")
|
||||
r.Log.Error(err, "Cannot sync RoleBinding")
|
||||
}
|
||||
r.Log.Info(fmt.Sprintf("Additional RoleBindings sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
r.Log.Info(fmt.Sprintf("RoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -104,76 +152,3 @@ func (r *Manager) syncAdditionalRoleBinding(tenant *capsulev1beta1.Tenant, ns st
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Each Tenant owner needs the admin Role attached to each Namespace, otherwise no actions on it can be performed.
|
||||
// Since RBAC is based on deny all first, some specific actions like editing Capsule resources are going to be blocked
|
||||
// via Dynamic Admission Webhooks.
|
||||
// TODO(prometherion): we could create a capsule:admin role rather than hitting webhooks for each action
|
||||
func (r *Manager) ownerRoleBinding(tenant *capsulev1beta1.Tenant) error {
|
||||
// getting RoleBinding label for the mutateFn
|
||||
var subjects []rbacv1.Subject
|
||||
|
||||
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newLabels := map[string]string{tl: tenant.Name}
|
||||
|
||||
for _, owner := range tenant.Spec.Owners {
|
||||
if owner.Kind == "ServiceAccount" {
|
||||
splitName := strings.Split(owner.Name, ":")
|
||||
subjects = append(subjects, rbacv1.Subject{
|
||||
Kind: owner.Kind.String(),
|
||||
Name: splitName[len(splitName)-1],
|
||||
Namespace: splitName[len(splitName)-2],
|
||||
})
|
||||
} else {
|
||||
subjects = append(subjects, rbacv1.Subject{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: owner.Kind.String(),
|
||||
Name: owner.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list := make(map[types.NamespacedName]rbacv1.RoleRef)
|
||||
|
||||
for _, i := range tenant.Status.Namespaces {
|
||||
list[types.NamespacedName{Namespace: i, Name: "namespace:admin"}] = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "admin",
|
||||
}
|
||||
list[types.NamespacedName{Namespace: i, Name: "namespace-deleter"}] = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: rbac.DeleterRoleName,
|
||||
}
|
||||
}
|
||||
|
||||
for namespacedName, roleRef := range list {
|
||||
target := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespacedName.Name,
|
||||
Namespace: namespacedName.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
|
||||
target.ObjectMeta.Labels = newLabels
|
||||
target.Subjects = subjects
|
||||
target.RoleRef = roleRef
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
|
||||
})
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err)
|
||||
|
||||
r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
|
||||
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
|
||||
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
|
||||
func (r *Manager) pruningResources(ns string, keys []string, obj client.Object) (err error) {
|
||||
func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {
|
||||
var capsuleLabel string
|
||||
|
||||
if capsuleLabel, err = capsulev1beta1.GetTypeLabel(obj); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -25,13 +26,16 @@ func (r *Manager) pruningResources(ns string, keys []string, obj client.Object)
|
||||
selector := labels.NewSelector()
|
||||
|
||||
var exists *labels.Requirement
|
||||
|
||||
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
selector = selector.Add(*exists)
|
||||
|
||||
if len(keys) > 0 {
|
||||
var notIn *labels.Requirement
|
||||
|
||||
if notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,7 +46,7 @@ func (r *Manager) pruningResources(ns string, keys []string, obj client.Object)
|
||||
r.Log.Info("Pruning objects with label selector " + selector.String())
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
|
||||
return r.DeleteAllOf(ctx, obj, &client.DeleteAllOfOptions{
|
||||
ListOptions: client.ListOptions{
|
||||
LabelSelector: selector,
|
||||
Namespace: ns,
|
||||
@@ -53,7 +57,8 @@ func (r *Manager) pruningResources(ns string, keys []string, obj client.Object)
|
||||
}
|
||||
|
||||
func (r *Manager) emitEvent(object runtime.Object, namespace string, res controllerutil.OperationResult, msg string, err error) {
|
||||
var eventType = corev1.EventTypeNormal
|
||||
eventType := corev1.EventTypeNormal
|
||||
|
||||
if err != nil {
|
||||
eventType = corev1.EventTypeWarning
|
||||
res = "Error"
|
||||
|
||||
19
controllers/utils/name_matching.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
)
|
||||
|
||||
func NamesMatchingPredicate(names ...string) builder.Predicates {
|
||||
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
for _, name := range names {
|
||||
if object.GetName() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}))
|
||||
}
|
||||
8
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
*.log
|
||||
.cache
|
||||
.DS_Store
|
||||
src/.temp
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.*
|
||||
12
docs/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Capsule Documentation
|
||||
|
||||
1. Ensure to have [`yarn`](https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable) installed in your path.
|
||||
2. `yarn install`
|
||||
|
||||
## Local development
|
||||
|
||||
```shell
|
||||
yarn develop
|
||||
```
|
||||
|
||||
This will create a local webserver listening on `localhost:8080` with hot-reload of your local changes.
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
BIN
docs/content/assets/proxy-kubernetes-dashboard.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
docs/content/assets/proxy-lens.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
docs/content/contributing/assets/dev-env.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
341
docs/content/contributing/development.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# Capsule Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have these tools installed:
|
||||
|
||||
- [Go 1.18+](https://golang.org/dl/)
|
||||
- [Operator SDK 1.7.2+](https://github.com/operator-framework/operator-sdk), or [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
|
||||
- [KinD](https://github.com/kubernetes-sigs/kind) or [k3d](https://k3d.io/), with `kubectl`
|
||||
- [ngrok](https://ngrok.com/) (if you want to run locally with remote Kubernetes)
|
||||
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
- OpenSSL
|
||||
|
||||
## Setup a Kubernetes Cluster
|
||||
|
||||
A lightweight Kubernetes within your laptop can be very handy for Kubernetes-native development like Capsule.
|
||||
|
||||
### By `k3d`
|
||||
|
||||
```shell
|
||||
# Install K3d cli by brew in Mac, or your preferred way
|
||||
$ brew install k3d
|
||||
|
||||
# Export your laptop's IP, e.g. retrieving it by: ifconfig
|
||||
# Do change this IP to yours
|
||||
$ export LAPTOP_HOST_IP=192.168.10.101
|
||||
|
||||
# Spin up a bare minimum cluster
|
||||
# Refer to here for more options: https://k3d.io/v4.4.8/usage/commands/k3d_cluster_create/
|
||||
$ k3d cluster create k3s-capsule --servers 1 --agents 1 --no-lb --k3s-server-arg --tls-san=${LAPTOP_HOST_IP}
|
||||
|
||||
# Get Kubeconfig
|
||||
$ k3d kubeconfig get k3s-capsule > /tmp/k3s-capsule && export KUBECONFIG="/tmp/k3s-capsule"
|
||||
|
||||
# This will create a cluster with 1 server and 1 worker node
|
||||
$ kubectl get nodes
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
k3d-k3s-capsule-server-0 Ready control-plane,master 2m13s v1.21.2+k3s1
|
||||
k3d-k3s-capsule-agent-0 Ready <none> 2m3s v1.21.2+k3s1
|
||||
|
||||
# Or 2 Docker containers if you view it from Docker perspective
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
5c26ad840c62 rancher/k3s:v1.21.2-k3s1 "/bin/k3s agent" 53 seconds ago Up 45 seconds k3d-k3s-capsule-agent-0
|
||||
753998879b28 rancher/k3s:v1.21.2-k3s1 "/bin/k3s server --t…" 53 seconds ago Up 51 seconds 0.0.0.0:49708->6443/tcp k3d-k3s-capsule-server-0
|
||||
```
|
||||
|
||||
### By `kind`
|
||||
|
||||
```shell
|
||||
# # Install kind cli by brew in Mac, or your preferred way
|
||||
$ brew install kind
|
||||
|
||||
# Prepare a kind config file with necessary customization
|
||||
$ cat > kind.yaml <<EOF
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
networking:
|
||||
apiServerAddress: "0.0.0.0"
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: ClusterConfiguration
|
||||
metadata:
|
||||
name: config
|
||||
apiServer:
|
||||
certSANs:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- kubernetes
|
||||
- kubernetes.default.svc
|
||||
- kubernetes.default.svc.cluster.local
|
||||
- kind
|
||||
- 0.0.0.0
|
||||
- ${LAPTOP_HOST_IP}
|
||||
- role: worker
|
||||
EOF
|
||||
|
||||
# Spin up a bare minimum cluster with 1 master 1 worker node
|
||||
$ kind create cluster --name kind-capsule --config kind.yaml
|
||||
|
||||
# This will create a cluster with 1 server and 1 worker node
|
||||
$ kubectl get nodes
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
kind-capsule-control-plane Ready control-plane,master 84s v1.21.1
|
||||
kind-capsule-worker Ready <none> 56s v1.21.1
|
||||
|
||||
# Or 2 Docker containers if you view it from Docker perspective
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
7b329fd3a838 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute 0.0.0.0:54894->6443/tcp kind-capsule-control-plane
|
||||
7d50f1633555 kindest/node:v1.21.1 "/usr/local/bin/entr…" About a minute ago Up About a minute kind-capsule-worker
|
||||
```
|
||||
|
||||
## Fork, build, and deploy Capsule
|
||||
|
||||
The `fork-clone-contribute-pr` flow is common for contributing to OSS projects like Kubernetes and Capsule.
|
||||
|
||||
Let's assume you've forked it into your GitHub namespace, say `myuser`, and then you can clone it with Git protocol.
|
||||
Do remember to change the `myuser` to yours.
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:myuser/capsule.git && cd capsule
|
||||
```
|
||||
|
||||
It's a good practice to add the upstream as the remote too so we can easily fetch and merge the upstream to our fork:
|
||||
|
||||
```shell
|
||||
$ git remote add upstream https://github.com/clastix/capsule.git
|
||||
$ git remote -vv
|
||||
origin git@github.com:myuser/capsule.git (fetch)
|
||||
origin git@github.com:myuser/capsule.git (push)
|
||||
upstream https://github.com/clastix/capsule.git (fetch)
|
||||
upstream https://github.com/clastix/capsule.git (push)
|
||||
```
|
||||
|
||||
Build and deploy:
|
||||
|
||||
```shell
|
||||
# Download the project dependencies
|
||||
$ go mod download
|
||||
|
||||
# Build the Capsule image
|
||||
$ make docker-build
|
||||
|
||||
# Retrieve the built image version
|
||||
$ export CAPSULE_IMAGE_VESION=`docker images --format '{{.Tag}}' quay.io/clastix/capsule`
|
||||
|
||||
# If k3s, load the image into cluster by
|
||||
$ k3d image import --cluster k3s-capsule capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
|
||||
# If Kind, load the image into cluster by
|
||||
$ kind load docker-image --name kind-capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
|
||||
|
||||
# deploy all the required manifests
|
||||
# Note: 1) please retry if you saw errors; 2) if you want to clean it up first, run: make remove
|
||||
$ make deploy
|
||||
|
||||
# Make sure the controller is running
|
||||
$ kubectl get pod -n capsule-system
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
capsule-controller-manager-5c6b8445cf-566dc 1/1 Running 0 23s
|
||||
|
||||
# Check the logs if needed
|
||||
$ kubectl -n capsule-system logs --all-containers -l control-plane=controller-manager
|
||||
|
||||
# You may have a try to deploy a Tenant too to make sure it works end to end
|
||||
$ kubectl apply -f - <<EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: oil
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
- name: system:serviceaccount:capsule-system:default
|
||||
kind: ServiceAccount
|
||||
EOF
|
||||
|
||||
# There shouldn't be any errors and you should see the newly created tenant
|
||||
$ kubectl get tenants
|
||||
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
oil Active 0 14s
|
||||
```
|
||||
|
||||
If you want to test namespace creation or such stuff, make sure to use impersonation:
|
||||
|
||||
```sh
|
||||
$ kubectl ... --as system:serviceaccount:capsule-system:default --as-group capsule.clastix.io
|
||||
```
|
||||
|
||||
As of now, a complete Capsule environment has been set up in `kind`- or `k3d`-powered cluster, and the `capsule-controller-manager` is running as a deployment serving as:
|
||||
|
||||
- The reconcilers for CRDs and;
|
||||
- A series of webhooks
|
||||
|
||||
## Setup the development environment
|
||||
|
||||
During development, we prefer that the code is running within our IDE locally, instead of running as the normal Pod(s) within the Kubernetes cluster.
|
||||
|
||||
Such a setup can be illustrated as below diagram:
|
||||
|
||||

|
||||
|
||||
To achieve that, there are some necessary steps we need to walk through, which have been made as a `make` target within our `Makefile`.
|
||||
|
||||
So the TL;DR answer is:
|
||||
|
||||
```shell
|
||||
# If you haven't installed or run `make deploy` before, do it first
|
||||
# Note: please retry if you saw errors
|
||||
$ make deploy
|
||||
|
||||
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
|
||||
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
||||
$ LAPTOP_HOST_IP="<YOUR_LAPTOP_IP>" make dev-setup
|
||||
```
|
||||
|
||||
This is a very common setup for typical Kubernetes Operator development so we'd better walk them through with more details here.
|
||||
|
||||
1. Scaling down the deployed Pod(s) to 0
|
||||
|
||||
We need to scale the existing replicas of `capsule-controller-manager` to 0 to avoid reconciliation competition between the Pod(s) and the code running outside of the cluster, in our preferred IDE for example.
|
||||
|
||||
```shell
|
||||
$ kubectl -n capsule-system scale deployment capsule-controller-manager --replicas=0
|
||||
deployment.apps/capsule-controller-manager scaled
|
||||
```
|
||||
|
||||
2. Preparing TLS certificate for the webhooks
|
||||
|
||||
Running webhooks requires TLS, we can prepare the TLS key pair in our development env to handle HTTPS requests.
|
||||
|
||||
```shell
|
||||
# Prepare a simple OpenSSL config file
|
||||
# Do remember to export LAPTOP_HOST_IP before running this command
|
||||
$ cat > _tls.cnf <<EOF
|
||||
[ req ]
|
||||
default_bits = 4096
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
[ req_distinguished_name ]
|
||||
countryName = SG
|
||||
stateOrProvinceName = SG
|
||||
localityName = SG
|
||||
organizationName = CAPSULE
|
||||
commonName = CAPSULE
|
||||
[ req_ext ]
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = ${LAPTOP_HOST_IP}
|
||||
EOF
|
||||
|
||||
# Create this dir to mimic the Pod mount point
|
||||
$ mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||
|
||||
# Generate the TLS cert/key under /tmp/k8s-webhook-server/serving-certs
|
||||
$ openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
||||
-subj "/C=SG/ST=SG/L=SG/O=CAPSULE/CN=CAPSULE" \
|
||||
-extensions req_ext \
|
||||
-config _tls.cnf \
|
||||
-keyout /tmp/k8s-webhook-server/serving-certs/tls.key \
|
||||
-out /tmp/k8s-webhook-server/serving-certs/tls.crt
|
||||
|
||||
# Clean it up
|
||||
$ rm -f _tls.cnf
|
||||
```
|
||||
|
||||
3. Patching the Webhooks
|
||||
|
||||
By default, the webhooks will be registered with the services, which will route to the Pods, inside the cluster.
|
||||
|
||||
We need to _delegate_ the controllers' and webbooks' services to the code running in our IDE by patching the `MutatingWebhookConfiguration` and `ValidatingWebhookConfiguration`.
|
||||
|
||||
```shell
|
||||
# Export your laptop's IP with the 9443 port exposed by controllers/webhooks' services
|
||||
$ export WEBHOOK_URL="https://${LAPTOP_HOST_IP}:9443"
|
||||
|
||||
# Export the cert we just generated as the CA bundle for webhook TLS
|
||||
$ export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`
|
||||
|
||||
# Patch the MutatingWebhookConfiguration webhook
|
||||
$ kubectl patch MutatingWebhookConfiguration capsule-mutating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/mutate-v1-namespace-owner-reference\",'caBundle':\"${CA_BUNDLE}\"}}\
|
||||
]"
|
||||
|
||||
# Verify it if you want
|
||||
$ kubectl get MutatingWebhookConfiguration capsule-mutating-webhook-configuration -o yaml
|
||||
|
||||
# Patch the ValidatingWebhookConfiguration webhooks
|
||||
# Note: there is a list of validating webhook endpoints, not just one
|
||||
$ kubectl patch ValidatingWebhookConfiguration capsule-validating-webhook-configuration \
|
||||
--type='json' -p="[\
|
||||
{'op': 'replace', 'path': '/webhooks/0/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/cordoning\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/1/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/ingresses\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/2/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/namespaces\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/3/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/networkpolicies\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/4/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/pods\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/services\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/tenants\",'caBundle':\"${CA_BUNDLE}\"}},\
|
||||
{'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"${WEBHOOK_URL}/nodes\",'caBundle':\"${CA_BUNDLE}\"}}\
|
||||
]"
|
||||
|
||||
# Verify it if you want
|
||||
$ kubectl get ValidatingWebhookConfiguration capsule-validating-webhook-configuration -o yaml
|
||||
```
|
||||
|
||||
## Run Capsule outside the cluster
|
||||
|
||||
Now we can run Capsule controllers with webhooks outside of the Kubernetes cluster:
|
||||
|
||||
```shell
|
||||
$ export NAMESPACE=capsule-system && export TMPDIR=/tmp/
|
||||
$ go run .
|
||||
```
|
||||
|
||||
To verify that, we can open a new console and create a new Tenant:
|
||||
|
||||
```shell
|
||||
$ kubectl apply -f - <<EOF
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: Tenant
|
||||
metadata:
|
||||
name: gas
|
||||
spec:
|
||||
owners:
|
||||
- name: alice
|
||||
kind: User
|
||||
EOF
|
||||
```
|
||||
|
||||
We should see output and logs in the `make run` console.
|
||||
|
||||
Now it's time to work through our familiar inner loop for development in our preferred IDE. For example, if you're using [Visual Studio Code](https://code.visualstudio.com), this `launch.json` file can be a good start.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"args": [
|
||||
"--zap-encoder=console",
|
||||
"--zap-log-level=debug",
|
||||
"--configuration-name=capsule-default"
|
||||
],
|
||||
"env": {
|
||||
"NAMESPACE": "capsule-system",
|
||||
"TMPDIR": "/tmp/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
22
docs/content/contributing/governance.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Project Governance
|
||||
|
||||
This document lays out the guidelines under which the Capsule project will be governed.
|
||||
The goal is to make sure that the roles and responsibilities are well defined and clarify how decisions are made.
|
||||
|
||||
## Roles
|
||||
|
||||
In the context of Capsule project, we consider the following roles:
|
||||
|
||||
* __Users__: everyone using Capsule, typically willing to provide feedback by proposing features and/or filing issues.
|
||||
|
||||
* __Contributors__: everyone contributing code, documentation, examples, tests, and participating in feature proposals as well as design discussions.
|
||||
|
||||
* __Maintainers__: are responsible for engaging with and assisting contributors to iterate on the contributions until it reaches acceptable quality. Maintainers can decide whether the contributions can be accepted into the project or rejected.
|
||||
|
||||
## Release Management
|
||||
|
||||
The release process will be governed by Maintainers.
|
||||
|
||||
## Roadmap Planning
|
||||
|
||||
Maintainers will share roadmap and release versions as milestones in GitHub.
|
||||
111
docs/content/contributing/guidelines.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Thank you for your interest in contributing to Capsule. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community.
|
||||
|
||||
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
|
||||
|
||||
1. You are working against the latest source on the *master* branch.
|
||||
1. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
|
||||
1. You open an issue to discuss any significant work: we would hate for your time to be wasted.
|
||||
|
||||
To send us a pull request, please:
|
||||
|
||||
1. Fork the repository.
|
||||
1. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it
|
||||
will be hard for us to focus on your change.
|
||||
1. Ensure local tests pass.
|
||||
1. Commit to your fork using clear commit messages.
|
||||
1. Send us a pull request, answering any default questions in the pull request interface.
|
||||
1. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
|
||||
|
||||
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
|
||||
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
Make sure to keep Pull Requests small and functional to make them easier to review, understand, and look up in commit history. This repository uses "Squash and Commit" to keep our history clean and make it easier to revert changes based on PR.
|
||||
|
||||
Adding the appropriate documentation, unit tests and e2e tests as part of a feature is the responsibility of the
|
||||
feature owner, whether it is done in the same Pull Request or not.
|
||||
|
||||
All the Pull Requests must refer to an already open issue: this is the first phase to contribute also for informing maintainers about the issue.
|
||||
|
||||
## Commits
|
||||
|
||||
Commit's first line should not exceed 50 columns.
|
||||
|
||||
A commit description is welcomed to explain more the changes: just ensure
|
||||
to put a blank line and an arbitrary number of maximum 72 characters long
|
||||
lines, at most one blank line between them.
|
||||
|
||||
Please, split changes into several and documented small commits: this will help us to perform a better review. Commits must follow the Conventional Commits Specification, a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with Semantic Versioning, by describing the features, fixes, and breaking changes made in commit messages. See [Conventional Commits Specification](https://www.conventionalcommits.org) to learn about Conventional Commits.
|
||||
|
||||
> In case of errors or need of changes to previous commits,
|
||||
> fix them squashing to make changes atomic.
|
||||
|
||||
## Code convention
|
||||
|
||||
Capsule is written in Golang. The changes must follow the Pull Request method where a _GitHub Action_ will
|
||||
check the `golangci-lint`, so ensure your changes respect the coding standard.
|
||||
|
||||
### golint
|
||||
|
||||
You can easily check them issuing the _Make_ recipe `golint`.
|
||||
|
||||
```
|
||||
# make golint
|
||||
golangci-lint run -c .golangci.yml
|
||||
```
|
||||
|
||||
> Enabled linters and related options are defined in the [.golanci.yml file](https://github.com/clastix/capsule/blob/master/.golangci.yml)
|
||||
|
||||
### goimports
|
||||
|
||||
Also, the Go import statements must be sorted following the best practice:
|
||||
|
||||
```
|
||||
<STANDARD LIBRARY>
|
||||
|
||||
<EXTERNAL PACKAGES>
|
||||
|
||||
<LOCAL PACKAGES>
|
||||
```
|
||||
|
||||
To help you out you can use the _Make_ recipe `goimports`
|
||||
|
||||
```
|
||||
# make goimports
|
||||
goimports -w -l -local "github.com/clastix/capsule" .
|
||||
```
|
||||
|
||||
## Finding contributions to work on
|
||||
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the
|
||||
default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted'
|
||||
and 'good first issue' issues are a great place to start.
|
||||
|
||||
## Design Docs
|
||||
|
||||
A contributor proposes a design with a PR on the repository to allow for revisions and discussions.
|
||||
If a design needs to be discussed before formulating a document for it, make use of GitHub Discussions to
|
||||
involve the community on the discussion.
|
||||
|
||||
## GitHub Issues
|
||||
|
||||
GitHub Issues are used to file bugs, work items, and feature requests with actionable items/issues.
|
||||
|
||||
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
|
||||
|
||||
* A reproducible test case or series of steps
|
||||
* The version of the code being used
|
||||
* Any modifications you've made relevant to the bug
|
||||
* Anything unusual about your environment or deployment
|
||||
|
||||
## Miscellanea
|
||||
|
||||
Please, add a new single line at end of any file as the current coding style.
|
||||
|
||||
## Licensing
|
||||
|
||||
See the [LICENSE](https://github.com/clastix/capsule/blob/master/LICENSE) file for our project's licensing. We can ask you to confirm the licensing of your contribution.
|
||||
3
docs/content/contributing/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contributing
|
||||
|
||||
Guidelines for community contribution.
|
||||
@@ -1,7 +1,9 @@
|
||||
# Getting started
|
||||
|
||||
Thanks for giving Capsule a try.
|
||||
|
||||
## Installation
|
||||
|
||||
Make sure you have access to a Kubernetes cluster as administrator.
|
||||
|
||||
There are two ways to install Capsule:
|
||||
@@ -10,6 +12,7 @@ There are two ways to install Capsule:
|
||||
* Use the [Capsule Helm Chart](https://github.com/clastix/capsule/blob/master/charts/capsule/README.md)
|
||||
|
||||
### Install with the single YAML file installer
|
||||
|
||||
Ensure you have `kubectl` installed in your `PATH`. Clone this repository and move to the repo folder:
|
||||
|
||||
```
|
||||
@@ -19,9 +22,11 @@ $ kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/conf
|
||||
It will install the Capsule controller in a dedicated namespace `capsule-system`.
|
||||
|
||||
### Install with Helm Chart
|
||||
|
||||
Please, refer to the instructions reported in the Capsule Helm Chart [README](https://github.com/clastix/capsule/blob/master/charts/capsule/README.md).
|
||||
|
||||
# Create your first Tenant
|
||||
## Create your first Tenant
|
||||
|
||||
In Capsule, a _Tenant_ is an abstraction to group multiple namespaces in a single entity within a set of boundaries defined by the Cluster Administrator. The tenant is then assigned to a user or group of users who is called _Tenant Owner_.
|
||||
|
||||
Capsule defines a Tenant as Custom Resource with cluster scope.
|
||||
@@ -49,7 +54,8 @@ NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
|
||||
oil Active 0 10s
|
||||
```
|
||||
|
||||
## Tenant owners
|
||||
## Login as Tenant Owner
|
||||
|
||||
Each tenant comes with a delegated user or group of users acting as the tenant admin. In the Capsule jargon, this is called the _Tenant Owner_. Other users can operate inside a tenant with different levels of permissions and authorizations assigned directly by the Tenant Owner.
|
||||
|
||||
Capsule does not care about the authentication strategy used in the cluster and all the Kubernetes methods of [authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) are supported. The only requirement to use Capsule is to assign tenant users to the group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
|
||||
@@ -68,7 +74,7 @@ Users authenticated through an _OIDC token_ must have in their token:
|
||||
]
|
||||
```
|
||||
|
||||
The [hack/create-user.sh](../../hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `alice` user acting as owner of a tenant called `oil`
|
||||
The [hack/create-user.sh](https://github.com/clastix/capsule/blob/master/hack/create-user.sh) can help you set up a dummy `kubeconfig` for the `alice` user acting as owner of a tenant called `oil`
|
||||
|
||||
```bash
|
||||
./hack/create-user.sh alice oil
|
||||
@@ -79,32 +85,36 @@ kubeconfig file is: alice-oil.kubeconfig
|
||||
to use it as alice export KUBECONFIG=alice-oil.kubeconfig
|
||||
```
|
||||
|
||||
Log as tenant owner
|
||||
Login as tenant owner
|
||||
|
||||
```
|
||||
$ export KUBECONFIG=alice-oil.kubeconfig
|
||||
```
|
||||
|
||||
and create a couple of new namespaces
|
||||
## Create namespaces
|
||||
|
||||
As tenant owner, you can create namespaces:
|
||||
|
||||
```
|
||||
$ kubectl create namespace oil-production
|
||||
$ kubectl create namespace oil-development
|
||||
```
|
||||
|
||||
As user `alice` you can operate with fully admin permissions:
|
||||
And operate with fully admin permissions:
|
||||
|
||||
```
|
||||
$ kubectl -n oil-development run nginx --image=docker.io/nginx
|
||||
$ kubectl -n oil-development get pods
|
||||
```
|
||||
|
||||
but limited to only your namespaces:
|
||||
## Limiting access
|
||||
|
||||
Tenant Owners have full administrative permissions limited to only the namespaces in the assigned tenant. They can create any namespaced resource in their namespaces but they do not have access to cluster resources or resources belonging to other tenants they do not own:
|
||||
|
||||
```
|
||||
$ kubectl -n kube-system get pods
|
||||
Error from server (Forbidden): pods is forbidden: User "alice" cannot list resource "pods" in API group "" in the namespace "kube-system"
|
||||
Error from server (Forbidden): pods is forbidden:
|
||||
User "alice" cannot list resource "pods" in API group "" in the namespace "kube-system"
|
||||
```
|
||||
|
||||
# What’s next
|
||||
The Tenant Owners have full administrative permissions limited to only the namespaces in the assigned tenant. However, their permissions can be controlled by the Cluster Admin by setting rules and policies on the assigned tenant. See the [use cases](./use-cases/overview.md) page for more getting more cool things you can do with Capsule.
|
||||
See the [tutorial](/docs/general/tutorial) for getting more cool things you can do with Capsule.
|
||||
2
docs/content/general/index.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Documentation
|
||||
General documentation for Capsule Operator
|
||||
2284
docs/content/general/mtb.md
Normal file
@@ -1,8 +1,6 @@
|
||||
# Capsule Proxy
|
||||
|
||||
Capsule Proxy is an add-on for [Capsule](https://github.com/clastix/capsule), the operator providing multi-tenancy in Kubernetes.
|
||||
|
||||
## The problem
|
||||
Capsule Proxy is an add-on for Capsule Operator addressing some RBAC issues when enabling multi-tenacy in Kubernetes since users cannot list the owned cluster-scoped resources.
|
||||
|
||||
Kubernetes RBAC cannot list only the owned cluster-scoped resources since there are no ACL-filtered APIs. For example:
|
||||
|
||||
@@ -27,36 +25,83 @@ With **Capsule**, we took a different approach. As one of the key goals, we want
|
||||
|
||||
## How it works
|
||||
|
||||
This project is an add-on of the main [Capsule](https://github.com/clastix/capsule) operator, so make sure you have a working instance of Caspule before attempting to install it.
|
||||
Use the `capsule-proxy` only if you want Tenant Owners to list their own Cluster-Scope resources.
|
||||
|
||||
The `capsule-proxy` implements a simple reverse proxy that intercepts only specific requests to the APIs server and Capsule does all the magic behind the scenes.
|
||||
|
||||
Current implementation filters the following requests:
|
||||
|
||||
* `api/v1/namespaces`
|
||||
* `api/v1/nodes`
|
||||
* `apis/storage.k8s.io/v1/storageclasses{/name}`
|
||||
* `apis/networking.k8s.io/{v1,v1beta1}/ingressclasses{/name}`
|
||||
* `api/scheduling.k8s.io/{v1}/priorityclasses{/name}`
|
||||
* `/api/scheduling.k8s.io/{v1}/priorityclasses{/name}`
|
||||
* `/api/v1/namespaces`
|
||||
* `/api/v1/nodes{/name}`
|
||||
* `/api/v1/pods?fieldSelector=spec.nodeName%3D{name}`
|
||||
* `/apis/coordination.k8s.io/v1/namespaces/kube-node-lease/leases/{name}`
|
||||
* `/apis/metrics.k8s.io/{v1beta1}/nodes{/name}`
|
||||
* `/apis/networking.k8s.io/{v1,v1beta1}/ingressclasses{/name}`
|
||||
* `/apis/storage.k8s.io/v1/storageclasses{/name}`
|
||||
|
||||
All other requestes are proxied transparently to the APIs server, so no side-effects are expected. We're planning to add new APIs in the future, so PRs are welcome!
|
||||
All other requests are proxied transparently to the APIs server, so no side effects are expected. We're planning to add new APIs in the future, so [PRs are welcome](https://github.com/clastix/capsule-proxy)!
|
||||
|
||||
## Installation
|
||||
|
||||
Capsule Proxy is an optional add-on of the main Capsule Operator, so make sure you have a working instance of Capsule before attempting to install it.
|
||||
Use the `capsule-proxy` only if you want Tenant Owners to list their own Cluster-Scope resources.
|
||||
|
||||
The `capsule-proxy` can be deployed in standalone mode, e.g. running as a pod bridging any Kubernetes client to the APIs server.
|
||||
Optionally, it can be deployed as a sidecar container in the backend of a dashboard.
|
||||
|
||||
Running outside a Kubernetes cluster is also viable, although a valid `KUBECONFIG` file must be provided, using the environment variable `KUBECONFIG` or the default file in `$HOME/.kube/config`.
|
||||
|
||||
An Helm Chart is available [here](./charts/capsule-proxy/README.md).
|
||||
A Helm Chart is available [here](https://github.com/clastix/capsule-proxy/blob/master/charts/capsule-proxy/README.md).
|
||||
|
||||
## Does it work with kubectl?
|
||||
Depending on your environment, you can expose the `capsule-proxy` by:
|
||||
|
||||
Yes, it works by intercepting all the requests from the `kubectl` client directed to the APIs server. It works with both users who use the TLS certificate authentication and those who use OIDC.
|
||||
- Ingress
|
||||
- NodePort Service
|
||||
- LoadBalance Service
|
||||
- HostPort
|
||||
- HostNetwork
|
||||
|
||||
## How RBAC is put in place?
|
||||
Here how it looks like when exposed through an Ingress Controller:
|
||||
|
||||
Each Tenant owner can have their capabilities managed pretty similar to a standard RBAC.
|
||||
```
|
||||
+-----------+ +-----------+ +-----------+
|
||||
kubectl ------>|:443 |--------->|:9001 |-------->|:6443 |
|
||||
+-----------+ +-----------+ +-----------+
|
||||
ingress-controller capsule-proxy kube-apiserver
|
||||
```
|
||||
|
||||
## CLI flags
|
||||
|
||||
- `capsule-configuration-name`: name of the `CapsuleConfiguration` resource which is containing the [Capsule configurations](/docs/general/references/#capsule-configuration) (default: `default`)
|
||||
- `capsule-user-group` (deprecated): old way to specify the user groups which request must be intercepted by the proxy
|
||||
- `ignored-user-group`: names of the groups which requests must be ignored and proxy-passed to the upstream server
|
||||
- `listening-port`: HTTP port the proxy listens to (default: `9001`)
|
||||
- `oidc-username-claim`: the OIDC field name used to identify the user (default: `preferred_username`), the proper value can be extracted from the Kubernetes API Server flags
|
||||
- `enable-ssl`: enable the bind on HTTPS for secure communication, allowing client-based certificate, also knows as mutual TLS (default: `true`)
|
||||
- `ssl-cert-path`: path to the TLS certificate, then TLS mode is enabled (default: `/opt/capsule-proxy/tls.crt`)
|
||||
- `ssl-key-path`: path to the TLS certificate key, when TLS mode is enabled (default: `/opt/capsule-proxy/tls.key`)
|
||||
- `rolebindings-resync-period`: resync period for RoleBinding resources reflector, lower values can help if you're facing [flaky etcd connection](https://github.com/clastix/capsule-proxy/issues/174) (default: `10h`)
|
||||
|
||||
## User Authentication
|
||||
|
||||
The `capsule-proxy` intercepts all the requests from the `kubectl` client directed to the APIs Server. Users using a TLS client based authentication with certificate and key are able to talks with APIs Server since it is able to forward client certificates to the Kubernetes APIs server.
|
||||
|
||||
It is possible to protect the `capsule-proxy` using a certificate provided by Let's Encrypt. Keep in mind that, in this way, the TLS termination will be executed by the Ingress Controller, meaning that the authentication based on client certificate will be withdrawn and not reversed to the upstream.
|
||||
|
||||
If your prerequisite is exposing `capsule-proxy` using an Ingress, you must rely on the token-based authentication, for example OIDC or Bearer tokens. Users providing tokens are always able to reach the APIs Server.
|
||||
|
||||
## Kubernetes dashboards integration
|
||||
|
||||
If you're using a client-only dashboard, for example [Lens](https://k8slens.dev/), the `capsule-proxy` can be used as with `kubectl` since this dashboard usually talks to the APIs server using just a `kubeconfig` file.
|
||||
|
||||

|
||||
|
||||
For a web-based dashboard, like the [Kubernetes Dashboard](https://github.com/kubernetes/dashboard), the `capsule-proxy` can be deployed as a sidecar container in the backend, following the well-known cloud-native _Ambassador Pattern_.
|
||||
|
||||

|
||||
|
||||
## Tenant Owner Authorization
|
||||
|
||||
Each Tenant owner can have their capabilities managed pretty similar to a standard Kubernetes RBAC.
|
||||
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
@@ -89,6 +134,7 @@ Each Resource kind can be granted with several verbs, such as:
|
||||
### Namespaces
|
||||
|
||||
As tenant owner `alice`, you can use `kubectl` to create some namespaces:
|
||||
|
||||
```
|
||||
$ kubectl --context alice-oidc@mycluster create namespace oil-production
|
||||
$ kubectl --context alice-oidc@mycluster create namespace oil-development
|
||||
@@ -107,7 +153,7 @@ oil-production Active 2m
|
||||
|
||||
### Nodes
|
||||
|
||||
The Capsule Proxy gives the owners the ability to access the nodes matching the `.spec.nodeSelector` in the Tenant manifest:
|
||||
The Capsule Proxy gives the owners the ability to access the nodes matching the `.spec.nodeSelector` in the Tenant manifest:
|
||||
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
@@ -134,6 +180,15 @@ capsule-gold-qwerty Ready <none> 43h v1.19.1
|
||||
|
||||
> Warning: when no `nodeSelector` is specified, the tenant owners has access to all the nodes, according to the permissions listed in the `proxySettings` specs.
|
||||
|
||||
### Special routes for kubectl describe
|
||||
|
||||
When issuing a `kubectl describe node`, some other endpoints are put in place:
|
||||
|
||||
* `api/v1/pods?fieldSelector=spec.nodeName%3D{name}`
|
||||
* `/apis/coordination.k8s.io/v1/namespaces/kube-node-lease/leases/{name}`
|
||||
|
||||
These are mandatory in order to retrieve the list of the running Pods on the required node, and providing info about the lease status of it.
|
||||
|
||||
### Storage Classes
|
||||
|
||||
A Tenant may be limited to use a set of allowed Storage Class resources, as follows.
|
||||
@@ -179,6 +234,19 @@ custom custom.tls/provisioner Delete WaitForFirstConsum
|
||||
glusterfs rook.io/glusterfs Delete WaitForFirstConsumer false 54m
|
||||
```
|
||||
|
||||
> The `name` label reflecting the resource name is mandatory, otherwise filtering of resources cannot be put in place
|
||||
|
||||
```yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
labels:
|
||||
name: cephfs
|
||||
name: cephfs
|
||||
provisioner: cephfs
|
||||
|
||||
```
|
||||
|
||||
### Ingress Classes
|
||||
|
||||
As for Storage Class, also Ingress Class can be enforced.
|
||||
@@ -225,9 +293,26 @@ external-lb example.com/external IngressParameters.k8s.example.com/e
|
||||
internal-lb example.com/internal IngressParameters.k8s.example.com/internal-lb 15m
|
||||
```
|
||||
|
||||
> The `name` label reflecting the resource name is mandatory, otherwise filtering of resources cannot be put in place
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
labels:
|
||||
name: external-lb
|
||||
name: external-lb
|
||||
spec:
|
||||
controller: example.com/ingress-controller
|
||||
parameters:
|
||||
apiGroup: k8s.example.com
|
||||
kind: IngressParameters
|
||||
name: external-lb
|
||||
```
|
||||
|
||||
### Priority Classes
|
||||
|
||||
Allowed PriorityClasses assigned to a Tenant Owner can be enforced as follows.
|
||||
Allowed PriorityClasses assigned to a Tenant Owner can be enforced as follows:
|
||||
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
@@ -239,12 +324,12 @@ spec:
|
||||
- kind: User
|
||||
name: alice
|
||||
proxySettings:
|
||||
- kind: IngressClasses
|
||||
- kind: PriorityClasses
|
||||
operations:
|
||||
- List
|
||||
priorityClasses:
|
||||
allowed:
|
||||
- best-effort
|
||||
- custom
|
||||
allowedRegex: "\\w+priority"
|
||||
```
|
||||
|
||||
@@ -271,67 +356,89 @@ maxpriority 1000 false 18s
|
||||
minpriority 1000 false 18s
|
||||
```
|
||||
|
||||
### Storage/Ingress class and PriorityClass required label
|
||||
|
||||
For Storage Class, Ingress Class and Priority Class resources, the `name` label reflecting the resource name is mandatory, otherwise filtering of resources cannot be put in place.
|
||||
> The `name` label reflecting the resource name is mandatory, otherwise filtering of resources cannot be put in place
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
labels:
|
||||
name: my-storage-class
|
||||
name: my-storage-class
|
||||
provisioner: org.tld/my-storage-class
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
labels:
|
||||
name: external-lb
|
||||
name: external-lb
|
||||
spec:
|
||||
controller: example.com/ingress-controller
|
||||
parameters:
|
||||
apiGroup: k8s.example.com
|
||||
kind: IngressParameters
|
||||
name: external-lb
|
||||
---
|
||||
apiVersion: scheduling.k8s.io/v1
|
||||
kind: PriorityClass
|
||||
metadata:
|
||||
labels:
|
||||
name: best-effort
|
||||
name: best-effort
|
||||
name: custom
|
||||
name: custom
|
||||
value: 1000
|
||||
globalDefault: false
|
||||
description: "Priority class for best-effort Tenants"
|
||||
description: "Priority class for Tenants"
|
||||
```
|
||||
|
||||
## Does it work with kubectl?
|
||||
Yes, it works by intercepting all the requests from the `kubectl` client directed to the APIs server. It works with both users who use the TLS certificate authentication and those who use OIDC.
|
||||
### ProxySetting Use Case
|
||||
Consider a scenario, where a cluster admin creates a tenant and assign ownership of the tenant to a user, so called tenant owner. Afterwards, tenant owner would in turn like to provide access to their cluster-scoped resources to a set of users (e.g. non-owners or tenant users), groups and service accounts, who doesn't require tenant owner level permissions.
|
||||
|
||||
As tenant owner `alice`, you are able to use `kubectl` to create some namespaces:
|
||||
Tenant Owner can provide access to following cluster-scoped resources to their tenant users, groups and service account by creating `ProxySetting` resource
|
||||
- `Nodes`
|
||||
- `StorageClasses`
|
||||
- `IngressClasses`
|
||||
- `PriorityClasses`
|
||||
|
||||
Each Resource kind can be granted with following verbs, such as:
|
||||
- `List`
|
||||
- `Update`
|
||||
- `Delete`
|
||||
|
||||
These tenant users, groups and services accounts have less privileged access than tenant owners.
|
||||
|
||||
As a Tenant Owner `alice`, you can create a `ProxySetting` resources to allow `bob` to list nodes, storage classes, ingress classes and priority classes
|
||||
```yaml
|
||||
apiVersion: capsule.clastix.io/v1beta1
|
||||
kind: ProxySetting
|
||||
metadata:
|
||||
name: sre-readers
|
||||
namespace: solar-production
|
||||
spec:
|
||||
subjects:
|
||||
- name: bob
|
||||
kind: User
|
||||
proxySettings:
|
||||
- kind: Nodes
|
||||
operations:
|
||||
- List
|
||||
- kind: StorageClasses
|
||||
operations:
|
||||
- List
|
||||
- kind: IngressClasses
|
||||
operations:
|
||||
- List
|
||||
- kind: PriorityClasses
|
||||
operations:
|
||||
- List
|
||||
```
|
||||
$ kubectl --context alice-oidc@mycluster create namespace oil-production
|
||||
$ kubectl --context alice-oidc@mycluster create namespace oil-development
|
||||
$ kubectl --context alice-oidc@mycluster create namespace gas-marketing
|
||||
As a Tenant User `bob`, you can list nodes, storage classes, ingress classes and priority classes
|
||||
|
||||
```bash
|
||||
$ kubectl auth can-i --context bob-oidc@mycluster get nodes
|
||||
yes
|
||||
$ kubectl auth can-i --context bob-oidc@mycluster get storageclasses
|
||||
yes
|
||||
$ kubectl auth can-i --context bob-oidc@mycluster get ingressclasses
|
||||
yes
|
||||
$ kubectl auth can-i --context bob-oidc@mycluster get priorityclasses
|
||||
yes
|
||||
```
|
||||
## HTTP support
|
||||
Capsule proxy supports `https` and `http`, although the latter is not recommended, we understand that it can be useful for some use cases (i.e. development, working behind a TLS-terminated reverse proxy and so on). As the default behaviour is to work with `https`, we need to use the flag `--enable-ssl=false` if we really want to work under `http`.
|
||||
|
||||
After having the `capsule-proxy` working under `http`, requests must provide authentication using an allowed Bearer Token.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ TOKEN=<type your TOKEN>
|
||||
$ curl -H "Authorization: Bearer $TOKEN" http://localhost:9001/api/v1/namespaces
|
||||
```
|
||||
|
||||
and list only those namespaces:
|
||||
```
|
||||
$ kubectl --context alice-oidc@mycluster get namespaces
|
||||
NAME STATUS AGE
|
||||
gas-marketing Active 2m
|
||||
oil-development Active 2m
|
||||
oil-production Active 2m
|
||||
```
|
||||
> NOTE: `kubectl` will not work against a `http` server.
|
||||
|
||||
# What’s next
|
||||
Have a fun with `capsule-proxy`:
|
||||
## Contributing
|
||||
|
||||
* [Standalone Installation](./standalone.md)
|
||||
* [Sidecar Installation](./sidecar.md)
|
||||
* [OIDC Authentication](./oidc-auth.md)
|
||||
* [Contributing](./contributing.md)
|
||||
`capsule-proxy` is an open-source software released with Apache2 [license](https://github.com/clastix/capsule-proxy/blob/master/LICENSE).
|
||||
|
||||
Contributing guidelines are available [here](https://github.com/clastix/capsule-proxy/blob/master/CONTRIBUTING.md).
|
||||
@@ -1,19 +1,12 @@
|
||||
# Reference
|
||||
|
||||
* [Custom Resource Definition](#customer-resource-definition)
|
||||
* [Capsule Configuration](#capsule-configuration)
|
||||
* [Capsule Permissions](#capsule-permissions)
|
||||
* [Admission Controllers](#admission-controller)
|
||||
* [Command Options](#command-options)
|
||||
* [Created Resources](#created-resources)
|
||||
Reference document for Capsule Operator configuration
|
||||
|
||||
## Custom Resource Definition
|
||||
|
||||
Capsule operator uses a Custom Resources Definition (CRD) for _Tenants_. In Capsule, Tenants are cluster wide resources. You need cluster level permissions to work with tenants.
|
||||
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:
|
||||
|
||||
You can learn about tenant CRD by the `kubectl explain` command:
|
||||
|
||||
```command
|
||||
```
|
||||
kubectl explain tenant
|
||||
|
||||
KIND: Tenant
|
||||
@@ -24,11 +17,15 @@ DESCRIPTION:
|
||||
|
||||
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:
|
||||
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:
|
||||
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>
|
||||
@@ -44,7 +41,7 @@ FIELDS:
|
||||
|
||||
For Tenant spec:
|
||||
|
||||
```command
|
||||
```
|
||||
kubectl explain tenant.spec
|
||||
|
||||
KIND: Tenant
|
||||
@@ -76,9 +73,8 @@ FIELDS:
|
||||
IngressClass. Optional.
|
||||
|
||||
limitRanges <Object>
|
||||
Specifies the NetworkPolicies assigned to the Tenant. The assigned
|
||||
NetworkPolicies are inherited by any namespace created in the Tenant.
|
||||
Optional.
|
||||
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
|
||||
@@ -124,7 +120,7 @@ FIELDS:
|
||||
|
||||
and Tenant status:
|
||||
|
||||
```command
|
||||
```
|
||||
kubectl explain tenant.status
|
||||
KIND: Tenant
|
||||
VERSION: capsule.clastix.io/v1beta1
|
||||
@@ -155,22 +151,32 @@ apiVersion: capsule.clastix.io/v1alpha1
|
||||
kind: CapsuleConfiguration
|
||||
metadata:
|
||||
name: default
|
||||
annotations:
|
||||
capsule.clastix.io/ca-secret-name: "capsule-ca"
|
||||
capsule.clastix.io/tls-secret-name: "capsule-tls"
|
||||
capsule.clastix.io/mutating-webhook-configuration-name: "capsule-mutating-webhook-configuration"
|
||||
capsule.clastix.io/validating-webhook-configuration-name: "capsule-validating-webhook-configuration"
|
||||
spec:
|
||||
userGroups: ["capsule.clastix.io"]
|
||||
forceTenantPrefix: false
|
||||
protectedNamespaceRegex: ""
|
||||
```
|
||||
|
||||
Option | Description | Default
|
||||
--- | --- | ---
|
||||
`.spec.forceTenantPrefix` | Force the tenant name as prefix for namespaces: `<tenant_name>-<namespace>`. | `false`
|
||||
`.spec.userGroups` | Array of Capsule groups to which all tenant owners must belong. | `[capsule.clastix.io]`
|
||||
`.spec.protectedNamespaceRegex` | Disallows creation of namespaces matching the passed regexp. | `null`
|
||||
Option | Description | Default
|
||||
--- |------------------------------------------------------------------------------| ---
|
||||
`.spec.forceTenantPrefix` | Force the tenant name as prefix for namespaces: `<tenant_name>-<namespace>`. | `false`
|
||||
`.spec.userGroups` | Array of Capsule groups to which all tenant owners must belong. | `[capsule.clastix.io]`
|
||||
`.spec.protectedNamespaceRegex` | Disallows creation of namespaces matching the passed regexp. | `null`
|
||||
`.metadata.annotations.capsule.clastix.io/ca-secret-name` | Set the Capsule Certificate Authority secret name | `capsule-ca`
|
||||
`.metadata.annotations.capsule.clastic.io/tls-secret-name` | Set the Capsule TLS secret name | `capsule-tls`
|
||||
`.metadata.annotations.capsule.clastix.io/mutating-webhook-configuration-name` | Set the MutatingWebhookConfiguration name | `mutating-webhook-configuration-name`
|
||||
`.metadata.annotations.capsule.clastix.io/validating-webhook-configuration-name` | Set the ValidatingWebhookConfiguration name | `validating-webhook-configuration-name`
|
||||
|
||||
Upon installation using Kustomize or Helm, a `capsule-default` resource will be created.
|
||||
The reference to this configuration is managed by the CLI flag `--configuration-name`.
|
||||
|
||||
## Capsule Permissions
|
||||
|
||||
In the current implementation, the Capsule operator requires cluster admin permissions to fully operate. Make sure you deploy Capsule having access to the default `cluster-admin` ClusterRole.
|
||||
|
||||
## Admission Controllers
|
||||
@@ -213,6 +219,7 @@ Option | Description | Default
|
||||
|
||||
|
||||
## Created Resources
|
||||
|
||||
Once installed, the Capsule operator creates the following resources in your cluster:
|
||||
|
||||
```
|
||||
1688
docs/content/general/tutorial.md
Normal file
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |