Compare commits

...

120 Commits

Author SHA1 Message Date
Dario Tranchitella
c0d4aab582 build(helm): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
6761fb93dc build(kustomize): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
bf9e0f6b10 test: PriorityClass proxy operations conversion 2021-07-21 16:48:13 +02:00
Dario Tranchitella
f937942c49 feat: capsule-proxy operations for PriorityClass resources 2021-07-21 16:48:13 +02:00
Dario Tranchitella
89d7f301c6 build(helm): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
2a6ff09340 build(kustomize): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
35f48107fc test(e2e): aligning tests to new v1beta1 structure and ExternalName case 2021-07-21 14:34:56 +02:00
Dario Tranchitella
7aa62b6f1d test: conversion for new Service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
58645f39bb chore(samples): example for ServiceOptions 2021-07-21 14:34:56 +02:00
Dario Tranchitella
0e55823a0c feat: toggling ExternalName service 2021-07-21 14:34:56 +02:00
Maksim Fedotov
ba690480a7 refactor: use OwnerListSpec to store tenant owners information 2021-07-20 11:21:40 +02:00
Maksim Fedotov
faa2306a30 chore: support multiple groups in create-{user}/{user-openshift}.sh scripts 2021-07-20 11:21:40 +02:00
Dario Tranchitella
c1448c82e9 build(installer): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
776a56b5bc build(helm): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e4883bb737 build(kustomize): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e70afb5e77 feat: add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
ee7af18f98 docs: bare installation of Capsule using kubectl 2021-07-19 15:21:56 +02:00
Dario Tranchitella
ac7de3bf88 chore(github): updating steps for single YAML file installer diffs 2021-07-19 15:21:56 +02:00
Dario Tranchitella
8883b15aa9 chore: single YAML file installer 2021-07-19 15:21:56 +02:00
Dario Tranchitella
e23132c820 chore(kustomize): using single YAML file to install Capsule 2021-07-19 15:21:56 +02:00
Dario Tranchitella
bec59a585e build(kustomize): updating to v0.1.0-rc3 2021-07-19 15:21:56 +02:00
Dario Tranchitella
9c649ac7eb chore(kustomize): adding v1beta1 Tenant 2021-07-19 15:05:22 +02:00
Dario Tranchitella
3455aed503 fix(samples): Tenant v1beta1 example 2021-07-19 15:05:22 +02:00
Dario Tranchitella
ad1edf57ac fix(samples): removing empty file 2021-07-19 15:05:22 +02:00
Dario Tranchitella
d64dcb5a44 fix: preserving v1alpha1 enable node ports false value avoiding CRD default 2021-07-19 08:15:24 +02:00
Dave
76d7697703 docs: minor improvements 2021-07-16 17:52:16 +02:00
alegrey91
96f4f31c17 docs(velero): add brief explanation about new cli flag 2021-07-16 09:19:36 +02:00
alegrey91
c3f9dfe652 feat(velero): improve usage function 2021-07-16 09:19:36 +02:00
alegrey91
502e9a556f feat(velero): add possibility to specify a tenant list by cli 2021-07-16 09:19:36 +02:00
alegrey91
6f208a6e0e fix(velero): fix wrong argument behaviour 2021-07-16 09:19:36 +02:00
alegrey91
1fb52003d5 fix(velero): add possibility to fix also apiVersion parameter 2021-07-16 09:19:36 +02:00
Dario Tranchitella
98e1640d9b fix: avoid nil slice during resource conversion 2021-07-14 20:54:43 +02:00
Maksim Fedotov
eb19a7a89f chore: fix linting issues 2021-07-12 11:27:13 +02:00
Maksim Fedotov
db8b8ac1d9 test(e2e): support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Maksim Fedotov
663ce93a3e build(helm): support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Maksim Fedotov
a6408f26b0 feat: support multiple tenant owners(add applications to act as tenant owners) 2021-07-12 11:27:13 +02:00
Dario Tranchitella
1aa026c977 chore(github): no need of fundings 2021-07-08 11:36:15 +02:00
Dario Tranchitella
6008373960 bug: ensuring to update the conversion webhook CA bundle 2021-07-05 17:58:49 +02:00
Dario Tranchitella
414c03a874 feat: reconciliation for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
4d34a9e3d7 build(helm): support for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
cb9b560926 build(kustomize): support for Tenant state 2021-07-05 16:28:39 +02:00
Dario Tranchitella
ef75d0496a feat(api): Tenant state 2021-07-05 16:28:39 +02:00
alegrey91
e1e75a093b docs(velero): add documentation about velero-restore script 2021-07-05 15:53:27 +02:00
alegrey91
80143ffd50 feat(velero): add script to manage velero backup restoration 2021-07-05 15:53:27 +02:00
Dario Tranchitella
3d54810f19 chore: bump-up to latest version 2021-07-05 13:55:39 +02:00
Dario Tranchitella
09dfe33a6a bug(kustomize): fixing JSON path for kustomize-based installation 2021-07-05 13:55:39 +02:00
Dario Tranchitella
01ea36b462 chore: updating kustomize 2021-07-05 13:55:39 +02:00
Dario Tranchitella
bd448d8c29 test(e2e): avoiding flaky tests for ingress hostnames collision 2021-07-02 11:16:52 +02:00
Maksim Fedotov
b58ca3a7d7 chore: v1beta1 goimports and formatting 2021-07-02 10:14:06 +02:00
Maksim Fedotov
52fb0948cb feat(v1beta1): add conversion webhook 2021-07-02 10:14:06 +02:00
Maksim Fedotov
1b0fa587eb chore: remove unused functions for v1alpha1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
92655f1872 build(helm): update crds to use v1beta1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
44bf846260 test(e2e): update tests to use v1beta1 version 2021-07-02 10:14:06 +02:00
Maksim Fedotov
e6b433dcd7 feat(v1beta1): update code to use v1beta1 version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
3e0882dbc8 refactor: domains is now API utils 2021-07-02 10:14:06 +02:00
Dario Tranchitella
416609362d feat(v1beta1): tenant spec
feat(v1beta1): remove unused structs and functions from v1beta1. Rename v1alpha1 structs to follow new naming. Move v1alpha1 structs to separate files
2021-07-02 10:14:06 +02:00
Dario Tranchitella
3d714dc124 build(kustomize)!: adding the conversion endpoint for v1beta1 2021-07-02 10:14:06 +02:00
Dario Tranchitella
bd01881dd3 feat(v1beta1): scaffolding the Convertible interface 2021-07-02 10:14:06 +02:00
Dario Tranchitella
ac6af13b07 feat(v1beta1): registering conversion webhook 2021-07-02 10:14:06 +02:00
Dario Tranchitella
8fb4b7d4a1 feat: scaffolding v1beta1 Tenant version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
d4280b8d7e chore(makefile): ensure validation for each version 2021-07-02 10:14:06 +02:00
Dario Tranchitella
6e39b17e7c chore(operatorsdk): required scaffolding for v1alpha2 2021-07-02 10:14:06 +02:00
Dario Tranchitella
b1a9603faa fix: ensuring single reconciliation for Capsule RoleBinding resources 2021-07-01 16:34:18 +02:00
alessio
0d4201a6c2 docs(helm): update documentation about hostNetwork 2021-06-29 11:03:07 +02:00
alegrey91
1734c906a9 build(helm): add hostNetwork for manager pod 2021-06-29 11:03:07 +02:00
Dario Tranchitella
184f054f2f test(e2e): adding further tests for collisions 2021-06-27 22:40:23 +02:00
Dario Tranchitella
126449b796 build(helm): fixing pairing between values and collision CRD keys 2021-06-27 22:40:23 +02:00
Dario Tranchitella
284e7da66f build(helm): support for admission review version to v1 2021-06-27 22:36:55 +02:00
Dario Tranchitella
99e1589828 build(helm)!: using multiple handlers per webhook 2021-06-27 22:36:55 +02:00
Dario Tranchitella
7cc2c3f4e9 build(kustomize)!: using multiple handlers per webhook 2021-06-27 22:36:55 +02:00
Dario Tranchitella
ba07f99c6e refactor!: using multiple handers per route 2021-06-27 22:36:55 +02:00
Petr Ruzicka
d79972691e docs: Amazon EKS documentation 2021-06-27 21:07:41 +02:00
ptx96
25f021e151 docs(helm): described serviceMonitor parameters 2021-06-25 12:03:07 +02:00
ptx96
320353e561 fix: adapted parameters for kustomize 2021-06-25 12:03:07 +02:00
ptx96
5bd2af89e4 feat(kustomize): added capsule dashboard 2021-06-25 12:03:07 +02:00
ptx96
61d394f844 feat(helm): added serviceMonitor parameters 2021-06-25 12:03:07 +02:00
ptx96
fc8f5a08fe feat(helm): added capsule dashboard 2021-06-25 12:03:07 +02:00
ptx96
ffcb7c4408 feat: added capsule dashboard 2021-06-25 12:03:07 +02:00
Dario Tranchitella
8d1a109f1c build(helm): webhook for Namespace handling when tenant is freezed 2021-06-24 13:47:43 +02:00
Dario Tranchitella
a19045419d build(kustomize): webhook for Namespace handling when tenant is freezed 2021-06-24 13:47:43 +02:00
Dario Tranchitella
7574335a8a refactor: using separated webhooks for Namespace handling 2021-06-24 13:47:43 +02:00
Dario Tranchitella
72e97b9960 feat: providing utility for webhook auth identification 2021-06-24 13:47:43 +02:00
Dario Tranchitella
b3c6082a1e feat: providing event for Tenant cordoning 2021-06-24 13:47:43 +02:00
Dario Tranchitella
9a940096c9 docs: fixing links 2021-06-24 13:47:43 +02:00
Dario Tranchitella
f9becf39e5 docs: Tenant cordoning 2021-06-24 13:47:43 +02:00
Dario Tranchitella
e1160b8862 test(e2e): Tenant cordoning webhook 2021-06-24 13:47:43 +02:00
Dario Tranchitella
6472b221ed build(helm): Tenant cordononing webhook 2021-06-24 13:47:43 +02:00
Dario Tranchitella
a2e5bbf26d build(kustomize): Tenant cordoning webhook 2021-06-24 13:47:43 +02:00
Dario Tranchitella
8804496bb2 feat: cordoning Tenant webhook 2021-06-24 13:47:43 +02:00
Dario Tranchitella
5de0a6d712 # This is a combination of 2 commits.
# This is the 1st commit message:

feat: cordoning Tenant webhook

# The commit message #2 will be skipped:

# 5cc
2021-06-24 13:47:43 +02:00
Dario Tranchitella
531cc4cf14 refactor: renaming Tenant webhook handler 2021-06-24 13:47:43 +02:00
alessio
3e33290c4c fix: fixed typo in script description 2021-06-23 17:57:09 +02:00
alessio
824442b9ee feat: add exits when encounters an error 2021-06-23 17:57:09 +02:00
Dario Tranchitella
34583352e5 refactor: meaningful error for complete block of Service external IPs 2021-06-21 12:12:04 +02:00
Dario Tranchitella
5681228789 fix: blocking non valid external IP 2021-06-21 12:12:04 +02:00
Dario Tranchitella
7237972b80 fix: using /32 in case of bare IPs 2021-06-21 12:12:04 +02:00
Dario Tranchitella
46fc65a988 fix: avoiding concurrent map write 2021-06-16 08:49:50 +02:00
bsctl
44acfaed86 feat: fix typo in event message 2021-06-15 21:42:39 +02:00
bsctl
7ca087cac5 feat: update event messages 2021-06-15 21:42:39 +02:00
Dario Tranchitella
b2b640dc96 test(e2e): refactoring to avoid flakiness 2021-06-15 21:42:39 +02:00
Dario Tranchitella
5b35e0b0d5 refactor(e2e): using non absolute version import name 2021-06-15 21:42:39 +02:00
Dario Tranchitella
accd9ca038 feat: emitting events for policies violations 2021-06-15 21:42:39 +02:00
Dario Tranchitella
e7b33bda26 docs: documenting ImagePullPolicy enforcement 2021-06-14 10:53:55 +02:00
Dario Tranchitella
08fbd26ec8 test(e2e): bug on PodPriorityClass case 2021-06-14 10:53:55 +02:00
Dario Tranchitella
006b0c80cf test(e2e): ImagePullPolicy for v1alpha using annotations 2021-06-14 10:53:55 +02:00
Dario Tranchitella
b6f3fccbea build(helm): webhook for ImagePullPolicy enforcement 2021-06-14 10:53:55 +02:00
Dario Tranchitella
bf79c25a8a build(kustomize): webhook for image PullPolicy 2021-06-14 10:53:55 +02:00
Dario Tranchitella
630e802708 feat: image PullPolicy webhook enforcer 2021-06-14 10:53:55 +02:00
Dario Tranchitella
e5a1861cac test: aligning to new additional RoleBinding name pattern 2021-06-07 14:56:03 +02:00
Dario Tranchitella
246c1a3c2c fix: misleading info message for additional RoleBindings sync 2021-06-07 14:56:03 +02:00
Dario Tranchitella
a06e68945c fix: avoiding Namespace's RoleBinding labels collision 2021-06-07 14:56:03 +02:00
Dario Tranchitella
61c9bc647c refactor: object labels must be set in the mutateFn 2021-06-06 22:18:51 +02:00
Dario Tranchitella
9c8b0377dc feat: emitting events for Tenant operations 2021-06-06 22:18:51 +02:00
ptx96
dfe0f5ea49 chore: do docker(x) build/push step in gh actions 2021-06-05 16:12:14 +02:00
ptx96
a1a2e5e00c build(helm): using arm compatible kubectl image 2021-06-05 16:12:14 +02:00
ptx96
20aa7657e4 build: using targetarch for arm support 2021-06-05 16:12:14 +02:00
Dario Tranchitella
7c1592e739 chore(license): switching over SPDX license header (#280) 2021-06-03 19:46:20 +02:00
Dario Tranchitella
f60f2b1542 build: using Quay.io-hoested builder image 2021-06-03 18:20:47 +02:00
alessio
53377e994c docs: Updated Golang version 2021-06-03 16:25:38 +02:00
alessio
d0893a5aa9 docs: Fixed typo 2021-06-03 14:32:54 +02:00
219 changed files with 11825 additions and 4881 deletions

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [prometherion]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -23,6 +23,8 @@ jobs:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Go modules
uses: actions/cache@v1
env:
@@ -35,10 +37,10 @@ jobs:
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
- run: make manifests
- name: Checking if manifests are disaligned
- 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
- name: Checking if manifests generated untracked files
- name: Checking if YAML installer generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted
run: test -z "$(git diff 2> /dev/null)"

73
.github/workflows/docker-ci.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: docker-ci
on:
push:
tags:
- "v*"
jobs:
docker-ci:
runs-on: ubuntu-20.04
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: |
quay.io/${{ github.repository }}
tags: |
type=semver,pattern={{raw}}
flavor: |
latest=false
-
name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
platforms: arm64,arm
-
name: Set up Docker Buildx
id: buildx
with:
install: true
uses: docker/setup-buildx-action@v1
-
name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
echo "Status: ${{ steps.buildx.outputs.status }}"
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
-
name: Login to quay.io Container Registry
uses: docker/login-action@v1
with:
registry: quay.io
username: ${{ github.repository_owner }}+github
password: ${{ secrets.BOT_QUAY_IO }}
-
name: Build and push
id: build-release
uses: docker/build-push-action@v2
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64,linux/arm
push: true
tags: ${{ steps.meta.outputs.tags }}
-
name: Image digest
run: echo ${{ steps.build-release.outputs.digest }}

View File

@@ -1,6 +1,7 @@
# Build the manager binary
FROM golang:1.16 as builder
ARG TARGETARCH
ARG GIT_HEAD_COMMIT
ARG GIT_TAG_COMMIT
ARG GIT_LAST_TAG
@@ -24,7 +25,7 @@ COPY controllers/ controllers/
COPY pkg/ pkg/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build \
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH GO111MODULE=on go build \
-gcflags "-N -l" \
-ldflags "-X main.GitRepo=$GIT_REPO -X main.GitTag=$GIT_LAST_TAG -X main.GitCommit=$GIT_HEAD_COMMIT -X main.GitDirty=$GIT_MODIFIED -X main.BuildTime=$BUILD_DATE" \
-o manager

View File

@@ -15,7 +15,7 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# Image URL to use all building/pushing image targets
IMG ?= quay.io/clastix/capsule:$(VERSION)
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
CRD_OPTIONS ?= "crd:preserveUnknownFields=false"
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
@@ -47,22 +47,26 @@ manager: generate fmt vet
run: generate manifests
go run ./main.go
# Creates the single file to install Capsule without any external dependency
installer: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > config/install.yaml
# Install CRDs into a cluster
install: manifests kustomize
install: installer
$(KUSTOMIZE) build config/crd | kubectl apply -f -
# Uninstall CRDs from a cluster
uninstall: manifests kustomize
uninstall: installer
$(KUSTOMIZE) build config/crd | kubectl delete -f -
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
deploy: installer
kubectl apply -f config/install.yaml
# Remove controller in the configured Kubernetes cluster in ~/.kube/config
remove: manifests kustomize
$(KUSTOMIZE) build config/default | kubectl delete -f -
remove: installer
kubectl delete -f config/install.yaml
kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
@@ -87,37 +91,27 @@ docker-build: test
docker-push:
docker push ${IMG}
# find or download controller-gen
# download controller-gen if necessary
controller-gen:
ifeq (, $(shell which controller-gen))
@{ \
set -e ;\
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$CONTROLLER_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
}
CONTROLLER_GEN=$(GOBIN)/controller-gen
else
CONTROLLER_GEN=$(shell which controller-gen)
endif
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)
kustomize:
ifeq (, $(shell which kustomize))
@{ \
set -e ;\
KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\
cd $$KUSTOMIZE_GEN_TMP_DIR ;\
go mod init tmp ;\
go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\
rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\
}
KUSTOMIZE=$(GOBIN)/kustomize
else
KUSTOMIZE=$(shell which kustomize)
endif
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)
# 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
@[ -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 ;\
}
endef
# Generate bundle manifests and metadata, then validate generated files.
bundle: manifests

40
PROJECT
View File

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

View File

@@ -61,17 +61,28 @@ 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 [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
* Use the [single YAML file installer](./config/install.yaml)
## Install with kustomize
Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
## Install with the single YAML file installer
Ensure you have `kubectl` installed in your `PATH`.
Clone this repository and move to the repo folder:
```
$ git clone https://github.com/clastix/capsule
$ cd capsule
$ make deploy
$ kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/install.yaml
namespace/capsule-system created
customresourcedefinition.apiextensions.k8s.io/capsuleconfigurations.capsule.clastix.io created
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding created
secret/capsule-ca created
secret/capsule-tls created
service/capsule-controller-manager-metrics-service created
service/capsule-webhook-service created
deployment.apps/capsule-controller-manager created
capsuleconfiguration.capsule.clastix.io/capsule-default created
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration created
validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration created
```
It will install the Capsule controller in a dedicated namespace `capsule-system`.

View File

@@ -0,0 +1,9 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import rbacv1 "k8s.io/api/rbac/v1"
type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -0,0 +1,500 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
const (
podAllowedImagePullPolicyAnnotation = "capsule.clastix.io/allowed-image-pull-policy"
podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed"
podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex"
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableExternalNameAnnotation = "capsule.clastix.io/enable-external-name"
ownerGroupsAnnotation = "owners.capsule.clastix.io/group"
ownerUsersAnnotation = "owners.capsule.clastix.io/user"
ownerServiceAccountAnnotation = "owners.capsule.clastix.io/serviceaccount"
enableNodeListingAnnotation = "capsule.clastix.io/enable-node-listing"
enableNodeUpdateAnnotation = "capsule.clastix.io/enable-node-update"
enableNodeDeletionAnnotation = "capsule.clastix.io/enable-node-deletion"
enableStorageClassListingAnnotation = "capsule.clastix.io/enable-storageclass-listing"
enableStorageClassUpdateAnnotation = "capsule.clastix.io/enable-storageclass-update"
enableStorageClassDeletionAnnotation = "capsule.clastix.io/enable-storageclass-deletion"
enableIngressClassListingAnnotation = "capsule.clastix.io/enable-ingressclass-listing"
enableIngressClassUpdateAnnotation = "capsule.clastix.io/enable-ingressclass-update"
enableIngressClassDeletionAnnotation = "capsule.clastix.io/enable-ingressclass-deletion"
enablePriorityClassListingAnnotation = "capsule.clastix.io/enable-priorityclass-listing"
enablePriorityClassUpdateAnnotation = "capsule.clastix.io/enable-priorityclass-update"
enablePriorityClassDeletionAnnotation = "capsule.clastix.io/enable-priorityclass-deletion"
)
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
var serviceKindToAnnotationMap = map[capsulev1beta1.ProxyServiceKind][]string{
capsulev1beta1.NodesProxy: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
capsulev1beta1.StorageClassesProxy: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
capsulev1beta1.IngressClassesProxy: {enableIngressClassListingAnnotation, enableIngressClassUpdateAnnotation, enableIngressClassDeletionAnnotation},
capsulev1beta1.PriorityClassesProxy: {enablePriorityClassListingAnnotation, enablePriorityClassUpdateAnnotation, enablePriorityClassDeletionAnnotation},
}
var annotationToOperationMap = map[string]capsulev1beta1.ProxyOperation{
enableNodeListingAnnotation: capsulev1beta1.ListOperation,
enableNodeUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableNodeDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableStorageClassListingAnnotation: capsulev1beta1.ListOperation,
enableStorageClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableStorageClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableIngressClassListingAnnotation: capsulev1beta1.ListOperation,
enableIngressClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableIngressClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enablePriorityClassListingAnnotation: capsulev1beta1.ListOperation,
enablePriorityClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enablePriorityClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
}
var 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)
for serviceKind, operationAnnotations := range serviceKindToAnnotationMap {
for _, operationAnnotation := range operationAnnotations {
val, ok := annotations[operationAnnotation]
if ok {
for _, owner := range strings.Split(val, ",") {
if _, exists := operations[owner]; !exists {
operations[owner] = make(map[capsulev1beta1.ProxyServiceKind][]capsulev1beta1.ProxyOperation)
}
operations[owner][serviceKind] = append(operations[owner][serviceKind], annotationToOperationMap[operationAnnotation])
}
}
}
}
var owners capsulev1beta1.OwnerListSpec
var getProxySettingsForOwner = func(ownerName string) (settings []capsulev1beta1.ProxySettings) {
ownerOperations, ok := operations[ownerName]
if ok {
for k, v := range ownerOperations {
settings = append(settings, capsulev1beta1.ProxySettings{
Kind: k,
Operations: v,
})
}
}
return
}
owners = append(owners, capsulev1beta1.OwnerSpec{
Kind: capsulev1beta1.OwnerKind(t.Spec.Owner.Kind),
Name: t.Spec.Owner.Name,
ProxyOperations: getProxySettingsForOwner(t.Spec.Owner.Name),
})
for ownerAnnotation, ownerKind := range annotationToOwnerKindMap {
val, ok := annotations[ownerAnnotation]
if ok {
for _, owner := range strings.Split(val, ",") {
owners = append(owners, capsulev1beta1.OwnerSpec{
Kind: ownerKind,
Name: owner,
ProxyOperations: getProxySettingsForOwner(owner),
})
}
}
}
return owners
}
func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*capsulev1beta1.Tenant)
annotations := t.GetAnnotations()
// ObjectMeta
dst.ObjectMeta = t.ObjectMeta
// Spec
dst.Spec.NamespaceQuota = t.Spec.NamespaceQuota
dst.Spec.NodeSelector = t.Spec.NodeSelector
dst.Spec.Owners = t.convertV1Alpha1OwnerToV1Beta1()
if t.Spec.NamespacesMetadata != nil {
dst.Spec.NamespacesMetadata = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.NamespacesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if t.Spec.ServicesMetadata != nil {
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
},
}
}
}
if t.Spec.StorageClasses != nil {
dst.Spec.StorageClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.StorageClasses.Exact,
Regex: t.Spec.StorageClasses.Regex,
}
}
if t.Spec.IngressClasses != nil {
dst.Spec.IngressClasses = &capsulev1beta1.AllowedListSpec{
Exact: t.Spec.IngressClasses.Exact,
Regex: t.Spec.IngressClasses.Regex,
}
}
if t.Spec.IngressHostnames != nil {
dst.Spec.IngressHostnames = &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{
Items: t.Spec.NetworkPolicies,
}
}
if len(t.Spec.LimitRanges) > 0 {
dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{
Items: t.Spec.LimitRanges,
}
}
if len(t.Spec.ResourceQuota) > 0 {
dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{
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{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if t.Spec.ExternalServiceIPs != nil {
dst.Spec.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: make([]capsulev1beta1.AllowedIP, len(t.Spec.ExternalServiceIPs.Allowed)),
}
for i, IP := range t.Spec.ExternalServiceIPs.Allowed {
dst.Spec.ExternalServiceIPs.Allowed[i] = capsulev1beta1.AllowedIP(IP)
}
}
pullPolicies, ok := annotations[podAllowedImagePullPolicyAnnotation]
if ok {
for _, policy := range strings.Split(pullPolicies, ",") {
dst.Spec.ImagePullPolicies = append(dst.Spec.ImagePullPolicies, capsulev1beta1.ImagePullPolicySpec(policy))
}
}
priorityClasses := capsulev1beta1.AllowedListSpec{}
priorityClassAllowed, ok := annotations[podPriorityAllowedAnnotation]
if ok {
priorityClasses.Exact = strings.Split(priorityClassAllowed, ",")
}
priorityClassesRegexp, ok := annotations[podPriorityAllowedRegexAnnotation]
if ok {
priorityClasses.Regex = priorityClassesRegexp
}
if !reflect.ValueOf(priorityClasses).IsZero() {
dst.Spec.PriorityClasses = &priorityClasses
}
enableNodePorts, ok := annotations[enableNodePortsAnnotation]
if ok {
val, err := strconv.ParseBool(enableNodePorts)
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)
}
enableExternalName, ok := annotations[enableExternalNameAnnotation]
if ok {
val, err := strconv.ParseBool(enableExternalName)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
}
// 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)
delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation)
delete(dst.ObjectMeta.Annotations, enableExternalNameAnnotation)
delete(dst.ObjectMeta.Annotations, ownerGroupsAnnotation)
delete(dst.ObjectMeta.Annotations, ownerUsersAnnotation)
delete(dst.ObjectMeta.Annotations, ownerServiceAccountAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodeDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableStorageClassDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassDeletionAnnotation)
return nil
}
func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
var ownersAnnotations = map[string][]string{
ownerGroupsAnnotation: nil,
ownerUsersAnnotation: nil,
ownerServiceAccountAnnotation: nil,
}
var proxyAnnotations = map[string][]string{
enableNodeListingAnnotation: nil,
enableNodeUpdateAnnotation: nil,
enableNodeDeletionAnnotation: nil,
enableStorageClassListingAnnotation: nil,
enableStorageClassUpdateAnnotation: nil,
enableStorageClassDeletionAnnotation: nil,
enableIngressClassListingAnnotation: nil,
enableIngressClassUpdateAnnotation: nil,
enableIngressClassDeletionAnnotation: nil,
}
for i, owner := range src.Spec.Owners {
if i == 0 {
t.Spec.Owner = OwnerSpec{
Name: owner.Name,
Kind: Kind(owner.Kind),
}
} else {
switch owner.Kind {
case capsulev1beta1.UserOwner:
ownersAnnotations[ownerUsersAnnotation] = append(ownersAnnotations[ownerUsersAnnotation], owner.Name)
case capsulev1beta1.GroupOwner:
ownersAnnotations[ownerGroupsAnnotation] = append(ownersAnnotations[ownerGroupsAnnotation], owner.Name)
case capsulev1beta1.ServiceAccountOwner:
ownersAnnotations[ownerServiceAccountAnnotation] = append(ownersAnnotations[ownerServiceAccountAnnotation], owner.Name)
}
}
for _, setting := range owner.ProxyOperations {
switch setting.Kind {
case capsulev1beta1.NodesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableNodeListingAnnotation] = append(proxyAnnotations[enableNodeListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableNodeUpdateAnnotation] = append(proxyAnnotations[enableNodeUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableNodeDeletionAnnotation] = append(proxyAnnotations[enableNodeDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.PriorityClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enablePriorityClassListingAnnotation] = append(proxyAnnotations[enablePriorityClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enablePriorityClassUpdateAnnotation] = append(proxyAnnotations[enablePriorityClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enablePriorityClassDeletionAnnotation] = append(proxyAnnotations[enablePriorityClassDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.StorageClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableStorageClassListingAnnotation] = append(proxyAnnotations[enableStorageClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableStorageClassUpdateAnnotation] = append(proxyAnnotations[enableStorageClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableStorageClassDeletionAnnotation] = append(proxyAnnotations[enableStorageClassDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.IngressClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableIngressClassListingAnnotation] = append(proxyAnnotations[enableIngressClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableIngressClassUpdateAnnotation] = append(proxyAnnotations[enableIngressClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableIngressClassDeletionAnnotation] = append(proxyAnnotations[enableIngressClassDeletionAnnotation], owner.Name)
}
}
}
}
}
for k, v := range ownersAnnotations {
if len(v) > 0 {
t.Annotations[k] = strings.Join(v, ",")
}
}
for k, v := range proxyAnnotations {
if len(v) > 0 {
t.Annotations[k] = strings.Join(v, ",")
}
}
}
func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*capsulev1beta1.Tenant)
// ObjectMeta
t.ObjectMeta = src.ObjectMeta
// Spec
t.Spec.NamespaceQuota = src.Spec.NamespaceQuota
t.Spec.NodeSelector = src.Spec.NodeSelector
if t.Annotations == nil {
t.Annotations = make(map[string]string)
}
t.convertV1Beta1OwnerToV1Alpha1(src)
if src.Spec.NamespacesMetadata != nil {
t.Spec.NamespacesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.NamespacesMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations,
}
}
if src.Spec.StorageClasses != nil {
t.Spec.StorageClasses = &AllowedListSpec{
Exact: src.Spec.StorageClasses.Exact,
Regex: src.Spec.StorageClasses.Regex,
}
}
if src.Spec.IngressClasses != nil {
t.Spec.IngressClasses = &AllowedListSpec{
Exact: src.Spec.IngressClasses.Exact,
Regex: src.Spec.IngressClasses.Regex,
}
}
if src.Spec.IngressHostnames != nil {
t.Spec.IngressHostnames = &AllowedListSpec{
Exact: src.Spec.IngressHostnames.Exact,
Regex: src.Spec.IngressHostnames.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 {
t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items
}
if src.Spec.LimitRanges != nil {
t.Spec.LimitRanges = src.Spec.LimitRanges.Items
}
if src.Spec.ResourceQuota != nil {
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{
ClusterRoleName: rb.ClusterRoleName,
Subjects: rb.Subjects,
})
}
}
if src.Spec.ExternalServiceIPs != nil {
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: make([]AllowedIP, len(src.Spec.ExternalServiceIPs.Allowed)),
}
for i, IP := range src.Spec.ExternalServiceIPs.Allowed {
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, ",")
}
if src.Spec.PriorityClasses != nil {
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)
}
// Status
t.Status = TenantStatus{
Size: src.Status.Size,
Namespaces: src.Status.Namespaces,
}
return nil
}

View File

@@ -0,0 +1,373 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
var namespaceQuota int32 = 5
var nodeSelector = map[string]string{
"foo": "bar",
}
var v1alpha1AdditionalMetadataSpec = &AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"foo": "bar",
},
AdditionalAnnotations: map[string]string{
"foo": "bar",
},
}
var v1alpha1AllowedListSpec = &AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
var v1beta1AdditionalMetadataSpec = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"foo": "bar",
},
AdditionalAnnotations: map[string]string{
"foo": "bar",
},
}
var v1beta1ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
},
}
var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
}
var networkPolicies = []networkingv1.NetworkPolicySpec{
{
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "tenant-resources",
},
},
},
{
PodSelector: &metav1.LabelSelector{},
},
{
IPBlock: &networkingv1.IPBlock{
CIDR: "192.168.0.0/12",
},
},
},
},
},
},
}
var limitRanges = []corev1.LimitRangeSpec{
{
Limits: []corev1.LimitRangeItem{
{
Type: corev1.LimitTypePod,
Min: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
Max: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
}
var resourceQuotas = []corev1.ResourceQuotaSpec{
{
Hard: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceLimitsCPU: resource.MustParse("8"),
corev1.ResourceLimitsMemory: resource.MustParse("16Gi"),
corev1.ResourceRequestsCPU: resource.MustParse("8"),
corev1.ResourceRequestsMemory: resource.MustParse("16Gi"),
},
Scopes: []corev1.ResourceQuotaScope{
corev1.ResourceQuotaScopeNotTerminating,
},
},
}
var v1beta1Tnt = capsulev1beta1.Tenant{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "alice",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
},
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Kind: "User",
Name: "alice",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List", "Update", "Delete"},
},
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Update", "Delete"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update", "Delete"},
},
},
},
{
Kind: "User",
Name: "bob",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "User",
Name: "jack",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "PriorityClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "Group",
Name: "owner-foo",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
Kind: "Group",
Name: "owner-bar",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "IngressClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Delete"},
},
},
},
{
Kind: "ServiceAccount",
Name: "system:serviceaccount:oil-production:default",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "Nodes",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
},
},
{
Kind: "ServiceAccount",
Name: "system:serviceaccount:gas-production:gas",
ProxyOperations: []capsulev1beta1.ProxySettings{
{
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"Update"},
},
},
},
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1beta1AdditionalMetadataSpec,
ServiceOptions: v1beta1ServiceOptions,
StorageClasses: v1beta1AllowedListSpec,
IngressClasses: v1beta1AllowedListSpec,
IngressHostnames: v1beta1AllowedListSpec,
ContainerRegistries: v1beta1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{
Items: networkPolicies,
},
LimitRanges: &capsulev1beta1.LimitRangesSpec{
Items: limitRanges,
},
ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{
Items: resourceQuotas,
},
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
{
Kind: "Group",
APIGroup: "rbac.authorization.k8s.io",
Name: "system:authenticated",
},
},
},
},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
},
ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"},
PriorityClasses: &capsulev1beta1.AllowedListSpec{
Exact: []string{"default"},
Regex: "^tier-.*$",
},
},
Status: capsulev1beta1.TenantStatus{
Size: 1,
Namespaces: []string{"foo", "bar"},
},
}
var v1alpha1Tnt = Tenant{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "alice",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent",
enableExternalNameAnnotation: "false",
enableNodePortsAnnotation: "false",
podPriorityAllowedAnnotation: "default",
podPriorityAllowedRegexAnnotation: "^tier-.*$",
ownerGroupsAnnotation: "owner-foo,owner-bar",
ownerUsersAnnotation: "bob,jack",
ownerServiceAccountAnnotation: "system:serviceaccount:oil-production:default,system:serviceaccount:gas-production:gas",
enableNodeUpdateAnnotation: "alice,system:serviceaccount:oil-production:default",
enableNodeDeletionAnnotation: "alice,jack",
enableStorageClassListingAnnotation: "bob,jack",
enableStorageClassUpdateAnnotation: "alice,system:serviceaccount:gas-production:gas",
enableStorageClassDeletionAnnotation: "alice,owner-bar",
enableIngressClassListingAnnotation: "alice,owner-foo,owner-bar",
enableIngressClassUpdateAnnotation: "alice,bob",
enableIngressClassDeletionAnnotation: "alice,jack",
enablePriorityClassListingAnnotation: "jack",
},
},
Spec: TenantSpec{
Owner: OwnerSpec{
Name: "alice",
Kind: "User",
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1alpha1AdditionalMetadataSpec,
ServicesMetadata: v1alpha1AdditionalMetadataSpec,
StorageClasses: v1alpha1AllowedListSpec,
IngressClasses: v1alpha1AllowedListSpec,
IngressHostnames: v1alpha1AllowedListSpec,
ContainerRegistries: v1alpha1AllowedListSpec,
NodeSelector: nodeSelector,
NetworkPolicies: networkPolicies,
LimitRanges: limitRanges,
ResourceQuota: resourceQuotas,
AdditionalRoleBindings: []AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
{
Kind: "Group",
APIGroup: "rbac.authorization.k8s.io",
Name: "system:authenticated",
},
},
},
},
ExternalServiceIPs: &ExternalServiceIPsSpec{
Allowed: []AllowedIP{"192.168.0.1"},
},
},
Status: TenantStatus{
Size: 1,
Namespaces: []string{"foo", "bar"},
},
}
return v1alpha1Tnt, v1beta1Tnt
}
func TestConversionHub_ConvertTo(t *testing.T) {
var 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
})
sort.Slice(v1beta1ConvertedTnt.Spec.Owners, func(i, j int) bool {
return v1beta1ConvertedTnt.Spec.Owners[i].Name < v1beta1ConvertedTnt.Spec.Owners[j].Name
})
for _, owner := range v1beta1tnt.Spec.Owners {
sort.Slice(owner.ProxyOperations, func(i, j int) bool {
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{}
v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs()
err := v1alpha1ConvertedTnt.ConvertFrom(&v1beta1tnt)
if assert.NoError(t, err) {
assert.EqualValues(t, v1alpha1Tnt, v1alpha1ConvertedTnt)
}
}

View File

@@ -1,22 +0,0 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package domain
type AllowedList interface {
ExactMatch(value string) bool
RegexMatch(value string) bool
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 contains API Schema definitions for the capsule.clastix.io v1alpha1 API group
// +kubebuilder:object:generate=true

View File

@@ -1,42 +0,0 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"sort"
)
type IngressHostnamesList []string
func (hostnames IngressHostnamesList) Len() int {
return len(hostnames)
}
func (hostnames IngressHostnamesList) Swap(i, j int) {
hostnames[i], hostnames[j] = hostnames[j], hostnames[i]
}
func (hostnames IngressHostnamesList) Less(i, j int) bool {
return hostnames[i] < hostnames[j]
}
func (hostnames IngressHostnamesList) IsStringInList(value string) (ok bool) {
sort.Sort(hostnames)
i := sort.SearchStrings(hostnames, value)
ok = i < hostnames.Len() && hostnames[i] == value
return
}

17
api/v1alpha1/owner.go Normal file
View File

@@ -0,0 +1,17 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
}
// +kubebuilder:validation:Enum=User;Group
type Kind string
func (k Kind) String() string {
return string(k)
}

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
@@ -22,6 +9,13 @@ import (
corev1 "k8s.io/api/core/v1"
)
func (t *Tenant) IsCordoned() bool {
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
return true
}
return false
}
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if t.Spec.NamespaceQuota == nil {

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

View File

@@ -1,53 +1,22 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type AdditionalMetadata struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}
type IngressHostnamesSpec struct {
Allowed IngressHostnamesList `json:"allowed"`
AllowedRegex string `json:"allowedRegex"`
}
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPs struct {
Allowed []AllowedIP `json:"allowed"`
}
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
Owner OwnerSpec `json:"owner"`
//+kubebuilder:validation:Minimum=1
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata,omitempty"`
ServicesMetadata AdditionalMetadata `json:"servicesMetadata,omitempty"`
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
ServicesMetadata *AdditionalMetadataSpec `json:"servicesMetadata,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
@@ -56,27 +25,8 @@ type TenantSpec struct {
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges []corev1.LimitRangeSpec `json:"limitRanges,omitempty"`
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
AdditionalRoleBindings []AdditionalRoleBindings `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPs `json:"externalServiceIPs,omitempty"`
}
type AdditionalRoleBindings struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
}
// +kubebuilder:validation:Enum=User;Group
type Kind string
func (k Kind) String() string {
return string(k)
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
}
// TenantStatus defines the observed state of Tenant

View File

@@ -0,0 +1,21 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"io/ioutil"
ctrl "sigs.k8s.io/controller-runtime"
)
func (t *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
certData, _ := ioutil.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(t).
Complete()
}

View File

@@ -1,20 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
@@ -22,13 +9,13 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.AdditionalLabels != nil {
in, out := &in.AdditionalLabels, &out.AdditionalLabels
@@ -46,32 +33,32 @@ func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadata.
func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadata)
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindings) DeepCopyInto(out *AdditionalRoleBindings) {
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]rbacv1.Subject, len(*in))
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindings.
func (in *AdditionalRoleBindings) DeepCopy() *AdditionalRoleBindings {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindings)
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
@@ -175,7 +162,7 @@ func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPs) DeepCopyInto(out *ExternalServiceIPs) {
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
@@ -184,51 +171,12 @@ func (in *ExternalServiceIPs) DeepCopyInto(out *ExternalServiceIPs) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPs.
func (in *ExternalServiceIPs) DeepCopy() *ExternalServiceIPs {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPs)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in IngressHostnamesList) DeepCopyInto(out *IngressHostnamesList) {
{
in := &in
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesList.
func (in IngressHostnamesList) DeepCopy() IngressHostnamesList {
if in == nil {
return nil
}
out := new(IngressHostnamesList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressHostnamesSpec) DeepCopyInto(out *IngressHostnamesSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesSpec.
func (in *IngressHostnamesSpec) DeepCopy() *IngressHostnamesSpec {
if in == nil {
return nil
}
out := new(IngressHostnamesSpec)
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
@@ -316,8 +264,16 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(int32)
**out = **in
}
in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata)
in.ServicesMetadata.DeepCopyInto(&out.ServicesMetadata)
if in.NamespacesMetadata != nil {
in, out := &in.NamespacesMetadata, &out.NamespacesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServicesMetadata != nil {
in, out := &in.ServicesMetadata, &out.ServicesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(AllowedListSpec)
@@ -347,7 +303,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.NetworkPolicies != nil {
in, out := &in.NetworkPolicies, &out.NetworkPolicies
*out = make([]v1.NetworkPolicySpec, len(*in))
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -368,14 +324,14 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindings, len(*in))
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPs)
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type AdditionalMetadataSpec struct {
AdditionalLabels map[string]string `json:"additionalLabels,omitempty"`
AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import rbacv1 "k8s.io/api/rbac/v1"
type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
type AllowedIP string
type ExternalServiceIPsSpec struct {
Allowed []AllowedIP `json:"allowed"`
}

View File

@@ -0,0 +1,23 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1beta1 contains API Schema definitions for the capsule v1beta1 API group
//+kubebuilder:object:generate=true
//+groupName=capsule.clastix.io
package v1beta1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// 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 = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,11 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
type ImagePullPolicySpec string
func (i ImagePullPolicySpec) String() string {
return string(i)
}

View File

@@ -0,0 +1,10 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import corev1 "k8s.io/api/core/v1"
type LimitRangesSpec struct {
Items []corev1.LimitRangeSpec `json:"items,omitempty"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
networkingv1 "k8s.io/api/networking/v1"
)
type NetworkPolicySpec struct {
Items []networkingv1.NetworkPolicySpec `json:"items,omitempty"`
}

54
api/v1beta1/owner.go Normal file
View File

@@ -0,0 +1,54 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type OwnerSpec struct {
// Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
Kind OwnerKind `json:"kind"`
// Name of tenant owner.
Name string `json:"name"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
}
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
type OwnerKind string
func (k OwnerKind) String() string {
return string(k)
}
type ProxySettings struct {
Kind ProxyServiceKind `json:"kind"`
Operations []ProxyOperation `json:"operations"`
}
// +kubebuilder:validation:Enum=List;Update;Delete
type ProxyOperation string
func (p ProxyOperation) String() string {
return string(p)
}
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses;PriorityClasses
type ProxyServiceKind string
func (p ProxyServiceKind) String() string {
return string(p)
}
const (
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
PriorityClassesProxy ProxyServiceKind = "PriorityClasses"
ListOperation ProxyOperation = "List"
UpdateOperation ProxyOperation = "Update"
DeleteOperation ProxyOperation = "Delete"
UserOwner OwnerKind = "User"
GroupOwner OwnerKind = "Group"
ServiceAccountOwner OwnerKind = "ServiceAccount"
)

34
api/v1beta1/owner_list.go Normal file
View File

@@ -0,0 +1,34 @@
package v1beta1
import (
"sort"
)
type OwnerListSpec []OwnerSpec
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
})
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
}
return
}
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]
}

View File

@@ -0,0 +1,83 @@
package v1beta1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOwnerListSpec_FindOwner(t *testing.T) {
var bla = OwnerSpec{
Kind: UserOwner,
Name: "bla",
ProxyOperations: []ProxySettings{
{
Kind: IngressClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var bar = OwnerSpec{
Kind: GroupOwner,
Name: "bar",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var baz = OwnerSpec{
Kind: UserOwner,
Name: "baz",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Update"},
},
},
}
var fim = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "fim",
ProxyOperations: []ProxySettings{
{
Kind: NodesProxy,
Operations: []ProxyOperation{"List"},
},
},
}
var bom = OwnerSpec{
Kind: GroupOwner,
Name: "bom",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
{
Kind: NodesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var qip = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "qip",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"List", "Delete"},
},
},
}
var owners = OwnerListSpec{bom, qip, bla, bar, baz, fim}
assert.Equal(t, owners.FindOwner("bom", GroupOwner), bom)
assert.Equal(t, owners.FindOwner("qip", ServiceAccountOwner), qip)
assert.Equal(t, owners.FindOwner("bla", UserOwner), bla)
assert.Equal(t, owners.FindOwner("bar", GroupOwner), bar)
assert.Equal(t, owners.FindOwner("baz", UserOwner), baz)
assert.Equal(t, owners.FindOwner("fim", ServiceAccountOwner), fim)
assert.Equal(t, owners.FindOwner("notfound", ServiceAccountOwner), OwnerSpec{})
}

View File

@@ -0,0 +1,10 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import corev1 "k8s.io/api/core/v1"
type ResourceQuotaSpec struct {
Items []corev1.ResourceQuotaSpec `json:"items,omitempty"`
}

View File

@@ -0,0 +1,20 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type ServiceOptions struct {
// Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Block or deny certain type of Services. Optional.
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
}
type AllowedServices struct {
//+kubebuilder:default=true
// Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
NodePort *bool `json:"nodePort,omitempty"`
//+kubebuilder:default=true
// Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
ExternalName *bool `json:"externalName,omitempty"`
}

View File

@@ -0,0 +1,25 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"fmt"
)
const (
AvailableIngressClassesAnnotation = "capsule.clastix.io/ingress-classes"
AvailableIngressClassesRegexpAnnotation = "capsule.clastix.io/ingress-classes-regexp"
AvailableStorageClassesAnnotation = "capsule.clastix.io/storage-classes"
AvailableStorageClassesRegexpAnnotation = "capsule.clastix.io/storage-classes-regexp"
AllowedRegistriesAnnotation = "capsule.clastix.io/allowed-registries"
AllowedRegistriesRegexpAnnotation = "capsule.clastix.io/allowed-registries-regexp"
)
func UsedQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/used-" + resource.String()
}
func HardQuotaFor(resource fmt.Stringer) string {
return "quota.capsule.clastix.io/hard-" + resource.String()
}

View File

@@ -0,0 +1,42 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"sort"
corev1 "k8s.io/api/core/v1"
)
func (t *Tenant) IsCordoned() bool {
if v, ok := t.Labels["capsule.clastix.io/cordon"]; ok && v == "enabled" {
return true
}
return false
}
func (t *Tenant) IsFull() bool {
// we don't have limits on assigned Namespaces
if t.Spec.NamespaceQuota == nil {
return false
}
return len(t.Status.Namespaces) >= int(*t.Spec.NamespaceQuota)
}
func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
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
t.Status.Size = uint(len(l))
}
func (t *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return t.Spec.Owners.FindOwner(name, kind).ProxyOperations
}

View File

@@ -0,0 +1,31 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
"fmt"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func GetTypeLabel(t runtime.Object) (label string, err error) {
switch v := t.(type) {
case *Tenant:
return "capsule.clastix.io/tenant", nil
case *corev1.LimitRange:
return "capsule.clastix.io/limit-range", nil
case *networkingv1.NetworkPolicy:
return "capsule.clastix.io/network-policy", nil
case *corev1.ResourceQuota:
return "capsule.clastix.io/resource-quota", nil
case *rbacv1.RoleBinding:
return "capsule.clastix.io/role-binding", nil
default:
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}
return
}

View File

@@ -0,0 +1,23 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
// +kubebuilder:validation:Enum=cordoned;active
type tenantState string
const (
TenantStateActive tenantState = "active"
TenantStateCordoned tenantState = "cordoned"
)
// Returns the observed state of the Tenant
type TenantStatus struct {
//+kubebuilder:default=active
// The operational state of the Tenant. Possible values are "active", "cordoned".
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.
Size uint `json:"size"`
// List of namespaces assigned to the Tenant.
Namespaces []string `json:"namespaces,omitempty"`
}

View File

@@ -0,0 +1,80 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
// Specifies the owners of the Tenant. Mandatory.
Owners OwnerListSpec `json:"owners"`
//+kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *ServiceOptions `json:"serviceOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
// Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies *NetworkPolicySpec `json:"networkPolicies,omitempty"`
// 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"`
// 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"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceQuota",description="The max amount of Namespaces can be created"
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API
type Tenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TenantSpec `json:"spec,omitempty"`
Status TenantStatus `json:"status,omitempty"`
}
func (t *Tenant) Hub() {}
//+kubebuilder:object:root=true
// TenantList contains a list of Tenant
type TenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Tenant `json:"items"`
}
func init() {
SchemeBuilder.Register(&Tenant{}, &TenantList{})
}

View File

@@ -0,0 +1,484 @@
// +build !ignore_autogenerated
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package v1beta1
import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) {
*out = *in
if in.AdditionalLabels != nil {
in, out := &in.AdditionalLabels, &out.AdditionalLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.AdditionalAnnotations != nil {
in, out := &in.AdditionalAnnotations, &out.AdditionalAnnotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSpec.
func (in *AdditionalMetadataSpec) DeepCopy() *AdditionalMetadataSpec {
if in == nil {
return nil
}
out := new(AdditionalMetadataSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSpec) {
*out = *in
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
if in == nil {
return nil
}
out := new(AdditionalRoleBindingsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*out = *in
if in.Exact != nil {
in, out := &in.Exact, &out.Exact
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedListSpec.
func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
if in == nil {
return nil
}
out := new(AllowedListSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedServices) DeepCopyInto(out *AllowedServices) {
*out = *in
if in.NodePort != nil {
in, out := &in.NodePort, &out.NodePort
*out = new(bool)
**out = **in
}
if in.ExternalName != nil {
in, out := &in.ExternalName, &out.ExternalName
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedServices.
func (in *AllowedServices) DeepCopy() *AllowedServices {
if in == nil {
return nil
}
out := new(AllowedServices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ByKindAndName) DeepCopyInto(out *ByKindAndName) {
{
in := &in
*out = make(ByKindAndName, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ByKindAndName.
func (in ByKindAndName) DeepCopy() ByKindAndName {
if in == nil {
return nil
}
out := new(ByKindAndName)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]AllowedIP, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalServiceIPsSpec.
func (in *ExternalServiceIPsSpec) DeepCopy() *ExternalServiceIPsSpec {
if in == nil {
return nil
}
out := new(ExternalServiceIPsSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LimitRangesSpec) DeepCopyInto(out *LimitRangesSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.LimitRangeSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitRangesSpec.
func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
if in == nil {
return nil
}
out := new(LimitRangesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]networkingv1.NetworkPolicySpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicySpec.
func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec {
if in == nil {
return nil
}
out := new(NetworkPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in OwnerListSpec) DeepCopyInto(out *OwnerListSpec) {
{
in := &in
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerListSpec.
func (in OwnerListSpec) DeepCopy() OwnerListSpec {
if in == nil {
return nil
}
out := new(OwnerListSpec)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
if in.ProxyOperations != nil {
in, out := &in.ProxyOperations, &out.ProxyOperations
*out = make([]ProxySettings, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec.
func (in *OwnerSpec) DeepCopy() *OwnerSpec {
if in == nil {
return nil
}
out := new(OwnerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxySettings) DeepCopyInto(out *ProxySettings) {
*out = *in
if in.Operations != nil {
in, out := &in.Operations, &out.Operations
*out = make([]ProxyOperation, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxySettings.
func (in *ProxySettings) DeepCopy() *ProxySettings {
if in == nil {
return nil
}
out := new(ProxySettings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceQuotaSpec) DeepCopyInto(out *ResourceQuotaSpec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]corev1.ResourceQuotaSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaSpec.
func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec {
if in == nil {
return nil
}
out := new(ResourceQuotaSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
*out = *in
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedServices != nil {
in, out := &in.AllowedServices, &out.AllowedServices
*out = new(AllowedServices)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions.
func (in *ServiceOptions) DeepCopy() *ServiceOptions {
if in == nil {
return nil
}
out := new(ServiceOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Tenant) DeepCopyInto(out *Tenant) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tenant.
func (in *Tenant) DeepCopy() *Tenant {
if in == nil {
return nil
}
out := new(Tenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Tenant) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantList) DeepCopyInto(out *TenantList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Tenant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantList.
func (in *TenantList) DeepCopy() *TenantList {
if in == nil {
return nil
}
out := new(TenantList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.NamespaceQuota != nil {
in, out := &in.NamespaceQuota, &out.NamespaceQuota
*out = new(int32)
**out = **in
}
if in.NamespacesMetadata != nil {
in, out := &in.NamespacesMetadata, &out.NamespacesMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServiceOptions != nil {
in, out := &in.ServiceOptions, &out.ServiceOptions
*out = new(ServiceOptions)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressClasses != nil {
in, out := &in.IngressClasses, &out.IngressClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressHostnames != nil {
in, out := &in.IngressHostnames, &out.IngressHostnames
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*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)
}
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
*out = make([]AdditionalRoleBindingsSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExternalServiceIPs != nil {
in, out := &in.ExternalServiceIPs, &out.ExternalServiceIPs
*out = new(ExternalServiceIPsSpec)
(*in).DeepCopyInto(*out)
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
if in.PriorityClasses != nil {
in, out := &in.PriorityClasses, &out.PriorityClasses
*out = new(AllowedListSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.
func (in *TenantSpec) DeepCopy() *TenantSpec {
if in == nil {
return nil
}
out := new(TenantSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = *in
if in.Namespaces != nil {
in, out := &in.Namespaces, &out.Namespaces
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.
func (in *TenantStatus) DeepCopy() *TenantStatus {
if in == nil {
return nil
}
out := new(TenantStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -60,6 +60,7 @@ 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`
`manager.options.forceTenantPrefix` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash | `false`
`manager.options.capsuleUserGroup` | Override the Capsule user group | `capsule.clastix.io`
@@ -88,6 +89,9 @@ Parameter | Description | Default
`replicaCount` | Set the replica count for Capsule pod. | `1`
`affinity` | Set affinity rules for the Capsule pod. | `{}`
`podSecurityPolicy.enabled` | Specify if a Pod Security Policy must be created. | `false`
`serviceMonitor.enabled` | Specify if a Service Monitor must be created. | `false`
`serviceMonitor.serviceAccount.name` | Specify Service Account name for metrics scrape. | `capsule`
`serviceMonitor.serviceAccount.namespace` | Specify Service Account namespace for metrics scrape. | `capsule-system`
## Created resources
@@ -107,8 +111,10 @@ This Helm Chart cretes the following Kubernetes resources in the release namespa
And optionally, depending on the values set:
* Capsule ServiceAccount
* Capsule Service Monitor
* PodSecurityPolicy
* RBAC ClusterRole and RoleBinding for pod security policy
* RBAC Role and Rolebinding for metrics scrape
## Notes on installing Custom Resource Definitions with Helm3

File diff suppressed because it is too large Load Diff

View File

@@ -9,5 +9,5 @@ spec:
- {{ . }}
{{- end}}
protectedNamespaceRegex: {{ .Values.manager.options.protectedNamespaceRegex | quote }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}
allowTenantIngressHostnamesCollision: {{ .Values.manager.options.allowTenantIngressHostnamesCollision }}
allowIngressHostnameCollision: {{ .Values.manager.options.allowIngressHostnameCollision }}

View File

@@ -23,6 +23,9 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
{{- if .Values.manager.hostNetwork }}
hostNetwork: true
{{- end }}
priorityClassName: {{ .Values.priorityClassName }}
{{- with .Values.nodeSelector }}
nodeSelector:

View File

@@ -0,0 +1,42 @@
{{- if .Values.serviceMonitor.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.labels }}
{{- toYaml .Values.serviceMonitor.labels | nindent 4 }}
{{- end }}
name: {{ include "capsule.fullname" . }}-metrics-role
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- pods
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.labels }}
{{- toYaml .Values.serviceMonitor.labels | nindent 4 }}
{{- end }}
name: {{ include "capsule.fullname" . }}-metrics-rolebinding
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "capsule.fullname" . }}-metrics-role
subjects:
- kind: ServiceAccount
name: {{ .Values.serviceMonitor.serviceAccount.name }}
namespace: {{ .Values.serviceMonitor.serviceAccount.namespace | default .Release.Namespace }}
{{- end }}

View File

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

View File

@@ -0,0 +1,23 @@
{{- if .Values.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "capsule.fullname" . }}-monitor
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.labels }}
{{- toYaml .Values.serviceMonitor.labels | nindent 4 }}
{{- end }}
spec:
endpoints:
- interval: 15s
port: metrics
path: /metrics
jobLabel: app.kubernetes.io/name
selector:
matchLabels: {{ include "capsule.labels" . | nindent 6 }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}
{{- end }}

View File

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

View File

@@ -7,6 +7,14 @@ manager:
repository: quay.io/clastix/capsule
pullPolicy: IfNotPresent
tag: ''
# Specifies if the container should be started in hostNetwork mode.
#
# Required for use in some managed kubernetes clusters (such as AWS EKS) with custom
# CNI (such as calico), because control-plane managed by AWS cannot communicate
# with pods' IP CIDR and admission webhooks are not working
hostNetwork: false
# Additional Capsule options
options:
logLevel: '4'
@@ -33,9 +41,9 @@ manager:
memory: 128Mi
jobs:
image:
repository: bitnami/kubectl
repository: quay.io/clastix/kubectl
pullPolicy: IfNotPresent
tag: "1.18"
tag: "v1.20.7"
mutatingWebhooksTimeoutSeconds: 30
validatingWebhooksTimeoutSeconds: 30
imagePullSecrets: []
@@ -56,3 +64,15 @@ replicaCount: 1
affinity: {}
podSecurityPolicy:
enabled: false
serviceMonitor:
enabled: false
# Install the ServiceMonitor into a different Namespace, as the monitoring stack one (default: the release one)
namespace:
# Assign additional labels according to Prometheus' serviceMonitorSelector matching labels
labels: {}
annotations: {}
matchLabels: {}
serviceAccount:
name: capsule
namespace: capsule-system

View File

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

View File

@@ -9,3 +9,6 @@ resources:
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml
patchesStrategicMerge:
- patches/webhook_in_tenants.yaml

View File

@@ -4,13 +4,15 @@ nameReference:
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/name
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/namespace
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:

View File

@@ -0,0 +1,17 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tenants.capsule.clastix.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1alpha1
- v1beta1

View File

@@ -0,0 +1,921 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "8.0.1"
},
{
"type": "panel",
"id": "heatmap",
"name": "Heatmap",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "stat",
"name": "Stat",
"version": ""
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"iteration": 1624217784640,
"links": [],
"panels": [
{
"cards": {
"cardPadding": null,
"cardRound": null
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"datasource": "${DS_PROMETHEUS}",
"description": "",
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 0
},
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"id": 21,
"legend": {
"show": false
},
"maxPerRow": 2,
"pluginVersion": "8.0.1",
"repeat": "controller_name",
"repeatDirection": "h",
"reverseYBuckets": false,
"targets": [
{
"exemplar": false,
"expr": "sum by (le) (increase(workqueue_work_duration_seconds_bucket{service=\"capsule-controller-manager-metrics-service\",name=\"$controller_name\"}[1m]))",
"hide": false,
"interval": "",
"legendFormat": "",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "$controller_name reconcile latency",
"tooltip": {
"show": true,
"showHistogram": false
},
"type": "heatmap",
"xAxis": {
"show": true
},
"xBucketNumber": null,
"xBucketSize": null,
"yAxis": {
"decimals": null,
"format": "ms",
"logBase": 1,
"max": null,
"min": null,
"show": true,
"splitFactor": null
},
"yBucketBound": "auto",
"yBucketNumber": null,
"yBucketSize": null
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 16
},
"id": 17,
"panels": [],
"title": "Requests",
"type": "row"
},
{
"datasource": "${DS_PROMETHEUS}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 12,
"x": 0,
"y": 17
},
"id": 22,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": false,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"200\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "200",
"queryType": "randomWalk",
"refId": "A"
},
{
"exemplar": false,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"201\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "201",
"queryType": "randomWalk",
"refId": "E"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"403\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "403",
"queryType": "randomWalk",
"refId": "B"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"409\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "409",
"queryType": "randomWalk",
"refId": "C"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"500\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "500",
"queryType": "randomWalk",
"refId": "D"
}
],
"title": "Rate & Errors",
"type": "timeseries"
},
{
"datasource": "${DS_PROMETHEUS}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 12,
"x": 12,
"y": 17
},
"id": 19,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": false,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"200\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "200",
"queryType": "randomWalk",
"refId": "A"
},
{
"exemplar": false,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"201\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "201",
"queryType": "randomWalk",
"refId": "B"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"403\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "403",
"queryType": "randomWalk",
"refId": "C"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"409\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "409",
"queryType": "randomWalk",
"refId": "D"
},
{
"exemplar": true,
"expr": "sum(rate(rest_client_requests_total{service=\"capsule-controller-manager-metrics-service\",code=\"500\"}[30s]))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "500",
"queryType": "randomWalk",
"refId": "E"
}
],
"title": "Rate & Errors",
"type": "stat"
},
{
"cards": {
"cardPadding": null,
"cardRound": null
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"datasource": "${DS_PROMETHEUS}",
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 28
},
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"id": 20,
"legend": {
"show": false
},
"pluginVersion": "8.0.1",
"reverseYBuckets": false,
"targets": [
{
"exemplar": false,
"expr": "sum by (le) (increase(rest_client_request_latency_seconds_bucket{service=\"capsule-controller-manager-metrics-service\"}[1m]))",
"hide": false,
"interval": "",
"legendFormat": "{{le}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Client Operator Latency",
"tooltip": {
"show": true,
"showHistogram": false
},
"type": "heatmap",
"xAxis": {
"show": true
},
"xBucketNumber": null,
"xBucketSize": null,
"yAxis": {
"decimals": null,
"format": "ms",
"logBase": 1,
"max": null,
"min": null,
"show": true,
"splitFactor": null
},
"yBucketBound": "auto",
"yBucketNumber": null,
"yBucketSize": null
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 35
},
"id": 12,
"panels": [],
"title": "Saturation",
"type": "row"
},
{
"datasource": "${DS_PROMETHEUS}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 9,
"x": 0,
"y": 36
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": true,
"expr": "rate(process_cpu_seconds_total{service=\"capsule-controller-manager-metrics-service\"}[1m])",
"interval": "",
"legendFormat": "{{pod}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "CPU Usage",
"type": "timeseries"
},
{
"datasource": "${DS_PROMETHEUS}",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 1,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "decbytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 9,
"x": 9,
"y": 36
},
"id": 24,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"exemplar": false,
"expr": "process_resident_memory_bytes{service=\"capsule-controller-manager-metrics-service\"}",
"interval": "",
"legendFormat": "{{pod}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Memory Usage",
"type": "timeseries"
},
{
"datasource": "${DS_PROMETHEUS}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 3,
"x": 18,
"y": 36
},
"id": 5,
"options": {
"colorMode": "background",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {
"valueSize": 80
},
"textMode": "value_and_name"
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": true,
"expr": "sum(up{service=\"capsule-controller-manager-metrics-service\"})",
"interval": "",
"legendFormat": "Active Controllers",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Stats",
"transparent": true,
"type": "stat"
},
{
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 44
},
"id": 10,
"title": "Workqueue",
"type": "row"
},
{
"datasource": "${DS_PROMETHEUS}",
"description": "service time to complete needed actions",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 45
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": true,
"expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{service=\"capsule-controller-manager-metrics-service\"}[1m])) by (instance, name, le))",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Workqueue latency",
"type": "stat"
},
{
"datasource": "${DS_PROMETHEUS}",
"description": "number of actions per unit time",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 45
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": true,
"expr": "sum(rate(workqueue_adds_total{service=\"capsule-controller-manager-metrics-service\"}[1m])) by (instance, name)",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Workqueue rate",
"type": "stat"
},
{
"datasource": "${DS_PROMETHEUS}",
"description": "number of actions waiting in the queue to be performed",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 16,
"y": 45
},
"id": 13,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.0.1",
"targets": [
{
"exemplar": false,
"expr": "sum(rate(workqueue_depth{service=\"capsule-controller-manager-metrics-service\"}[1m])) by (instance, name)",
"format": "time_series",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Workqueue depth",
"type": "stat"
}
],
"refresh": "1m",
"schemaVersion": 30,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "${DS_PROMETHEUS}",
"definition": "label_values(workqueue_work_duration_seconds_bucket{service=\"capsule-controller-manager-metrics-service\"},name)",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "controller_name",
"options": [],
"query": {
"query": "label_values(workqueue_work_duration_seconds_bucket{service=\"capsule-controller-manager-metrics-service\"},name)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "capsule",
"uid": "nEFvaoR7z",
"version": 28
}

1642
config/install.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,2 +1,4 @@
resources:
- monitor.yaml
- role.yaml
- rolebinding.yaml

View File

@@ -1,16 +1,18 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-monitor
name: capsule-monitor
namespace: system
spec:
endpoints:
- path: /metrics
- interval: 15s
path: /metrics
port: metrics
jobLabel: controller-manager
namespaceSelector:
selector:
matchLabels:
control-plane: controller-manager

View File

@@ -0,0 +1,18 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
control-plane: controller-manager
name: capsule-metrics-role
namespace: capsule-system
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- pods
verbs:
- get
- list
- watch

View File

@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
control-plane: controller-manager
name: capsule-metrics-rolebinding
namespace: system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: capsule-metrics-role
subjects:
- kind: ServiceAccount
name: capsule
namespace: capsule-system

View File

@@ -0,0 +1,136 @@
---
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: gas
spec:
additionalRoleBindings:
-
clusterRoleName: tenant-sample-viewer
subjects:
-
kind: User
name: bob
containerRegistries:
allowed:
- docker.io
- quay.io
allowedRegex: ^\w+.gcr.io$
serviceOptions:
additionalMetadata:
additionalAnnotations:
capsule.clastix.io/bgp: "true"
additionalLabels:
capsule.clastix.io/pool: gas
allowedServices:
nodePort: false
externalName: false
externalServiceIPs:
allowed:
- 10.20.0.0/16
- "10.96.42.42"
imagePullPolicies:
- Always
ingressClasses:
allowed:
- default
allowedRegex: ^\w+-lb$
ingressHostnames:
allowed:
- gas.acmecorp.com
allowedRegex: ^.*acmecorp.com$
limitRanges:
items:
-
limits:
-
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Pod
-
default:
cpu: 200m
memory: 100Mi
defaultRequest:
cpu: 100m
memory: 10Mi
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Container
-
max:
storage: 10Gi
min:
storage: 1Gi
type: PersistentVolumeClaim
namespaceQuota: 3
namespacesMetadata:
additionalAnnotations:
capsule.clastix.io/backup: "false"
additionalLabels:
capsule.clastix.io/tenant: gas
networkPolicies:
items:
-
egress:
-
to:
-
ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.0.0/12
ingress:
-
from:
-
namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: gas
-
podSelector: {}
-
ipBlock:
cidr: 192.168.0.0/12
podSelector: {}
policyTypes:
- Ingress
- Egress
nodeSelector:
kubernetes.io/os: linux
owners:
-
kind: User
name: bob
priorityClasses:
allowed:
- shared-nodes
allowedRegex: ^\w-gas$
resourceQuotas:
items:
-
hard:
limits.cpu: "8"
limits.memory: 16Gi
requests.cpu: "8"
requests.memory: 16Gi
scopes:
- NotTerminating
-
hard:
pods: "10"
-
hard:
requests.storage: 100Gi
storageClasses:
allowed:
- default
allowedRegex: ^\w+fs$

View File

@@ -2,3 +2,4 @@
resources:
- capsule_v1alpha1_capsuleconfiguration.yaml
- capsule_v1alpha1_tenant.yaml
- capsule_v1beta1_tenant.yaml

View File

@@ -12,7 +12,7 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /mutate-v1-namespace-owner-reference
path: /namespace-owner-reference
failurePolicy: Fail
name: owner.namespace.capsule.clastix.io
rules:
@@ -39,20 +39,20 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-ingress
path: /cordoning
failurePolicy: Fail
name: ingress-v1beta1.capsule.clastix.io
name: cordoning.tenant.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
- extensions
- '*'
apiVersions:
- v1beta1
- '*'
operations:
- CREATE
- UPDATE
- DELETE
resources:
- ingresses
- '*'
sideEffects: None
- admissionReviewVersions:
- v1
@@ -60,13 +60,15 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-ingress
path: /ingresses
failurePolicy: Fail
name: ingress-v1.capsule.clastix.io
name: ingress.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
- extensions
apiVersions:
- v1beta1
- v1
operations:
- CREATE
@@ -80,9 +82,9 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validate-v1-namespace-quota
path: /namespaces
failurePolicy: Fail
name: quota.namespace.capsule.clastix.io
name: namespaces.capsule.clastix.io
rules:
- apiGroups:
- ""
@@ -90,6 +92,8 @@ webhooks:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- namespaces
sideEffects: None
@@ -99,16 +103,15 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-network-policy
path: /networkpolicies
failurePolicy: Fail
name: validating.network-policy.capsule.clastix.io
name: networkpolicies.capsule.clastix.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
- DELETE
resources:
@@ -120,9 +123,9 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-podpriority
failurePolicy: Ignore
name: podpriority.capsule.clastix.io
path: /pods
failurePolicy: Fail
name: pods.capsule.clastix.io
rules:
- apiGroups:
- ""
@@ -139,7 +142,7 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-pvc
path: /persistentvolumeclaims
failurePolicy: Fail
name: pvc.capsule.clastix.io
rules:
@@ -158,28 +161,9 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-registry
failurePolicy: Ignore
name: pod.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-external-service-ips
path: /services
failurePolicy: Fail
name: validating-external-service-ips.capsule.clastix.io
name: services.capsule.clastix.io
rules:
- apiGroups:
- ""
@@ -197,36 +181,18 @@ webhooks:
service:
name: webhook-service
namespace: system
path: /validating-v1-tenant
path: /tenants
failurePolicy: Fail
name: tenant.capsule.clastix.io
name: tenants.capsule.clastix.io
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1alpha1
- v1beta1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- tenants
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validating-v1-namespace-tenant-prefix
failurePolicy: Fail
name: prefix.namespace.capsule.clastix.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- namespaces
sideEffects: None

View File

@@ -35,8 +35,20 @@
- key: capsule.clastix.io/tenant
operator: Exists
- op: add
path: /webhooks/7/namespaceSelector
value:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
path: /webhooks/0/rules/0/scope
value: Namespaced
- op: add
path: /webhooks/1/rules/0/scope
value: Namespaced
- 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

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package rbac
@@ -28,7 +15,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
@@ -67,7 +54,7 @@ func forOptionPerInstanceName(instanceName string) builder.ForOption {
func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
For(&capsulev1alpha1.CapsuleConfiguration{}, forOptionPerInstanceName(configurationName)).
Complete(r)
}

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package rbac

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package rbac
@@ -36,7 +23,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/configuration"
)
@@ -92,7 +79,7 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) (
return r.filterByNames(genericEvent.Object.GetName())
},
})).
Watches(source.NewKindWithCache(&v1alpha1.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{
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 {

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret
@@ -24,11 +11,13 @@ import (
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
v1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -50,10 +39,45 @@ func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
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 {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
crd := &apiextensionsv1.CustomResourceDefinition{}
err = r.Get(context.TODO(), 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 {
crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
Strategy: "Webhook",
Webhook: &apiextensionsv1.WebhookConversion{
ClientConfig: &apiextensionsv1.WebhookClientConfig{
Service: &apiextensionsv1.ServiceReference{
Namespace: r.Namespace,
Name: "capsule-webhook-service",
Path: pointer.StringPtr("/convert"),
Port: pointer.Int32Ptr(443),
},
CABundle: caBundle,
},
ConversionReviewVersions: []string{"v1alpha1", "v1beta1"},
},
}
return nil
})
return err
})
}
//nolint:dupl
func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
vw := &v1.ValidatingWebhookConfiguration{}
vw := &admissionregistrationv1.ValidatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-validating-webhook-configuration"}, vw)
if err != nil {
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
@@ -72,7 +96,7 @@ func (r CAReconciler) UpdateValidatingWebhookConfiguration(caBundle []byte) erro
//nolint:dupl
func (r CAReconciler) UpdateMutatingWebhookConfiguration(caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
mw := &v1.MutatingWebhookConfiguration{}
mw := &admissionregistrationv1.MutatingWebhookConfiguration{}
err = r.Get(context.TODO(), types.NamespacedName{Name: "capsule-mutating-webhook-configuration"}, mw)
if err != nil {
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
@@ -140,6 +164,9 @@ func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl
group.Go(func() error {
return r.UpdateValidatingWebhookConfiguration(crt.Bytes())
})
group.Go(func() error {
return r.UpdateCustomResourceDefinition(crt.Bytes())
})
if err = group.Wait(); err != nil {
return reconcile.Result{}, err

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package servicelabels
@@ -33,7 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
type abstractServiceLabelsReconciler struct {
@@ -66,23 +53,23 @@ func (r *abstractServiceLabelsReconciler) Reconcile(ctx context.Context, request
}
_, err = controllerutil.CreateOrUpdate(ctx, r.client, r.obj, func() (err error) {
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServicesMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServicesMetadata.AdditionalAnnotations))
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations))
return nil
})
return reconcile.Result{}, err
}
func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*v1alpha1.Tenant, error) {
func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*capsulev1beta1.Tenant, error) {
ns := &corev1.Namespace{}
tenant := &v1alpha1.Tenant{}
tenant := &capsulev1beta1.Tenant{}
if err := client.Get(ctx, types.NamespacedName{Name: namespacedName.Namespace}, ns); err != nil {
return nil, err
}
capsuleLabel, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if _, ok := ns.GetLabels()[capsuleLabel]; !ok {
return nil, NewNonTenantObject(namespacedName.Name)
}
@@ -91,7 +78,7 @@ func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespa
return nil, err
}
if tenant.Spec.ServicesMetadata.AdditionalLabels == nil && tenant.Spec.ServicesMetadata.AdditionalAnnotations == nil {
if tenant.Spec.ServiceOptions == nil || tenant.Spec.ServiceOptions.AdditionalMetadata == nil {
return nil, NewNoServicesMetadata(namespacedName.Name)
}
@@ -131,7 +118,7 @@ func (r *abstractServiceLabelsReconciler) forOptionPerInstanceName() builder.For
}
func (r *abstractServiceLabelsReconciler) IsNamespaceInTenant(namespace string) bool {
tl := &v1alpha1.TenantList{}
tl := &capsulev1beta1.TenantList{}
if err := r.client.List(context.Background(), tl, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
}); err != nil {

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package servicelabels

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package servicelabels

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package servicelabels

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package servicelabels

View File

@@ -1,18 +1,5 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
@@ -36,26 +23,28 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
"github.com/clastix/capsule/controllers/rbac"
)
// TenantReconciler reconciles a Tenant object
type TenantReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *TenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1alpha1.Tenant{}).
For(&capsulev1beta1.Tenant{}).
Owns(&corev1.Namespace{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
@@ -68,7 +57,7 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1alpha1.Tenant{}
instance := &capsulev1beta1.Tenant{}
err = r.Get(ctx, request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
@@ -78,6 +67,11 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
r.Log.Error(err, "Error reading the object")
return
}
// Ensuring the Tenant Status
if err = r.updateTenantStatus(instance); err != nil {
r.Log.Error(err, "Cannot update Tenant status")
return
}
// Ensuring all namespaces are collected
r.Log.Info("Ensuring all Namespaces are collected")
@@ -92,27 +86,33 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
return
}
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
if err = r.syncNetworkPolicies(instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
return
if instance.Spec.NetworkPolicies != nil {
r.Log.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies.Items))
if err = r.syncNetworkPolicies(instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
return
}
}
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges))
if err = r.syncLimitRanges(instance); err != nil {
r.Log.Error(err, "Cannot sync LimitRange items")
return
if instance.Spec.LimitRanges != nil {
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
if err = r.syncLimitRanges(instance); err != nil {
r.Log.Error(err, "Cannot sync LimitRange items")
return
}
}
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota))
if err = r.syncResourceQuotas(instance); err != nil {
r.Log.Error(err, "Cannot sync ResourceQuota 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 PSP for owner")
r.Log.Info("Ensuring additional RoleBindings for owner")
if err = r.syncAdditionalRoleBindings(instance); err != nil {
r.Log.Error(err, "Cannot sync additional Role Bindings items")
r.Log.Error(err, "Cannot sync additional RoleBindings items")
return
}
@@ -135,7 +135,7 @@ func (r TenantReconciler) Reconcile(ctx context.Context, request ctrl.Request) (
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
func (r *TenantReconciler) pruningResources(ns string, keys []string, obj client.Object) error {
capsuleLabel, err := capsulev1alpha1.GetTypeLabel(obj)
capsuleLabel, err := capsulev1beta1.GetTypeLabel(obj)
if err != nil {
return err
}
@@ -191,8 +191,8 @@ func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName
found.Annotations = make(map[string]string)
}
found.Labels = rq.Labels
found.Annotations[capsulev1alpha1.UsedQuotaFor(resourceName)] = actual.String()
found.Annotations[capsulev1alpha1.HardQuotaFor(resourceName)] = limit.String()
found.Annotations[capsulev1beta1.UsedQuotaFor(resourceName)] = actual.String()
found.Annotations[capsulev1beta1.HardQuotaFor(resourceName)] = limit.String()
// Updating the Resource according to the actual.Cmp result
found.Spec.Hard = rq.Spec.Hard
return nil
@@ -215,25 +215,31 @@ func (r *TenantReconciler) resourceQuotasUpdate(resourceName corev1.ResourceName
// Additional Role Bindings can be used in many ways: applying Pod Security Policies or giving
// access to CRDs or specific API groups.
func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1beta1.Tenant) (err error) {
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hash := func(value string) string {
hash := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(value))
_, _ = h.Write([]byte(binding.ClusterRoleName))
for _, sub := range binding.Subjects {
_, _ = h.Write([]byte(sub.Kind + sub.Name))
}
return fmt.Sprintf("%x", h.Sum64())
}
// getting requested Role Binding keys
var keys []string
for _, i := range tenant.Spec.AdditionalRoleBindings {
keys = append(keys, hash(i.ClusterRoleName))
keys = append(keys, hash(i))
}
var tl, ll string
tl, err = capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return
}
ll, err = capsulev1alpha1.GetTypeLabel(&rbacv1.RoleBinding{})
ll, err = capsulev1beta1.GetTypeLabel(&rbacv1.RoleBinding{})
if err != nil {
return
}
@@ -242,11 +248,11 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
if err = r.pruningResources(ns, keys, &rbacv1.RoleBinding{}); err != nil {
return err
}
for _, i := range tenant.Spec.AdditionalRoleBindings {
lv := hash(i.ClusterRoleName)
for i, roleBinding := range tenant.Spec.AdditionalRoleBindings {
lv := hash(roleBinding)
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%s", tenant.Name, i.ClusterRoleName),
Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, roleBinding.ClusterRoleName),
Namespace: ns,
},
}
@@ -259,11 +265,15 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
rb.RoleRef = rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: i.ClusterRoleName,
Name: roleBinding.ClusterRoleName,
}
rb.Subjects = i.Subjects
rb.Subjects = roleBinding.Subjects
return controllerutil.SetControllerReference(tenant, rb, r.Scheme)
})
r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", rb.GetName()), err)
if err != nil {
r.Log.Error(err, "Cannot sync Additional RoleBinding")
}
@@ -285,19 +295,19 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
// .Status.Used value as the .Hard value.
// This will trigger a following reconciliation but that's ok: the mutateFn will re-use the same business logic, letting
// the mutateFn along with the CreateOrUpdate to don't perform the update since resources are identical.
func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1beta1.Tenant) error {
// getting requested ResourceQuota keys
keys := make([]string, 0, len(tenant.Spec.ResourceQuota))
for i := range tenant.Spec.ResourceQuota {
keys := make([]string, 0, len(tenant.Spec.ResourceQuota.Items))
for i := range tenant.Spec.ResourceQuota.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting ResourceQuota labels for the mutateFn
tenantLabel, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tenantLabel, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
typeLabel, err := capsulev1alpha1.GetTypeLabel(&corev1.ResourceQuota{})
typeLabel, err := capsulev1beta1.GetTypeLabel(&corev1.ResourceQuota{})
if err != nil {
return err
}
@@ -306,19 +316,18 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
if err := r.pruningResources(ns, keys, &corev1.ResourceQuota{}); err != nil {
return err
}
for i, q := range tenant.Spec.ResourceQuota {
for i, q := range tenant.Spec.ResourceQuota.Items {
target := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
Annotations: make(map[string]string),
Labels: map[string]string{
tenantLabel: tenant.Name,
typeLabel: strconv.Itoa(i),
},
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.SetLabels(map[string]string{
tenantLabel: tenant.Name,
typeLabel: strconv.Itoa(i),
})
// Requirement to list ResourceQuota of the current Tenant
tr, err := labels.NewRequirement(tenantLabel, selection.Equals, []string{tenant.Name})
if err != nil {
@@ -395,6 +404,9 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
}
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err)
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
@@ -406,19 +418,19 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
}
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1beta1.Tenant) error {
// getting requested LimitRange keys
keys := make([]string, 0, len(tenant.Spec.LimitRanges))
for i := range tenant.Spec.LimitRanges {
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items))
for i := range tenant.Spec.LimitRanges.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting LimitRange labels for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
ll, err := capsulev1alpha1.GetTypeLabel(&corev1.LimitRange{})
ll, err := capsulev1beta1.GetTypeLabel(&corev1.LimitRange{})
if err != nil {
return err
}
@@ -427,7 +439,7 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
if err := r.pruningResources(ns, keys, &corev1.LimitRange{}); err != nil {
return err
}
for i, spec := range tenant.Spec.LimitRanges {
for i, spec := range tenant.Spec.LimitRanges.Items {
t := &corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
@@ -442,6 +454,9 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", t.GetName()), err)
r.Log.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
@@ -452,65 +467,87 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
return nil
}
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1alpha1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev1beta1.Tenant) (err error) {
var res controllerutil.OperationResult
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
ns := &corev1.Namespace{}
if err = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
if conflictErr = r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil {
return
}
a := tnt.Spec.NamespacesMetadata.AdditionalAnnotations
if a == nil {
a = make(map[string]string)
}
if tnt.Spec.NodeSelector != nil {
var selector []string
for k, v := range tnt.Spec.NodeSelector {
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
}
a["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
}
if tnt.Spec.IngressClasses != nil {
if len(tnt.Spec.IngressClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
}
if len(tnt.Spec.IngressClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
a[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
a[capsulev1alpha1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
a[capsulev1alpha1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
a[capsulev1alpha1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
ns.SetAnnotations(a)
res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
a := make(map[string]string)
l := tnt.Spec.NamespacesMetadata.AdditionalLabels
if l == nil {
l = make(map[string]string)
}
l["name"] = namespace
capsuleLabel, _ := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
l[capsuleLabel] = tnt.GetName()
ns.SetLabels(l)
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations {
a[k] = v
}
}
return r.Client.Update(context.TODO(), ns, &client.UpdateOptions{})
if tnt.Spec.NodeSelector != nil {
var selector []string
for k, v := range tnt.Spec.NodeSelector {
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
}
a["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
}
if tnt.Spec.IngressClasses != nil {
if len(tnt.Spec.IngressClasses.Exact) > 0 {
a[capsulev1beta1.AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressClasses.Exact, ",")
}
if len(tnt.Spec.IngressClasses.Regex) > 0 {
a[capsulev1beta1.AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressClasses.Regex
}
}
if tnt.Spec.StorageClasses != nil {
if len(tnt.Spec.StorageClasses.Exact) > 0 {
a[capsulev1beta1.AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",")
}
if len(tnt.Spec.StorageClasses.Regex) > 0 {
a[capsulev1beta1.AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex
}
}
if tnt.Spec.ContainerRegistries != nil {
if len(tnt.Spec.ContainerRegistries.Exact) > 0 {
a[capsulev1beta1.AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",")
}
if len(tnt.Spec.ContainerRegistries.Regex) > 0 {
a[capsulev1beta1.AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex
}
}
ns.SetAnnotations(a)
l := make(map[string]string)
if tnt.Spec.NamespacesMetadata != nil {
for k, v := range tnt.Spec.NamespacesMetadata.AdditionalLabels {
l[k] = v
}
}
l["name"] = namespace
capsuleLabel, _ := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
l[capsuleLabel] = tnt.GetName()
ns.SetLabels(l)
return nil
})
return
})
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
return
}
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err error) {
func (r *TenantReconciler) syncNamespaces(tenant *capsulev1beta1.Tenant) (err error) {
group := errgroup.Group{}
for _, item := range tenant.Status.Namespaces {
@@ -528,19 +565,19 @@ func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err e
}
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1beta1.Tenant) error {
// getting requested NetworkPolicy keys
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies))
for i := range tenant.Spec.NetworkPolicies {
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items))
for i := range tenant.Spec.NetworkPolicies.Items {
keys = append(keys, strconv.Itoa(i))
}
// getting NetworkPolicy labels for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
nl, err := capsulev1alpha1.GetTypeLabel(&networkingv1.NetworkPolicy{})
nl, err := capsulev1beta1.GetTypeLabel(&networkingv1.NetworkPolicy{})
if err != nil {
return err
}
@@ -549,21 +586,25 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e
if err := r.pruningResources(ns, keys, &networkingv1.NetworkPolicy{}); err != nil {
return err
}
for i, spec := range tenant.Spec.NetworkPolicies {
for i, spec := range tenant.Spec.NetworkPolicies.Items {
t := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
Namespace: ns,
Labels: map[string]string{
tl: tenant.Name,
nl: strconv.Itoa(i),
},
},
}
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, t, func() (err error) {
t.SetLabels(map[string]string{
tl: tenant.Name,
nl: strconv.Itoa(i),
})
t.Spec = spec
return controllerutil.SetControllerReference(tenant, t, r.Scheme)
})
r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", t.GetName()), err)
r.Log.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
if err != nil {
return err
@@ -578,19 +619,32 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e
// Since RBAC is based on deny all first, some specific actions like editing Capsule resources are going to be blocked
// via Dynamic Admission Webhooks.
// TODO(prometherion): we could create a capsule:admin role rather than hitting webhooks for each action
func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1beta1.Tenant) error {
// getting RoleBinding label for the mutateFn
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
var subjects []rbacv1.Subject
tl, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
return err
}
l := map[string]string{tl: tenant.Name}
s := []rbacv1.Subject{
{
Kind: tenant.Spec.Owner.Kind.String(),
Name: tenant.Spec.Owner.Name,
},
for _, owner := range tenant.Spec.Owners {
if owner.Kind == "ServiceAccount" {
splitName := strings.Split(owner.Name, ":")
subjects = append(subjects, rbacv1.Subject{
Kind: owner.Kind.String(),
Name: splitName[len(splitName)-1],
Namespace: splitName[len(splitName)-2],
})
} else {
subjects = append(subjects, rbacv1.Subject{
APIGroup: "rbac.authorization.k8s.io",
Kind: owner.Kind.String(),
Name: owner.Name,
})
}
}
rbl := make(map[types.NamespacedName]rbacv1.RoleRef)
@@ -618,10 +672,13 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, target, func() (err error) {
target.ObjectMeta.Labels = l
target.Subjects = s
target.Subjects = subjects
target.RoleRef = rr
return controllerutil.SetControllerReference(tenant, target, r.Scheme)
})
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err)
r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return err
@@ -630,10 +687,10 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
return nil
}
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1alpha1.Tenant{}
found := &capsulev1beta1.Tenant{}
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
}
@@ -642,7 +699,17 @@ func (r *TenantReconciler) ensureNamespaceCount(tenant *capsulev1alpha1.Tenant)
})
}
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.Tenant) error {
func (r *TenantReconciler) emitEvent(object runtime.Object, namespace string, res controllerutil.OperationResult, msg string, err error) {
var eventType = corev1.EventTypeNormal
if err != nil {
eventType = corev1.EventTypeWarning
res = "Error"
}
r.Recorder.AnnotatedEventf(object, map[string]string{"OperationResult": string(res)}, eventType, namespace, msg)
}
func (r *TenantReconciler) collectNamespaces(tenant *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
nl := &corev1.NamespaceList{}
err = r.Client.List(context.TODO(), nl, client.MatchingFieldsSelector{
@@ -658,3 +725,15 @@ func (r *TenantReconciler) collectNamespaces(tenant *capsulev1alpha1.Tenant) err
return
})
}
func (r *TenantReconciler) updateTenantStatus(tnt *capsulev1beta1.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if tnt.IsCordoned() {
tnt.Status.State = capsulev1beta1.TenantStateCordoned
} else {
tnt.Status.State = capsulev1beta1.TenantStateActive
}
return r.Client.Status().Update(context.Background(), tnt)
})
}

View File

@@ -6,8 +6,8 @@ The first step is to set up your local development environment as stated below:
## Setting up the development environment
The following dependencies are mandatory:
- [Go 1.13.8](https://golang.org/dl/)
- [OperatorSDK 1.9](https://github.com/operator-framework/operator-sdk)
- [Go 1.16](https://golang.org/dl/)
- [OperatorSDK 1.7.2](https://github.com/operator-framework/operator-sdk)
- [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
- [KinD](https://github.com/kubernetes-sigs/kind)
- [ngrok](https://ngrok.com/) (if you want to run locally)

View File

@@ -0,0 +1,149 @@
# Capsule with Amazon EKS
This is an example how to install Amazon EKS cluster and one user
manged by capsule.
It is based on [Using IAM Groups to manage Kubernetes access](https://www.eksworkshop.com/beginner/091_iam-groups/intro/)
Create EKS cluster:
```bash
export AWS_DEFAULT_REGION="eu-west-1"
export AWS_ACCESS_KEY_ID="xxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxx"
eksctl create cluster \
--name=test-k8s \
--managed \
--node-type=t3.small \
--node-volume-size=20 \
--kubeconfig=kubeconfig.conf
```
Create AWS User `alice` using CloudFormation, create AWS access files and
kubeconfig for such user:
```bash
cat > cf.yml << \EOF
Parameters:
ClusterName:
Type: String
Resources:
UserAlice:
Type: AWS::IAM::User
Properties:
UserName: !Sub "alice-${ClusterName}"
Policies:
- PolicyName: !Sub "alice-${ClusterName}-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowAssumeOrganizationAccountRole
Effect: Allow
Action: sts:AssumeRole
Resource: !GetAtt RoleAlice.Arn
AccessKeyAlice:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref UserAlice
RoleAlice:
Type: AWS::IAM::Role
Properties:
Description: !Sub "IAM role for the alice-${ClusterName} user"
RoleName: !Sub "alice-${ClusterName}"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: sts:AssumeRole
Outputs:
RoleAliceArn:
Description: The ARN of the Alice IAM Role
Value: !GetAtt RoleAlice.Arn
Export:
Name:
Fn::Sub: "${AWS::StackName}-RoleAliceArn"
AccessKeyAlice:
Description: The AccessKey for Alice user
Value: !Ref AccessKeyAlice
Export:
Name:
Fn::Sub: "${AWS::StackName}-AccessKeyAlice"
SecretAccessKeyAlice:
Description: The SecretAccessKey for Alice user
Value: !GetAtt AccessKeyAlice.SecretAccessKey
Export:
Name:
Fn::Sub: "${AWS::StackName}-SecretAccessKeyAlice"
EOF
eval aws cloudformation deploy --capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides "ClusterName=test-k8s" \
--stack-name "test-k8s-users" --template-file cf.yml
AWS_CLOUDFORMATION_DETAILS=$(aws cloudformation describe-stacks --stack-name "test-k8s-users")
ALICE_ROLE_ARN=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"RoleAliceArn\") .OutputValue")
ALICE_USER_ACCESSKEY=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"AccessKeyAlice\") .OutputValue")
ALICE_USER_SECRETACCESSKEY=$(echo "${AWS_CLOUDFORMATION_DETAILS}" | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"SecretAccessKeyAlice\") .OutputValue")
eksctl create iamidentitymapping --cluster="test-k8s" --arn="${ALICE_ROLE_ARN}" --username alice --group capsule.clastix.io
cat > aws_config << EOF
[profile alice]
role_arn=${ALICE_ROLE_ARN}
source_profile=alice
EOF
cat > aws_credentials << EOF
[alice]
aws_access_key_id=${ALICE_USER_ACCESSKEY}
aws_secret_access_key=${ALICE_USER_SECRETACCESSKEY}
EOF
eksctl utils write-kubeconfig --cluster=test-k8s --kubeconfig="kubeconfig-alice.conf"
cat >> kubeconfig-alice.conf << EOF
- name: AWS_PROFILE
value: alice
- name: AWS_CONFIG_FILE
value: aws_config
- name: AWS_SHARED_CREDENTIALS_FILE
value: aws_credentials
EOF
```
----
Export "admin" kubeconfig to be able to install capsule:
```bash
export KUBECONFIG=kubeconfig.conf
```
Install capsule from helm chart:
```bash
helm repo add clastix https://clastix.github.io/charts
helm upgrade --install --version 0.0.19 --namespace capsule-system --create-namespace capsule clastix/capsule
```
Use the default Tenant example:
```bash
kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/samples/capsule_v1alpha1_tenant.yaml
```
Based on the tenant configuration above the user `alice` should be able
to create namespace...
Switch to new terminal tab and try to create namespace as user `alice`:
```bash
# Unset AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY if defined
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
kubectl create namespace test --kubeconfig="kubeconfig-alice.conf"
... do other commands allowed by Tenant configuration ...
```

View File

@@ -0,0 +1,43 @@
# Cordoning a Tenant
Bill needs to cordon a Tenant and its Namespaces for several reasons:
- Avoid accidental resource modification(s) including deletion during a Production Freeze Window
- During Kubernetes upgrade, to prevent any workload updates
- During incidents or outages
- During planned maintenance of a dedicated nodes pool in a BYOD scenario
With this said, the Tenant Owner and the related Service Account living into managed Namespaces, cannot proceed to any update, create or delete action.
This is possible just labelling the Tenant as follows:
```shell
$ kubectl label tenant oil capsule.clastix.io/cordon=enabled
tenant oil labeled
```
Any operation performed by Alice, the Tenant Owner, will be rejected.
```shell
$ kubectl --as alice --as-group capsule.clastix.io -n oil-dev create deployment nginx --image nginx
error: failed to create deployment: admission webhook "cordoning.tenant.capsule.clastix.io" denied the request: tenant oil is freezed: please, reach out to the system administrator
$ kubectl --as alice --as-group capsule.clastix.io -n oil-dev delete ingress,deployment,serviceaccount --all
error: failed to create deployment: admission webhook "cordoning.tenant.capsule.clastix.io" denied the request: tenant oil is freezed: please, reach out to the system administrator
```
Uncordoning can be done removing the said label:
```shell
$ kubectl label tenant oil capsule.clastix.io/cordon-
tenant.capsule.clastix.io/oil labeled
$ kubectl --as alice --as-group capsule.clastix.io -n oil-dev create deployment nginx --image nginx
deployment.apps/nginx created
```
# Whats next
This end our tour in Capsule use cases.
As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future.
Stay tuned!

View File

@@ -0,0 +1,34 @@
# Enforcing Pod containers image PullPolicy
Bill is a cluster admin providing a Container as a Service platform using shared nodes.
Alice, a Tenant Owner, can start container images using private images: according to the Kubernetes architecture, the `kubelet` will download the layers on its cache.
Bob, an attacker, could try to schedule a Pod on the same node where Alice is running their Pod backed by private images: they could start new Pods using `ImagePullPolicy=IfNotPresent` and able to start them, even without required authentication since the image is cached on the node.
To avoid this kind of attack all the Tenant Owners must start their Pods using the `ImagePullPolicy` to `Always`, enforcing the `kubelet` to check the authorization first.
Capsule provides a way to enforce this behavior, as follows.
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: Tenant
metadata:
name: oil
annotations:
capsule.clastix.io/allowed-image-pull-policy: Always
spec:
owner:
name: alice
kind: User
```
If you need to address specific use-case, the said annotation supports multiple values comma separated
```yaml
capsule.clastix.io/allowed-image-pull-policy: Always,IfNotPresent
```
# Whats next
See how Bill, the cluster admin, can assign trusted images registries to Alice's tenant. [Assign Trusted Images Registries](./images-registries.md).

View File

@@ -107,4 +107,5 @@ kubectl create -f gas-production-ns.yaml
>`Unable to assign namespace to tenant. Please use capsule.clastix.io/tenant label when creating a namespace.`
# Whats next
This end our tour in Capsule use cases. As we improve Capsule, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future. Stay tuned!
See how Bill, the cluster admin, can cordon all the Namespaces belonging to a Tenant. [Cordoning a Tenant](./cordoning-tenant.md).

View File

@@ -85,14 +85,14 @@ capsule-oil-0 <none> 42h
production-network-policy <none> 3m
```
an delete the namespace network-policies
And delete the namespace network policies
```
alice@caas# kubectl -n oil-production delete networkpolicy production-network-policy
```
However, the Capsule controller prevents Alice to delete the tenant network policy:
However, the Capsule controller prevents Alice from deleting the tenant network policy:
```
alice@caas# kubectl -n oil-production delete networkpolicy capsule-oil-0
@@ -100,4 +100,5 @@ Error from server (Capsule Network Policies cannot be deleted: please, reach out
```
# Whats next
See how Bill, the cluster admin, can assign trusted images registries to Alice's tenant. [Assign Trusted Images Registries](./images-registries.md).
See how Bill can enforce the Pod containers image pull policy to `Always` to avoid leaking of private images when running on shared nodes.
[Enforcing Pod containers image PullPolicy](./images-pullpolicy.md)

View File

@@ -32,11 +32,14 @@ Bill, at Acme Corp. can use Capsule to address any of the following scenarios:
* [Assign Storage Classes](./storage-classes.md)
* [Disable NodePort Services](./node-ports.md)
* [Assign Network Policies](./network-policies.md)
* [Enforcing Pod containers image PullPolicy](./images-pullpolicy.md)
* [Assign Trusted Images Registries](./images-registries.md)
* [Assign Pod Security Policies](./pod-security-policies.md)
* [Create Custom Resources](./custom-resources.md)
* [Taint Namespaces](./taint-namespaces.md)
* [Assign multiple Tenants to an owner](./multiple-tenants.md)
* [Cordoning a Tenant](./cordoning-tenant.md)
* [Velero Backup Restoration](./velero-backup-restoration.md)
> NB: as we improve Capsule, more use cases about multi-tenancy and cluster governance will be covered.

View File

@@ -1,7 +1,7 @@
# Assign permissions
Alice acts as the tenant admin. Other users can operate inside the tenant with different levels of permissions and authorizations. Alice is responsible for creating additional roles and assigning these roles to other users to work in the same tenant.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill has not to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill does not have to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
Capsule leaves Alice the freedom to create RBAC roles at the namespace level, or using the pre-defined cluster roles already available in Kubernetes, and assign them to other users in the tenant. Since roles and rolebindings are limited to a namespace scope, Alice can assign the roles to the other users accessing the same tenant only after the namespace is created. This gives Alice the power to administer the tenant without the intervention of the cluster admin.

View File

@@ -31,3 +31,7 @@ With the said Tenant specification Alice can create Pod resource if `spec.priori
- `tier-gold`, `tier-silver`, or `tier-bronze`, since these compile the regex declared in the annotation `priorityclass.capsule.clastix.io/allowed-regex`
If a Pod is going to use a non-allowed _Priority Class_, it will be rejected by the Validation Webhook enforcing it.
# Whats next
See how Bill, the cluster admin, can assign a pool of nodes to Alice's tenant. [Assign a nodes pool](./nodes-pool.md).

View File

@@ -70,7 +70,7 @@ roleRef:
name: 'psp:privileged'
```
With the above example, Capsule is forbidding to any authenticated user in `oil-production` namespace to run privileged pods and let them to performs privilege escalation as declared by the Cluster Role `psp:privileged`.
With the above example, Capsule is forbidding any authenticated user in `oil-production` namespace to run privileged pods and to perform privilege escalation as declared by the Cluster Role `psp:privileged`.
# Whats next
See how Bill, the cluster admin, can assign to Alice the permissions to create custom resources in her tenant. [Create Custom Resources](./custom-resources.md).

View File

@@ -19,7 +19,7 @@ spec:
scopes:
- NotTerminating
- hard:
pods: "100"
pods: "10"
services: "50"
- hard:
requests.storage: 10Gi
@@ -210,4 +210,5 @@ no - no RBAC policy matched
```
# Whats next
See how Bill, the cluster admin, can assign a pool of nodes to Alice's tenant. [Assign a nodes pool](./nodes-pool.md).
See how Bill, the cluster admin, can enforce the PriorityClass of Pods running of Alice's tenant Namespaces. [Enforce Pod Priority Classes](./pod-priority-class.md)

View File

@@ -0,0 +1,27 @@
# Velero Backup Restoration
Velero is a backup system that perform disaster recovery, and migrate Kubernetes cluster resources and persistent volumes.
Using this in a Kubernetes cluster where Capsule is installed can lead to an incomplete restore of the cluster's Tenants. This is due to the fact that Velero omits the `ownerReferences` section from the tenant's namespace manifests when backup them.
To avoid this problem you can use the script `velero-restore.sh` under the `hack/` folder.
Below are some examples on how to use the script to avoid incomplete restorations.
## Getting Started
In case of a data loss, the right thing to do is to restore the cluster with **Velero** at first. Once Velero has finished, you can proceed using the script to complete the restoration.
```bash
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig restore
```
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
Additionally you can also specify a selected range of tenants to be restored:
```bash
./velero-restore.sh --tenant "gas oil" restore
```
In this way, only the tenants **gas** and **oil** will be restored.

View File

@@ -1,20 +1,7 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
@@ -27,20 +14,22 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating a Namespace with an additional Role Binding", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "additional-role-binding",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "dale",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "dale",
Kind: "User",
},
},
AdditionalRoleBindings: []v1alpha1.AdditionalRoleBindings{
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "crds-rolebinding",
Subjects: []rbacv1.Subject{
@@ -68,14 +57,14 @@ var _ = Describe("creating a Namespace with an additional Role Binding", func()
It("should be assigned to each Namespace", func() {
for _, ns := range []string{"rb-1", "rb-2", "rb-3"} {
ns := NewNamespace(ns)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
var rb *rbacv1.RoleBinding
Eventually(func() (err error) {
cs := ownerClient(tnt)
rb, err = cs.RbacV1().RoleBindings(ns.Name).Get(context.Background(), fmt.Sprintf("capsule-%s-%s", tnt.Name, "crds-rolebinding"), metav1.GetOptions{})
cs := ownerClient(tnt.Spec.Owners[0])
rb, err = cs.RbacV1().RoleBindings(ns.Name).Get(context.Background(), fmt.Sprintf("capsule-%s-0-%s", tnt.Name, "crds-rolebinding"), metav1.GetOptions{})
return err
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
Expect(rb.RoleRef.Name).Should(Equal(tnt.Spec.AdditionalRoleBindings[0].ClusterRoleName))

View File

@@ -1,20 +1,7 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
@@ -27,21 +14,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("enforcing an allowed set of Service external IPs", func() {
tnt := &v1alpha1.Tenant{
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed-external-ip",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "google",
Kind: "User",
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
ExternalServiceIPs: &v1alpha1.ExternalServiceIPs{
Allowed: []v1alpha1.AllowedIP{
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{
"10.20.0.0/16",
"192.168.1.2/32",
},
@@ -61,7 +50,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should fail creating an evil service", func() {
ns := NewNamespace("evil-service")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -86,7 +75,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())
@@ -94,7 +83,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should allow the first CIDR block", func() {
ns := NewNamespace("allowed-service-cidr")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -119,7 +108,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())
@@ -127,7 +116,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
It("should allow the /32 CIDR block", func() {
ns := NewNamespace("allowed-service-strict")
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -151,7 +140,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
},
}
EventuallyCreation(func() error {
cs := ownerClient(tnt)
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())

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