Compare commits

...

28 Commits

Author SHA1 Message Date
Dario Tranchitella
261876b59b docs: documenting new support for dynamic tenant owners clusterrole 2022-06-29 10:53:35 +00:00
Dario Tranchitella
ab750141c6 refactor: support for rfc 1123 for tenant owners cluster roles overrides 2022-06-29 10:53:35 +00:00
Oliver Bähler
e237249815 feat: improve chart documentation
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2022-06-29 08:35:43 +00:00
Dario Tranchitella
e15191c2a0 refactor: sentinel error for running in out of cluster mode 2022-06-29 08:31:21 +00:00
Dario Tranchitella
741db523e5 chore(gh): adding 1.24 to the e2e test matrix 2022-06-14 14:39:05 +00:00
Dario Tranchitella
7b3f850035 chore(gh): disabling fail fast for e2e 2022-06-13 09:52:58 +00:00
jandres - moscardo
72733415f0 fix(docs): helm example was wrong when customizing value 2022-06-10 13:48:49 +02:00
Oliver Bähler
cac2920827 feat: grant global patch privileges and add patch handler 2022-06-09 18:32:39 +00:00
Dario Tranchitella
e0b339d68a fix(tests): cleaning up protected tenant upon test success 2022-06-09 18:30:52 +00:00
Dario Tranchitella
4f55dd8db8 refactor: removing unrequired verb for clusterrole namespace deleter 2022-06-09 18:30:52 +00:00
Massimiliano Giovagnoli
fd738341ed docs: fix typos
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-06-09 12:29:10 +00:00
Maksim Fedotov
fce1658827 chore: remove unused CASecretNameAnnotation constant 2022-06-08 11:12:35 +00:00
Maksim Fedotov
93547c128f build(helm): revert bumping chart version 2022-06-08 11:12:35 +00:00
Maksim Fedotov
f1dc028649 feat: generate TLS certificates before starting controllers 2022-06-08 11:12:35 +00:00
Maksim Fedotov
37381184d2 build(helm): refactor capsule TLS certificates management 2022-06-08 11:12:35 +00:00
Maksim Fedotov
82b58d7d53 feat: refactor capsule TLS certificates management 2022-06-08 11:12:35 +00:00
tony
60e826dc83 docs: update tenant owner default cluster documentation 2022-06-06 06:48:18 +02:00
dependabot[bot]
6e8ddd102f build(deps): bump eventsource from 1.1.0 to 1.1.1 in /docs
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-03 13:10:21 +00:00
Dario Tranchitella
b64aaebc89 docs: referring to docker hub image 2022-05-31 12:38:38 +00:00
Dario Tranchitella
9a85631bb8 chore(yaml): using docker hub image 2022-05-31 12:38:38 +00:00
Dario Tranchitella
51ed42981f chore(helm): using docker hub image 2022-05-31 12:38:38 +00:00
Dario Tranchitella
cf313d415b chore(make): using docker hub image 2022-05-31 12:38:38 +00:00
Adriano Pezzuto
526a6053a5 docs: documenting charmed operator (#572)
* docs: documenting charmed operator
2022-05-27 21:20:35 +02:00
Dario Tranchitella
0dd13a96fc chore(yaml): aligning to v0.1.2-rc0 image 2022-05-24 15:50:48 +00:00
Dario Tranchitella
1c8a5d8f5a docs(proxy): documenting retrieval of a single namespace 2022-05-24 15:32:04 +00:00
song
b9fc50861b style: removing unused struct field 2022-05-24 15:31:24 +00:00
ptx96
29d29ccd4b feat(ci): added docker.io repository 2022-05-24 13:26:57 +00:00
Massimiliano Giovagnoli
f207546af0 docs(readme.md): add slack link
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-05-24 09:02:10 +00:00
52 changed files with 1149 additions and 869 deletions

View File

@@ -36,6 +36,7 @@ jobs:
with:
images: |
quay.io/${{ github.repository }}
docker.io/${{ github.repository }}
tags: |
type=semver,pattern={{raw}}
flavor: |
@@ -68,6 +69,13 @@ jobs:
username: ${{ github.repository_owner }}+github
password: ${{ secrets.BOT_QUAY_IO }}
- name: Login to docker.io Container Registry
uses: docker/login-action@v1
with:
registry: docker.io
username: ${{ secrets.USER_DOCKER_IO }}
password: ${{ secrets.BOT_DOCKER_IO }}
- name: Build and push
id: build-release
uses: docker/build-push-action@v2

View File

@@ -28,8 +28,9 @@ jobs:
kind:
name: Kubernetes
strategy:
fail-fast: false
matrix:
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.4', 'v1.23.0']
k8s-version: ['v1.16.15', 'v1.17.11', 'v1.18.8', 'v1.19.4', 'v1.20.7', 'v1.21.2', 'v1.22.4', 'v1.23.6', 'v1.24.1']
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
@@ -46,7 +47,7 @@ jobs:
- uses: engineerd/setup-kind@v0.5.0
with:
skipClusterCreation: true
version: v0.11.1
version: v0.14.0
- uses: azure/setup-helm@v1
with:
version: 3.3.4

1
.gitignore vendored
View File

@@ -28,4 +28,5 @@ bin
**/*.crt
**/*.key
.DS_Store
*.tgz

View File

@@ -2,7 +2,7 @@
VERSION ?= $$(git describe --abbrev=0 --tags --match "v*")
# Default bundle image tag
BUNDLE_IMG ?= quay.io/clastix/capsule:$(VERSION)-bundle
BUNDLE_IMG ?= clastix/capsule:$(VERSION)-bundle
# Options for 'bundle-build'
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
@@ -13,7 +13,7 @@ endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# Image URL to use all building/pushing image targets
IMG ?= quay.io/clastix/capsule:$(VERSION)
IMG ?= clastix/capsule:$(VERSION)
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:preserveUnknownFields=false"

View File

@@ -5,6 +5,9 @@
<a href="https://github.com/clastix/capsule/releases">
<img src="https://img.shields.io/github/v/release/clastix/capsule"/>
</a>
<a href="https://charmhub.io/capsule-k8s">
<img src="https://charmhub.io/capsule-k8s/badge.svg"/>
</a>
</p>
<p align="center">
@@ -13,13 +16,15 @@
---
**Join the community** on the [#capsule](https://kubernetes.slack.com/archives/C03GETTJQRL) channel in the [Kubernetes Slack](https://slack.k8s.io/).
# Kubernetes multi-tenancy made easy
**Capsule** implements a multi-tenant and policy-based environment in your Kubernetes cluster. It is designed as a micro-services-based ecosystem with the minimalist approach, leveraging only on upstream Kubernetes.
# What's the problem with the current status?
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well know phenomena of the _clusters sprawl_.
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well known phenomena of the _clusters sprawl_.
# Entering Capsule
@@ -65,6 +70,16 @@ Please, check the project [documentation](https://capsule.clastix.io) for the co
Capsule is Open Source with Apache 2 license and any contribution is welcome.
## Chart Development
The documentation for each chart is done with [helm-docs](https://github.com/norwoodj/helm-docs). This way we can ensure that values are consistent with the chart documentation.
We have a script on the repository which will execute the helm-docs docker container, so that you don't have to worry about downloading the binary etc. Simply execute the script (Bash compatible):
```
bash scripts/helm-docs.sh
```
## Community
Join the community, share and learn from it. You can find all the resources to how to contribute code and docs, connect with people in the [community repository](https://github.com/clastix/capsule-community).

View File

@@ -5,8 +5,8 @@ const (
ForbiddenNodeLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-node-labels-regexp"
ForbiddenNodeAnnotationsAnnotation = "capsule.clastix.io/forbidden-node-annotations"
ForbiddenNodeAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-node-annotations-regexp"
CASecretNameAnnotation = "capsule.clastix.io/ca-secret-name"
TLSSecretNameAnnotation = "capsule.clastix.io/tls-secret-name"
MutatingWebhookConfigurationName = "capsule.clastix.io/mutating-webhook-configuration-name"
ValidatingWebhookConfigurationName = "capsule.clastix.io/validating-webhook-configuration-name"
GenerateCertificatesAnnotationName = "capsule.clastix.io/generate-certificates"
)

View File

@@ -12,12 +12,37 @@ const (
ClusterRoleNamesAnnotation = "clusterrolenames.capsule.clastix.io"
)
func (in OwnerSpec) GetRoles(tenant Tenant) []string {
// GetRoles read the annotation available in the Tenant specification and if it matches the pattern
// clusterrolenames.capsule.clastix.io/${KIND}.${NAME} returns the associated roles.
// Kubernetes annotations and labels must respect RFC 1123 about DNS names and this could be cumbersome in two cases:
// 1. identifying users based on their email address
// 2. the overall length of the annotation key that is exceeding 63 characters
// For emails, the symbol @ can be replaced with the placeholder __AT__.
// For the latter one, the index of the owner can be used to force the retrieval.
func (in OwnerSpec) GetRoles(tenant Tenant, index int) []string {
for key, value := range tenant.GetAnnotations() {
if key == fmt.Sprintf("%s/%s.%s", ClusterRoleNamesAnnotation, strings.ToLower(in.Kind.String()), strings.ToLower(in.Name)) {
if !strings.HasPrefix(key, fmt.Sprintf("%s/", ClusterRoleNamesAnnotation)) {
continue
}
for symbol, replace := range in.convertMap() {
key = strings.ReplaceAll(key, symbol, replace)
}
nameBased := key == fmt.Sprintf("%s/%s.%s", ClusterRoleNamesAnnotation, strings.ToLower(in.Kind.String()), strings.ToLower(in.Name))
indexBased := key == fmt.Sprintf("%s/%d", ClusterRoleNamesAnnotation, index)
if nameBased || indexBased {
return strings.Split(value, ",")
}
}
return []string{"admin", "capsule-namespace-deleter"}
}
func (in OwnerSpec) convertMap() map[string]string {
return map[string]string{
"__AT__": "@",
}
}

View File

@@ -269,6 +269,21 @@ func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NonLimitedResourceError) DeepCopyInto(out *NonLimitedResourceError) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonLimitedResourceError.
func (in *NonLimitedResourceError) DeepCopy() *NonLimitedResourceError {
if in == nil {
return nil
}
out := new(NonLimitedResourceError)
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) {
{

View File

@@ -21,3 +21,4 @@
.idea/
*.tmproj
.vscode/
README.md.gotmpl

View File

@@ -54,50 +54,98 @@ The values in your overrides file `myvalues.yaml` will override their counterpar
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
$ helm install capsule capsule-helm-chart --set force_tenant_prefix=false -n capsule-system
$ helm install capsule capsule-helm-chart --set manager.options.forceTenantPrefix=false -n capsule-system
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.capsuleUserGroups` | Override the Capsule user groups | `[capsule.clastix.io]`
`manager.options.protectedNamespaceRegex` | If specified, disallows creation of namespaces matching the passed regexp | `null`
`manager.options.enableSecretController` | Boolean, enables apsule secret controller which reconciles TLS and CA secrets for capsule webhooks. | `true`
`manager.image.repository` | Set the image repository of the controller. | `quay.io/clastix/capsule`
`manager.image.tag` | Overrides the image tag whose default is the chart. `appVersion` | `null`
`manager.image.pullPolicy` | Set the image pull policy. | `IfNotPresent`
`manager.livenessProbe` | Configure the liveness probe using Deployment probe spec | `GET :10080/healthz`
`manager.readinessProbe` | Configure the readiness probe using Deployment probe spec | `GET :10080/readyz`
`manager.resources.requests/cpu` | Set the CPU requests assigned to the controller. | `200m`
`manager.resources.requests/memory` | Set the memory requests assigned to the controller. | `128Mi`
`manager.resources.limits/cpu` | Set the CPU limits assigned to the controller. | `200m`
`manager.resources.limits/cpu` | Set the memory limits assigned to the controller. | `128Mi`
`mutatingWebhooksTimeoutSeconds` | Timeout in seconds for mutating webhooks. | `30`
`validatingWebhooksTimeoutSeconds` | Timeout in seconds for validating webhooks. | `30`
`webhooks` | Additional configuration for capsule webhooks. |
`imagePullSecrets` | Configuration for `imagePullSecrets` so that you can use a private images registry. | `[]`
`serviceAccount.create` | Specifies whether a service account should be created. | `true`
`serviceAccount.annotations` | Annotations to add to the service account. | `{}`
`serviceAccount.name` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template | `capsule`
`podAnnotations` | Annotations to add to the Capsule pod. | `{}`
`priorityClassName` | Set the priority class name of the Capsule pod. | `null`
`nodeSelector` | Set the node selector for the Capsule pod. | `{}`
`tolerations` | Set list of tolerations for the Capsule pod. | `[]`
`replicaCount` | Set the replica count for Capsule pod. | `1`
`affinity` | Set affinity rules for the Capsule pod. | `{}`
`podSecurityPolicy.enabled` | Specify if a Pod Security Policy must be created. | `false`
`serviceMonitor.enabled` | Specifies if a service monitor must be created. | `false`
`serviceMonitor.labels` | Additional labels which will be added to service monitor. | `{}`
`serviceMonitor.annotations` | Additional annotations which will be added to service monitor. | `{}`
`serviceMonitor.matchLabels` | Additional matchLabels which will be added to service monitor. | `{}`
`serviceMonitor.serviceAccount.name` | Specifies service account name for metrics scrape. | `capsule`
`serviceMonitor.serviceAccount.namespace` | Specifies service account namespace for metrics scrape. | `capsule-system`
`customLabels` | Additional labels which will be added to all resources created by Capsule helm chart . | `{}`
`customAnnotations` | Additional annotations which will be added to all resources created by Capsule helm chart . | `{}`
`certManager.generateCertificates` | Specifies whether capsule webhooks certificates should be generated using cert-manager. | `false`
### General Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | Set affinity rules for the Capsule pod |
| certManager.generateCertificates | bool | `false` | Specifies whether capsule webhooks certificates should be generated using cert-manager |
| customAnnotations | object | `{}` | Additional annotations which will be added to all resources created by Capsule helm chart |
| customLabels | object | `{}` | Additional labels which will be added to all resources created by Capsule helm chart |
| jobs.image.pullPolicy | string | `"IfNotPresent"` | Set the image pull policy of the helm chart job |
| jobs.image.repository | string | `"quay.io/clastix/kubectl"` | Set the image repository of the helm chart job |
| jobs.image.tag | string | `""` | Set the image tag of the helm chart job |
| mutatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for mutating webhooks |
| nodeSelector | object | `{}` | Set the node selector for the Capsule pod |
| podAnnotations | object | `{}` | Annotations to add to the capsule pod. |
| podSecurityPolicy.enabled | bool | `false` | Specify if a Pod Security Policy must be created |
| priorityClassName | string | `""` | Set the priority class name of the Capsule pod |
| replicaCount | int | `1` | Set the replica count for capsule pod |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |
| serviceAccount.name | string | `"capsule"` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template |
| tolerations | list | `[]` | Set list of tolerations for the Capsule pod |
| validatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for validating webhooks |
### Manager Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| manager.hostNetwork | bool | `false` | Specifies if the container should be started in hostNetwork mode. Required for use in some managed kubernetes clusters (such as AWS EKS) with custom CNI (such as calico), because control-plane managed by AWS cannot communicate with pods' IP CIDR and admission webhooks are not working |
| manager.image.pullPolicy | string | `"IfNotPresent"` | Set the image pull policy. |
| manager.image.repository | string | `"clastix/capsule"` | Set the image repository of the capsule. |
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
| manager.imagePullSecrets | list | `[]` | Configuration for `imagePullSecrets` so that you can use a private images registry. |
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
| manager.options.capsuleUserGroups | list | `["capsule.clastix.io"]` | Override the Capsule user groups |
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
| manager.readinessProbe | object | `{"httpGet":{"path":"/readyz","port":10080}}` | Configure the readiness probe using Deployment probe spec |
| manager.resources.limits.cpu | string | `"200m"` | |
| manager.resources.limits.memory | string | `"128Mi"` | |
| manager.resources.requests.cpu | string | `"200m"` | |
| manager.resources.requests.memory | string | `"128Mi"` | |
### ServiceMonitor Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| serviceMonitor.annotations | object | `{}` | Assign additional Annotations |
| serviceMonitor.enabled | bool | `false` | Enable ServiceMonitor |
| serviceMonitor.endpoint.interval | string | `"15s"` | Set the scrape interval for the endpoint of the serviceMonitor |
| serviceMonitor.endpoint.metricRelabelings | list | `[]` | Set metricRelabelings for the endpoint of the serviceMonitor |
| serviceMonitor.endpoint.relabelings | list | `[]` | Set relabelings for the endpoint of the serviceMonitor |
| serviceMonitor.endpoint.scrapeTimeout | string | `""` | Set the scrape timeout for the endpoint of the serviceMonitor |
| serviceMonitor.labels | object | `{}` | Assign additional labels according to Prometheus' serviceMonitorSelector matching labels |
| serviceMonitor.matchLabels | object | `{}` | Change matching labels |
| serviceMonitor.namespace | string | `""` | Install the ServiceMonitor into a different Namespace, as the monitoring stack one (default: the release one) |
| serviceMonitor.serviceAccount.name | string | `"capsule"` | ServiceAccount for Metrics RBAC |
| serviceMonitor.serviceAccount.namespace | string | `"capsule-system"` | ServiceAccount Namespace for Metrics RBAC |
| serviceMonitor.targetLabels | list | `[]` | Set targetLabels for the serviceMonitor |
### Webhook Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| webhooks.cordoning.failurePolicy | string | `"Fail"` | |
| webhooks.cordoning.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.cordoning.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.ingresses.failurePolicy | string | `"Fail"` | |
| webhooks.ingresses.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.ingresses.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.namespaceOwnerReference.failurePolicy | string | `"Fail"` | |
| webhooks.namespaces.failurePolicy | string | `"Fail"` | |
| webhooks.networkpolicies.failurePolicy | string | `"Fail"` | |
| webhooks.networkpolicies.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.networkpolicies.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.nodes.failurePolicy | string | `"Fail"` | |
| webhooks.persistentvolumeclaims.failurePolicy | string | `"Fail"` | |
| webhooks.persistentvolumeclaims.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.persistentvolumeclaims.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.pods.failurePolicy | string | `"Fail"` | |
| webhooks.pods.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.pods.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.services.failurePolicy | string | `"Fail"` | |
| webhooks.services.namespaceSelector.matchExpressions[0].key | string | `"capsule.clastix.io/tenant"` | |
| webhooks.services.namespaceSelector.matchExpressions[0].operator | string | `"Exists"` | |
| webhooks.tenants.failurePolicy | string | `"Fail"` | |
## Created resources
This Helm Chart creates the following Kubernetes resources in the release namespace:

View File

@@ -0,0 +1,132 @@
# Deploying the Capsule Operator
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
## Requirements
* [Helm 3](https://github.com/helm/helm/releases) is required when installing the Capsule Operator chart. Follow Helms official [steps](https://helm.sh/docs/intro/install/) for installing helm on your particular operating system.
* A Kubernetes cluster 1.16+ with following [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled:
* PodNodeSelector
* LimitRanger
* ResourceQuota
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
* A [`kubeconfig`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file accessing the Kubernetes cluster with cluster admin permissions.
## Quick Start
The Capsule Operator Chart can be used to instantly deploy the Capsule Operator on your Kubernetes cluster.
1. Add this repository:
$ helm repo add clastix https://clastix.github.io/charts
2. Install the Chart:
$ helm install capsule clastix/capsule -n capsule-system --create-namespace
3. Show the status:
$ helm status capsule -n capsule-system
4. Upgrade the Chart
$ helm upgrade capsule clastix/capsule -n capsule-system
5. Uninstall the Chart
$ helm uninstall capsule -n capsule-system
## Customize the installation
There are two methods for specifying overrides of values during chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the chart:
$ helm install capsule capsule-helm-chart --values myvalues.yaml -n capsule-system
The values in your overrides file `myvalues.yaml` will override their counterparts in the charts values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
$ helm install capsule capsule-helm-chart --set manager.options.forceTenantPrefix=false -n capsule-system
Here the values you can override:
### General Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
{{- range .Values }}
{{- if not (or (hasPrefix "manager" .Key) (hasPrefix "serviceMonitor" .Key) (hasPrefix "webhook" .Key) (hasPrefix "capsule-proxy" .Key) ) }}
| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |
{{- end }}
{{- end }}
### Manager Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
{{- range .Values }}
{{- if hasPrefix "manager" .Key }}
| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |
{{- end }}
{{- end }}
### ServiceMonitor Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
{{- range .Values }}
{{- if hasPrefix "serviceMonitor" .Key }}
| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |
{{- end }}
{{- end }}
### Webhook Parameters
| Key | Type | Default | Description |
|-----|------|---------|-------------|
{{- range .Values }}
{{- if hasPrefix "webhook" .Key }}
| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |
{{- end }}
{{- end }}
## Created resources
This Helm Chart creates the following Kubernetes resources in the release namespace:
* Capsule Namespace
* Capsule Operator Deployment
* Capsule Service
* CA Secret
* Certificate Secret
* Tenant Custom Resource Definition
* CapsuleConfiguration Custom Resource Definition
* MutatingWebHookConfiguration
* ValidatingWebHookConfiguration
* RBAC Cluster Roles
* Metrics Service
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
Capsule, as many other add-ons, defines its own set of Custom Resource Definitions (CRDs). Helm3 removed the old CRDs installation method for a more simple methodology. In the Helm Chart, there is now a special directory called `crds` to hold the CRDs. These CRDs are not templated, but will be installed by default when running a `helm install` for the chart. If the CRDs already exist (for example, you already executed `helm install`), it will be skipped with a warning. When you wish to skip the CRDs installation, and do not see the warning, you can pass the `--skip-crds` flag to the `helm install` command.
## More
See Capsule [tutorial](https://github.com/clastix/capsule/blob/master/docs/content/general/tutorial.md) for more information about how to use Capsule.

View File

@@ -65,20 +65,6 @@ ServiceAccount annotations
{{- end }}
{{- end }}
{{/*
Webhook annotations
*/}}
{{- define "capsule.webhookAnnotations" -}}
{{- if .Values.certManager.generateCertificates -}}
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
{{- end }}
{{- if .Values.customAnnotations }}
{{ toYaml .Values.customAnnotations }}
{{- end }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
@@ -133,17 +119,6 @@ Create the Capsule Deployment name to use
{{- printf "%s-controller-manager" (include "capsule.fullname" .) -}}
{{- end }}
{{/*
Create the Capsule CA Secret name to use
*/}}
{{- define "capsule.secretCaName" -}}
{{- if .Values.certManager.generateCertificates }}
{{- printf "%s-tls" (include "capsule.fullname" .) -}}
{{- else }}
{{- printf "%s-ca" (include "capsule.fullname" .) -}}
{{- end }}
{{- end }}
{{/*
Create the Capsule TLS Secret name to use
*/}}

View File

@@ -1,12 +0,0 @@
{{- if not .Values.certManager.generateCertificates }}
apiVersion: v1
kind: Secret
metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "capsule.secretCaName" . }}
{{- end }}

View File

@@ -29,5 +29,8 @@ spec:
issuerRef:
kind: Issuer
name: {{ include "capsule.fullname" . }}-webhook-selfsigned
secretName: {{ include "capsule.fullname" . }}-tls
secretName: {{ include "capsule.secretTlsName" . }}
subject:
organizations:
- clastix.io
{{- end }}

View File

@@ -5,10 +5,10 @@ metadata:
labels:
{{- include "capsule.labels" . | nindent 4 }}
annotations:
capsule.clastix.io/ca-secret-name: {{ include "capsule.secretCaName" . }}
capsule.clastix.io/mutating-webhook-configuration-name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
capsule.clastix.io/tls-secret-name: {{ include "capsule.secretTlsName" . }}
capsule.clastix.io/validating-webhook-configuration-name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
capsule.clastix.io/generate-certificates: "{{ .Values.manager.options.generateCertificates }}"
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}

View File

@@ -47,7 +47,7 @@ spec:
- name: cert
secret:
defaultMode: 420
secretName: {{ include "capsule.fullname" . }}-tls
secretName: {{ include "capsule.secretTlsName" . }}
containers:
- name: manager
command:
@@ -56,7 +56,6 @@ spec:
- --enable-leader-election
- --zap-log-level={{ default 4 .Values.manager.options.logLevel }}
- --configuration-name=default
- --enable-secret-controller={{ .Values.manager.options.enableSecretController }}
image: {{ include "capsule.managerFullyQualifiedDockerImage" . }}
imagePullPolicy: {{ .Values.manager.image.pullPolicy }}
env:

View File

@@ -4,9 +4,9 @@ metadata:
name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if or (.Values.certManager.generateCertificates) (.Values.customAnnotations) }}
{{- with .Values.customAnnotations }}
annotations:
{{- include "capsule.webhookAnnotations" . | nindent 4 }}
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:
- admissionReviewVersions:

View File

@@ -1,4 +1,4 @@
{{- $cmd := printf "while [ -z $$(kubectl -n $NAMESPACE get secret %s -o jsonpath='{.data.tls\\\\.crt}') ];" (include "capsule.secretCaName" .) -}}
{{- $cmd := printf "while [ -z $$(kubectl -n $NAMESPACE get secret %s -o jsonpath='{.data.tls\\\\.crt}') ];" (include "capsule.secretTlsName" .) -}}
{{- $cmd = printf "%s do echo 'waiting Capsule to be up and running...' && sleep 5;" $cmd -}}
{{- $cmd = printf "%s done" $cmd -}}
apiVersion: batch/v1

View File

@@ -1,6 +1,6 @@
{{- $cmd := printf "kubectl scale deployment -n $NAMESPACE %s --replicas 0 &&" (include "capsule.deploymentName" .) -}}
{{- if not .Values.certManager.generateCertificates }}
{{- $cmd = printf "%s kubectl delete secret -n $NAMESPACE %s %s --ignore-not-found &&" $cmd (include "capsule.secretTlsName" .) (include "capsule.secretCaName" .) -}}
{{- $cmd = printf "%s kubectl delete secret -n $NAMESPACE %s --ignore-not-found &&" $cmd (include "capsule.secretTlsName" .) -}}
{{- end }}
{{- $cmd = printf "%s kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found &&" $cmd -}}
{{- $cmd = printf "%s kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found" $cmd -}}

View File

@@ -15,17 +15,33 @@ metadata:
{{- end }}
spec:
endpoints:
- interval: 15s
{{- with .Values.serviceMonitor.endpoint }}
- interval: {{ .interval }}
port: metrics
path: /metrics
{{- with .scrapeTimeout }}
scrapeTimeout: {{ . }}
{{- end }}
{{- with .metricRelabelings }}
metricRelabelings: {{- toYaml . | nindent 6 }}
{{- end }}
{{- with .relabelings }}
relabelings: {{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
jobLabel: app.kubernetes.io/name
{{- with .Values.serviceMonitor.targetLabels }}
targetLabels: {{- toYaml . | nindent 4 }}
{{- end }}
selector:
matchLabels:
{{- include "capsule.labels" . | nindent 6 }}
{{- with .Values.serviceMonitor.matchLabels }}
{{- toYaml . | nindent 6 }}
{{- if .Values.serviceMonitor.matchLabels }}
{{- toYaml .Values.serviceMonitor.matchLabels | nindent 6 }}
{{- else }}
{{- include "capsule.labels" . | nindent 6 }}
{{- end }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}
{{- end }}

View File

@@ -4,9 +4,9 @@ metadata:
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- if or (.Values.certManager.generateCertificates) (.Values.customAnnotations) }}
{{- with .Values.customAnnotations }}
annotations:
{{- include "capsule.webhookAnnotations" . | nindent 4 }}
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:
- admissionReviewVersions:

View File

@@ -2,30 +2,47 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Manager Options
manager:
image:
repository: quay.io/clastix/capsule
# -- Set the image repository of the capsule.
repository: clastix/capsule
# -- Set the image pull policy.
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
tag: ''
# Specifies if the container should be started in hostNetwork mode.
# -- Configuration for `imagePullSecrets` so that you can use a private images registry.
imagePullSecrets: []
# -- 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
# Additional Capsule Controller Options
options:
# -- Set the log verbosity of the capsule with a value from 1 to 10
logLevel: '4'
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
forceTenantPrefix: false
# -- Override the Capsule user groups
capsuleUserGroups: ["capsule.clastix.io"]
# -- If specified, disallows creation of namespaces matching the passed regexp
protectedNamespaceRegex: ""
enableSecretController: true
# -- Specifies whether capsule webhooks certificates should be generated by capsule operator
generateCertificates: true
# -- Configure the liveness probe using Deployment probe spec
livenessProbe:
httpGet:
path: /healthz
port: 10080
# -- Configure the readiness probe using Deployment probe spec
readinessProbe:
httpGet:
path: /readyz
@@ -38,49 +55,63 @@ manager:
requests:
cpu: 200m
memory: 128Mi
jobs:
image:
repository: quay.io/clastix/kubectl
pullPolicy: IfNotPresent
tag: ""
imagePullSecrets: []
serviceAccount:
create: true
annotations: {}
name: "capsule"
# -- Annotations to add to the capsule pod.
podAnnotations: {}
# The following annotations guarantee scheduling for critical add-on pods
# podAnnotations:
# scheduler.alpha.kubernetes.io/critical-pod: ''
# -- Set the priority class name of the Capsule pod
priorityClassName: '' #system-cluster-critical
# -- Set the node selector for the Capsule pod
nodeSelector: {}
# node-role.kubernetes.io/master: ""
# -- Set list of tolerations for the Capsule pod
tolerations: []
#- key: CriticalAddonsOnly
# operator: Exists
#- effect: NoSchedule
# key: node-role.kubernetes.io/master
# -- Set the replica count for capsule pod
replicaCount: 1
# -- Set affinity rules for the Capsule pod
affinity: {}
podSecurityPolicy:
# -- Specify if a Pod Security Policy must be created
enabled: false
jobs:
image:
# -- Set the image repository of the helm chart job
repository: quay.io/clastix/kubectl
# -- Set the image pull policy of the helm chart job
pullPolicy: IfNotPresent
# -- Set the image tag of the helm chart job
tag: ""
# ServiceAccount
serviceAccount:
# -- Specifies whether a service account should be created.
create: true
# -- Annotations to add to the service account.
annotations: {}
# -- The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template
name: "capsule"
certManager:
# -- Specifies whether capsule webhooks certificates should be generated using cert-manager
generateCertificates: 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
# Additional labels
# -- Additional labels which will be added to all resources created by Capsule helm chart
customLabels: {}
# Additional annotations
# -- Additional annotations which will be added to all resources created by Capsule helm chart
customAnnotations: {}
# Webhooks configurations
@@ -129,5 +160,37 @@ webhooks:
operator: Exists
nodes:
failurePolicy: Fail
# -- Timeout in seconds for mutating webhooks
mutatingWebhooksTimeoutSeconds: 30
# -- Timeout in seconds for validating webhooks
validatingWebhooksTimeoutSeconds: 30
# ServiceMonitor
serviceMonitor:
# -- Enable 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: {}
# -- Assign additional Annotations
annotations: {}
# -- Change matching labels
matchLabels: {}
# -- Set targetLabels for the serviceMonitor
targetLabels: []
serviceAccount:
# -- ServiceAccount for Metrics RBAC
name: capsule
# -- ServiceAccount Namespace for Metrics RBAC
namespace: capsule-system
endpoint:
# -- Set the scrape interval for the endpoint of the serviceMonitor
interval: "15s"
# -- Set the scrape timeout for the endpoint of the serviceMonitor
scrapeTimeout: ""
# -- Set metricRelabelings for the endpoint of the serviceMonitor
metricRelabelings: []
# -- Set relabelings for the endpoint of the serviceMonitor
relabelings: []

View File

@@ -1411,7 +1411,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/clastix/capsule:v0.1.1
image: clastix/capsule:v0.1.2-rc0
imagePullPolicy: IfNotPresent
name: manager
ports:

View File

@@ -6,5 +6,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: quay.io/clastix/capsule
newTag: v0.1.1
newName: clastix/capsule
newTag: v0.1.2-rc0

View File

@@ -23,7 +23,7 @@ var (
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"create"},
Verbs: []string{"create", "patch"},
},
},
},
@@ -35,7 +35,7 @@ var (
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"delete", "patch"},
Verbs: []string{"delete"},
},
},
},

View File

@@ -1,258 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret
import (
"bytes"
"context"
"errors"
"time"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
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/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/pkg/cert"
"github.com/clastix/capsule/pkg/configuration"
)
type CAReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Namespace string
Configuration configuration.Configuration
}
func (r *CAReconciler) SetupWithManager(mgr ctrl.Manager) error {
enqueueFn := handler.EnqueueRequestsFromMapFunc(func(client.Object) []reconcile.Request {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: r.Namespace,
Name: r.Configuration.CASecretName(),
},
},
}
})
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
Watches(source.NewKindWithCache(&admissionregistrationv1.ValidatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.Configuration.ValidatingWebhookConfigurationName()
}))).
Watches(source.NewKindWithCache(&admissionregistrationv1.MutatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.Configuration.MutatingWebhookConfigurationName()
}))).
Complete(r)
}
// By default helm doesn't allow to use templates in CRD (https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you).
// In order to overcome this, we are setting conversion strategy in helm chart to None, and then update it with CA and namespace information.
func (r *CAReconciler) UpdateCustomResourceDefinition(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
crd := &apiextensionsv1.CustomResourceDefinition{}
err = r.Get(ctx, types.NamespacedName{Name: "tenants.capsule.clastix.io"}, crd)
if err != nil {
r.Log.Error(err, "cannot retrieve CustomResourceDefinition")
return err
}
_, err = controllerutil.CreateOrUpdate(ctx, 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(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
vw := &admissionregistrationv1.ValidatingWebhookConfiguration{}
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.ValidatingWebhookConfigurationName()}, vw)
if err != nil {
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
return err
}
for i, w := range vw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
vw.Webhooks[i].ClientConfig.CABundle = caBundle
}
}
return r.Update(ctx, vw, &client.UpdateOptions{})
})
}
//nolint:dupl
func (r CAReconciler) UpdateMutatingWebhookConfiguration(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
mw := &admissionregistrationv1.MutatingWebhookConfiguration{}
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.MutatingWebhookConfigurationName()}, mw)
if err != nil {
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
return err
}
for i, w := range mw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
mw.Webhooks[i].ClientConfig.CABundle = caBundle
}
}
return r.Update(ctx, mw, &client.UpdateOptions{})
})
}
func (r CAReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
var err error
if request.Name != r.Configuration.CASecretName() {
return ctrl.Result{}, nil
}
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.Log.Info("Reconciling CA Secret")
// Fetch the CA instance
instance := &corev1.Secret{}
if err = r.Client.Get(ctx, request.NamespacedName, instance); err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.CA
var rq time.Duration
ca, err = getCertificateAuthority(ctx, r.Client, r.Namespace, r.Configuration.CASecretName())
if err != nil && errors.Is(err, MissingCaError{}) {
ca, err = cert.GenerateCertificateAuthority()
if err != nil {
return reconcile.Result{}, err
}
} else if err != nil {
return reconcile.Result{}, err
}
r.Log.Info("Handling CA Secret")
rq, err = ca.ExpiresIn(time.Now())
if err != nil {
r.Log.Info("CA is expired, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
} else {
r.Log.Info("Updating CA secret with new PEM and RSA")
var crt *bytes.Buffer
var key *bytes.Buffer
crt, _ = ca.CACertificatePem()
key, _ = ca.CAPrivateKeyPem()
instance.Data = map[string][]byte{
corev1.TLSCertKey: crt.Bytes(),
corev1.TLSPrivateKeyKey: key.Bytes(),
}
group := new(errgroup.Group)
group.Go(func() error {
return r.UpdateMutatingWebhookConfiguration(ctx, crt.Bytes())
})
group.Go(func() error {
return r.UpdateValidatingWebhookConfiguration(ctx, crt.Bytes())
})
group.Go(func() error {
return r.UpdateCustomResourceDefinition(ctx, crt.Bytes())
})
if err = group.Wait(); err != nil {
return reconcile.Result{}, err
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.Log.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
if res == controllerutil.OperationResultUpdated {
r.Log.Info("Capsule CA has been updated, we need to trigger TLS update too")
tls := &corev1.Secret{}
err = r.Get(ctx, types.NamespacedName{
Namespace: r.Namespace,
Name: r.Configuration.TLSSecretName(),
}, tls)
if err != nil {
r.Log.Error(err, "Capsule TLS Secret missing")
}
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tls, func() error {
tls.Data = map[string][]byte{}
return nil
})
return err
})
if err != nil {
r.Log.Error(err, "Cannot clean Capsule TLS Secret due to CA update")
return reconcile.Result{}, err
}
}
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

View File

@@ -1,10 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret
type MissingCaError struct{}
func (MissingCaError) Error() string {
return "CA has not been created yet, please generate a new"
}

View File

@@ -1,34 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/clastix/capsule/pkg/cert"
)
func getCertificateAuthority(ctx context.Context, client client.Client, namespace, name string) (ca cert.CA, err error) {
instance := &corev1.Secret{}
if err = client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, instance); err != nil {
return nil, fmt.Errorf("missing secret %s, cannot reconcile", name)
}
if instance.Data == nil {
return nil, MissingCaError{}
}
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[corev1.TLSCertKey], instance.Data[corev1.TLSPrivateKeyKey])
if err != nil {
return
}
return
}

View File

@@ -1,172 +0,0 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package secret
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
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"
"github.com/clastix/capsule/pkg/cert"
"github.com/clastix/capsule/pkg/configuration"
)
type TLSReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Namespace string
Configuration configuration.Configuration
}
func (r *TLSReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
Complete(r)
}
func (r TLSReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
var err error
if request.Name != r.Configuration.TLSSecretName() {
return ctrl.Result{}, nil
}
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
r.Log.Info("Reconciling TLS Secret")
// Fetch the Secret instance
instance := &corev1.Secret{}
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
var ca cert.CA
var rq time.Duration
ca, err = getCertificateAuthority(ctx, r.Client, r.Namespace, r.Configuration.CASecretName())
if err != nil {
return reconcile.Result{}, err
}
var shouldCreate bool
for _, key := range []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey} {
if _, ok := instance.Data[key]; !ok {
shouldCreate = true
break
}
}
if shouldCreate {
r.Log.Info("Missing Capsule TLS certificate")
rq = 6 * 30 * 24 * time.Hour
opts := cert.NewCertOpts(time.Now().Add(rq), fmt.Sprintf("capsule-webhook-service.%s.svc", r.Namespace))
var crt, key *bytes.Buffer
if crt, key, err = ca.GenerateCertificate(opts); err != nil {
r.Log.Error(err, "Cannot generate new TLS certificate")
return reconcile.Result{}, err
}
instance.Data = map[string][]byte{
corev1.TLSCertKey: crt.Bytes(),
corev1.TLSPrivateKeyKey: key.Bytes(),
}
} else {
var c *x509.Certificate
var b *pem.Block
b, _ = pem.Decode(instance.Data[corev1.TLSCertKey])
c, err = x509.ParseCertificate(b.Bytes)
if err != nil {
r.Log.Error(err, "cannot parse Capsule TLS")
return reconcile.Result{}, err
}
rq = time.Until(c.NotAfter)
err = ca.ValidateCert(c)
if err != nil {
r.Log.Info("Capsule TLS is expired or invalid, cleaning to obtain a new one")
instance.Data = map[string][]byte{}
}
}
var res controllerutil.OperationResult
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, t, func() error {
t.Data = instance.Data
return nil
})
if err != nil {
r.Log.Error(err, "cannot update Capsule TLS")
return reconcile.Result{}, err
}
// nolint:nestif
if instance.Name == r.Configuration.TLSSecretName() && res == controllerutil.OperationResultUpdated {
r.Log.Info("Capsule TLS certificates has been updated, Controller pods must be restarted to load new certificate")
hostname, _ := os.Hostname()
leaderPod := &corev1.Pod{}
if err = r.Client.Get(ctx, types.NamespacedName{Namespace: os.Getenv("NAMESPACE"), Name: hostname}, leaderPod); err != nil {
r.Log.Error(err, "cannot retrieve the leader Pod, probably running in out of the cluster mode")
return reconcile.Result{}, nil
}
podList := &corev1.PodList{}
if err = r.Client.List(ctx, podList, client.MatchingLabels(leaderPod.ObjectMeta.Labels)); err != nil {
r.Log.Error(err, "cannot retrieve list of Capsule pods requiring restart upon TLS update")
return reconcile.Result{}, nil
}
for _, p := range podList.Items {
nonLeaderPod := p
// Skipping this Pod, must be deleted at the end
if nonLeaderPod.GetName() == leaderPod.GetName() {
continue
}
if err = r.Client.Delete(ctx, &nonLeaderPod); err != nil {
r.Log.Error(err, "cannot delete the non-leader Pod due to TLS update")
}
}
if err = r.Client.Delete(ctx, leaderPod); err != nil {
r.Log.Error(err, "cannot delete the leader Pod due to TLS update")
}
}
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}

View File

@@ -12,7 +12,6 @@ import (
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -28,7 +27,6 @@ type abstractServiceLabelsReconciler struct {
obj client.Object
client client.Client
log logr.Logger
scheme *runtime.Scheme
}
func (r *abstractServiceLabelsReconciler) InjectClient(c client.Client) error {

View File

@@ -61,8 +61,8 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta1.T
// getting requested Role Binding keys
keys := make([]string, 0, len(tenant.Spec.Owners))
// Generating for dynamic tenant owners cluster roles
for _, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetRoles(*tenant) {
for index, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetRoles(*tenant, index) {
cr := r.ownerClusterRoleBindings(owner, clusterRoleName)
keys = append(keys, hashFn(cr))
@@ -103,8 +103,8 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
var roleBindings []capsulev1beta1.AdditionalRoleBindingsSpec
for _, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetRoles(*tenant) {
for index, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetRoles(*tenant, index) {
roleBindings = append(roleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName))
}
}

10
controllers/tls/errors.go Normal file
View File

@@ -0,0 +1,10 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tls
type RunningInOutOfClusterModeError struct{}
func (r RunningInOutOfClusterModeError) Error() string {
return "cannot retrieve the leader Pod, probably running in out of the cluster mode"
}

347
controllers/tls/manager.go Normal file
View File

@@ -0,0 +1,347 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package tls
import (
"context"
"fmt"
"os"
"time"
"github.com/go-logr/logr"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"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/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/clastix/capsule/controllers/utils"
"github.com/clastix/capsule/pkg/cert"
"github.com/clastix/capsule/pkg/configuration"
)
const (
certificateExpirationThreshold = 3 * 24 * time.Hour
certificateValidity = 6 * 30 * 24 * time.Hour
PodUpdateAnnotationName = "capsule.clastix.io/updated"
)
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Namespace string
Configuration configuration.Configuration
}
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
enqueueFn := handler.EnqueueRequestsFromMapFunc(func(client.Object) []reconcile.Request {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: r.Namespace,
Name: r.Configuration.TLSSecretName(),
},
},
}
})
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, utils.NamesMatchingPredicate(r.Configuration.TLSSecretName())).
Watches(source.NewKindWithCache(&admissionregistrationv1.ValidatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.Configuration.ValidatingWebhookConfigurationName()
}))).
Watches(source.NewKindWithCache(&admissionregistrationv1.MutatingWebhookConfiguration{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.Configuration.MutatingWebhookConfigurationName()
}))).
Watches(source.NewKindWithCache(&apiextensionsv1.CustomResourceDefinition{}, mgr.GetCache()), enqueueFn, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.Configuration.TenantCRDName()
}))).
Complete(r)
}
func (r Reconciler) ReconcileCertificates(ctx context.Context, certSecret *corev1.Secret) error {
if r.shouldUpdateCertificate(certSecret) {
r.Log.Info("Generating new TLS certificate")
ca, err := cert.GenerateCertificateAuthority()
if err != nil {
return err
}
opts := cert.NewCertOpts(time.Now().Add(certificateValidity), fmt.Sprintf("capsule-webhook-service.%s.svc", r.Namespace))
crt, key, err := ca.GenerateCertificate(opts)
if err != nil {
r.Log.Error(err, "Cannot generate new TLS certificate")
return err
}
caCrt, _ := ca.CACertificatePem()
certSecret.Data = map[string][]byte{
corev1.TLSCertKey: crt.Bytes(),
corev1.TLSPrivateKeyKey: key.Bytes(),
corev1.ServiceAccountRootCAKey: caCrt.Bytes(),
}
t := &corev1.Secret{ObjectMeta: certSecret.ObjectMeta}
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, t, func() error {
t.Data = certSecret.Data
return nil
})
if err != nil {
r.Log.Error(err, "cannot update Capsule TLS")
return err
}
}
var caBundle []byte
var ok bool
if caBundle, ok = certSecret.Data[corev1.ServiceAccountRootCAKey]; !ok {
return fmt.Errorf("missing %s field in %s secret", corev1.ServiceAccountRootCAKey, r.Configuration.TLSSecretName())
}
r.Log.Info("Updating caBundle in webhooks and crd")
group := new(errgroup.Group)
group.Go(func() error {
return r.updateMutatingWebhookConfiguration(ctx, caBundle)
})
group.Go(func() error {
return r.updateValidatingWebhookConfiguration(ctx, caBundle)
})
group.Go(func() error {
return r.updateCustomResourceDefinition(ctx, caBundle)
})
operatorPods, err := r.getOperatorPods(ctx)
if err != nil {
if errors.As(err, &RunningInOutOfClusterModeError{}) {
r.Log.Info("skipping annotation of Pods for cert-manager", "error", err.Error())
return nil
}
return err
}
r.Log.Info("Updating capsule operator pods")
for _, pod := range operatorPods.Items {
p := pod
group.Go(func() error {
return r.updateOperatorPod(ctx, p)
})
}
if err := group.Wait(); err != nil {
return err
}
return nil
}
func (r Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
r.Log = r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
certSecret := &corev1.Secret{}
if err := r.Client.Get(ctx, request.NamespacedName, certSecret); err != nil {
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
if err := r.ReconcileCertificates(ctx, certSecret); err != nil {
return reconcile.Result{}, err
}
if r.Configuration.GenerateCertificates() {
certificate, err := cert.GetCertificateFromBytes(certSecret.Data[corev1.TLSCertKey])
if err != nil {
return reconcile.Result{}, err
}
now := time.Now()
requeueTime := certificate.NotAfter.Add(-(certificateExpirationThreshold - 1*time.Second))
rq := requeueTime.Sub(now)
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}
return reconcile.Result{}, nil
}
func (r Reconciler) shouldUpdateCertificate(secret *corev1.Secret) bool {
if !r.Configuration.GenerateCertificates() {
r.Log.Info("Skipping TLS certificate generation as it is disabled in CapsuleConfiguration")
return false
}
if _, ok := secret.Data[corev1.ServiceAccountRootCAKey]; !ok {
return true
}
certificate, key, err := cert.GetCertificateWithPrivateKeyFromBytes(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey])
if err != nil {
return true
}
if err := cert.ValidateCertificate(certificate, key, certificateExpirationThreshold); err != nil {
r.Log.Error(err, "failed to validate certificate, generating new one")
return true
}
r.Log.Info("Skipping TLS certificate generation as it is still valid")
return false
}
// 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 *Reconciler) updateCustomResourceDefinition(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
crd := &apiextensionsv1.CustomResourceDefinition{}
err = r.Get(ctx, types.NamespacedName{Name: "tenants.capsule.clastix.io"}, crd)
if err != nil {
r.Log.Error(err, "cannot retrieve CustomResourceDefinition")
return err
}
_, err = controllerutil.CreateOrUpdate(ctx, 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 Reconciler) updateValidatingWebhookConfiguration(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
vw := &admissionregistrationv1.ValidatingWebhookConfiguration{}
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.ValidatingWebhookConfigurationName()}, vw)
if err != nil {
r.Log.Error(err, "cannot retrieve ValidatingWebhookConfiguration")
return err
}
for i, w := range vw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
vw.Webhooks[i].ClientConfig.CABundle = caBundle
}
}
return r.Update(ctx, vw, &client.UpdateOptions{})
})
}
//nolint:dupl
func (r Reconciler) updateMutatingWebhookConfiguration(ctx context.Context, caBundle []byte) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
mw := &admissionregistrationv1.MutatingWebhookConfiguration{}
err = r.Get(ctx, types.NamespacedName{Name: r.Configuration.MutatingWebhookConfigurationName()}, mw)
if err != nil {
r.Log.Error(err, "cannot retrieve MutatingWebhookConfiguration")
return err
}
for i, w := range mw.Webhooks {
// Updating CABundle only in case of an internal service reference
if w.ClientConfig.Service != nil {
mw.Webhooks[i].ClientConfig.CABundle = caBundle
}
}
return r.Update(ctx, mw, &client.UpdateOptions{})
})
}
func (r Reconciler) updateOperatorPod(ctx context.Context, pod corev1.Pod) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
// Need to get latest version of pod
p := &corev1.Pod{}
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, p); err != nil && !apierrors.IsNotFound(err) {
r.Log.Error(err, "cannot get pod", "name", pod.Name, "namespace", pod.Namespace)
return err
}
if p.Annotations == nil {
p.Annotations = map[string]string{}
}
p.Annotations[PodUpdateAnnotationName] = time.Now().Format(time.RFC3339Nano)
if err := r.Client.Update(ctx, p, &client.UpdateOptions{}); err != nil {
r.Log.Error(err, "cannot update pod", "name", pod.Name, "namespace", pod.Namespace)
return err
}
return nil
})
}
func (r Reconciler) getOperatorPods(ctx context.Context) (*corev1.PodList, error) {
hostname, _ := os.Hostname()
leaderPod := &corev1.Pod{}
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: os.Getenv("NAMESPACE"), Name: hostname}, leaderPod); err != nil {
return nil, RunningInOutOfClusterModeError{}
}
podList := &corev1.PodList{}
if err := r.Client.List(ctx, podList, client.MatchingLabels(leaderPod.ObjectMeta.Labels)); err != nil {
r.Log.Error(err, "cannot retrieve list of Capsule pods")
return nil, err
}
return podList, nil
}

View File

@@ -125,12 +125,12 @@ $ go mod download
$ make docker-build
# Retrieve the built image version
$ export CAPSULE_IMAGE_VESION=`docker images --format '{{.Tag}}' quay.io/clastix/capsule`
$ export CAPSULE_IMAGE_VESION=`docker images --format '{{.Tag}}' clastix/capsule`
# If k3s, load the image into cluster by
$ k3d image import --cluster k3s-capsule capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
$ k3d image import --cluster k3s-capsule capsule clastix/capsule:${CAPSULE_IMAGE_VESION}
# If Kind, load the image into cluster by
$ kind load docker-image --name kind-capsule quay.io/clastix/capsule:${CAPSULE_IMAGE_VESION}
$ kind load docker-image --name kind-capsule clastix/capsule:${CAPSULE_IMAGE_VESION}
# deploy all the required manifests
# Note: 1) please retry if you saw errors; 2) if you want to clean it up first, run: make remove

View File

@@ -30,7 +30,7 @@ The `capsule-proxy` implements a simple reverse proxy that intercepts only speci
Current implementation filters the following requests:
* `/api/scheduling.k8s.io/{v1}/priorityclasses{/name}`
* `/api/v1/namespaces`
* `/api/v1/namespaces{/name}`
* `/api/v1/nodes{/name}`
* `/api/v1/pods?fieldSelector=spec.nodeName%3D{name}`
* `/apis/coordination.k8s.io/v1/namespaces/kube-node-lease/leases/{name}`
@@ -151,6 +151,21 @@ oil-development Active 2m
oil-production Active 2m
```
Capsule Proxy supports applying a Namespace configuration using the `apply` command, as follows.
```
$: cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: solar-development
EOF
namespace/solar-development unchanged
# or, in case of non existing Namespace:
namespace/solar-development created
```
### Nodes
The Capsule Proxy gives the owners the ability to access the nodes matching the `.spec.nodeSelector` in the Tenant manifest:

View File

@@ -24,8 +24,11 @@ By default, all Tenant Owners will be granted with two ClusterRole resources usi
1. the Kubernetes default one, `admin`, that grants most of the Namespace scoped resources management operations
2. a custom one, named `capsule-namespace-deleter`, allowing to delete the created Namespace
In the example below, assuming Alice create a namespace `oil-production` in Tenant `oil`,getting the tenant owner's Alice default
ClusterRoles command:
```
$: kubectl get rolebindings.rbac.authorization.k8s.io
$: kubectl get rolebindings.rbac.authorization.k8s.io -n oil-production
NAME ROLE AGE
capsule-oil-0-admin ClusterRole/admin 6s
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
@@ -63,6 +66,41 @@ capsule-oil-2-readonly ClusterRole/readonly 2s
> The pattern for the annotation is `clusterrolenames.capsule.clastix.io/${KIND}.${NAME}`.
> The placeholders `${KIND}` and `${NAME}` are referring to the Tenant Owner specification fields, both lower-cased.
>
> In the case of users that are identified using their email address, the symbol `@` wouldn't be supported by the RFC 1123.
> For such cases, the `@` symbol can be replaced with the placeholder `__AT__`.
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/alice__AT__clastix.io: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: alice@clastix.io
> ```
>
> Instead, with the resulting annotation key exceeding 63 characters length, the zero-based index of the owner can be specified as follows:
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/1: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: very-long-user-name-that-breaks-rfc-1123@org.tld
> ```
>
> This latter example will assign the roles `editor` and `manager`, assigned to the user `very-long-user-name-that-breaks-rfc-1123@org.tld`.
### User as tenant owner
Bill, the cluster admin, receives a new request from Acme Corp.'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the Acme Corp. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.

View File

@@ -0,0 +1,5 @@
# Install Capsule on Charmed Kubernetes distribution
[Canonical Charmed Kubernetes](https://github.com/charmed-kubernetes) is a Kubernetes distribution coming with out-of-the-box tools that support deployments and operational management and make microservice development easier. Combined with Capsule, Charmed Kubernetes allows users to further reduce the operational overhead of Kubernetes setup and management.
The Charm package for Capsule is available to Charmed Kubernetes users via [Charmhub.io](https://charmhub.io/capsule-k8s).

View File

@@ -5,7 +5,7 @@
## What's the problem with the current status?
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well know phenomena of the _clusters sprawl_.
Kubernetes introduces the _Namespace_ object type to create logical partitions of the cluster as isolated *slices*. However, implementing advanced multi-tenancy scenarios, it soon becomes complicated because of the flat structure of Kubernetes namespaces and the impossibility to share resources among namespaces belonging to the same tenant. To overcome this, cluster admins tend to provision a dedicated cluster for each groups of users, teams, or departments. As an organization grows, the number of clusters to manage and keep aligned becomes an operational nightmare, described as the well known phenomena of the _clusters sprawl_.
## Entering Capsule

View File

@@ -70,6 +70,10 @@ module.exports = function (api) {
label: 'Upgrading Tenant version',
path: '/docs/guides/upgrading'
},
{
label: 'Install on Charmed Kubernetes',
path: '/docs/guides/charmed'
},
{
title: 'Managed Kubernetes',
subItems: [

34
docs/package-lock.json generated
View File

@@ -4474,7 +4474,7 @@
"defined": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
"integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==",
"dev": true
},
"delayed-stream": {
@@ -4530,14 +4530,14 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="
},
"detective": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
"integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz",
"integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==",
"dev": true,
"requires": {
"acorn-node": "^1.6.1",
"acorn-node": "^1.8.2",
"defined": "^1.0.0",
"minimist": "^1.1.1"
"minimist": "^1.2.6"
}
},
"didyoumean": {
@@ -4976,9 +4976,9 @@
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"eventsource": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz",
"integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==",
"requires": {
"original": "^1.0.0"
}
@@ -7807,7 +7807,7 @@
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
"integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==",
"dev": true
},
"lodash.uniq": {
@@ -10830,9 +10830,9 @@
"dev": true
},
"nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
"picocolors": {
@@ -10842,12 +10842,12 @@
"dev": true
},
"postcss": {
"version": "8.4.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
"integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==",
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"dev": true,
"requires": {
"nanoid": "^3.3.1",
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}

View File

@@ -3710,9 +3710,9 @@ events@^3.0.0:
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource@^1.0.7:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf"
integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==
version "1.1.1"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f"
integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==
dependencies:
original "^1.0.0"

View File

@@ -11,6 +11,7 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
@@ -33,6 +34,13 @@ var _ = Describe("Deleting a tenant with protected annotation", func() {
},
}
JustAfterEach(func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
tnt.SetAnnotations(map[string]string{})
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should fail", func() {
Expect(k8sClient.Create(context.TODO(), tnt)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), tnt)).ShouldNot(Succeed())

2
go.mod
View File

@@ -51,7 +51,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect

2
go.sum
View File

@@ -696,6 +696,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=

227
main.go
View File

@@ -11,12 +11,12 @@ import (
flag "github.com/spf13/pflag"
"go.uber.org/zap/zapcore"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
utilVersion "k8s.io/apimachinery/pkg/util/version"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
@@ -28,9 +28,9 @@ import (
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
configcontroller "github.com/clastix/capsule/controllers/config"
rbaccontroller "github.com/clastix/capsule/controllers/rbac"
secretcontroller "github.com/clastix/capsule/controllers/secret"
servicelabelscontroller "github.com/clastix/capsule/controllers/servicelabels"
tenantcontroller "github.com/clastix/capsule/controllers/tenant"
tlscontroller "github.com/clastix/capsule/controllers/tls"
"github.com/clastix/capsule/pkg/configuration"
"github.com/clastix/capsule/pkg/indexer"
"github.com/clastix/capsule/pkg/webhook"
@@ -70,15 +70,13 @@ func printVersion() {
// nolint:maintidx
func main() {
var enableLeaderElection, enableSecretController, version bool
var enableLeaderElection, version bool
var metricsAddr, namespace, configurationName string
var goFlagSet goflag.FlagSet
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.BoolVar(&enableSecretController, "enable-secret-controller", true,
"Enable secret controller which reconciles TLS and CA secrets for capsule webhooks.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
@@ -133,34 +131,6 @@ func main() {
cfg := configuration.NewCapsuleConfiguration(ctx, manager.GetClient(), configurationName)
if enableSecretController {
if err = (&secretcontroller.CAReconciler{
Client: manager.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("CA"),
Namespace: namespace,
Configuration: cfg,
}).SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
if err = (&secretcontroller.TLSReconciler{
Client: manager.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tls"),
Namespace: namespace,
Configuration: cfg,
}).SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
}
clientset, err := kubernetes.NewForConfig(ctrl.GetConfigOrDie())
if err != nil {
setupLog.Error(err, "unable to create kubernetes clientset")
os.Exit(1)
}
directClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{
Scheme: manager.GetScheme(),
Mapper: manager.GetRESTMapper(),
@@ -172,116 +142,125 @@ func main() {
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, configurationName)
ca, err := clientset.CoreV1().Secrets(namespace).Get(ctx, directCfg.CASecretName(), metav1.GetOptions{})
if err != nil {
setupLog.Error(err, "unable to get Capsule CA secret")
tlsReconciler := &tlscontroller.Reconciler{
Client: directClient,
Log: ctrl.Log.WithName("controllers").WithName("TLS"),
Namespace: namespace,
Configuration: directCfg,
}
if err = tlsReconciler.SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
tls, err := clientset.CoreV1().Secrets(namespace).Get(ctx, directCfg.TLSSecretName(), metav1.GetOptions{})
if err != nil {
tlsCert := &corev1.Secret{}
if err = directClient.Get(ctx, types.NamespacedName{Namespace: namespace, Name: directCfg.TLSSecretName()}, tlsCert); err != nil {
setupLog.Error(err, "unable to get Capsule TLS secret")
os.Exit(1)
}
// nolint:nestif
if len(ca.Data) > 0 && len(tls.Data) > 0 {
if err = (&tenantcontroller.Manager{
RESTConfig: manager.GetConfig(),
Client: manager.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Recorder: manager.GetEventRecorderFor("tenant-controller"),
}).SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
os.Exit(1)
}
if err = (&capsulev1alpha1.Tenant{}).SetupWebhookWithManager(manager); err != nil {
setupLog.Error(err, "unable to create conversion webhook", "webhook", "Tenant")
os.Exit(1)
}
// Reconcile TLS certificates before starting controllers and webhooks
if err = tlsReconciler.ReconcileCertificates(ctx, tlsCert); err != nil {
setupLog.Error(err, "unable to reconcile Capsule TLS secret")
os.Exit(1)
}
if err = indexer.AddToManager(ctx, setupLog, manager); err != nil {
setupLog.Error(err, "unable to setup indexers")
os.Exit(1)
}
if err = (&tenantcontroller.Manager{
RESTConfig: manager.GetConfig(),
Client: manager.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Recorder: manager.GetEventRecorderFor("tenant-controller"),
}).SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
os.Exit(1)
}
var kubeVersion *utilVersion.Version
if err = (&capsulev1alpha1.Tenant{}).SetupWebhookWithManager(manager); err != nil {
setupLog.Error(err, "unable to create conversion webhook", "webhook", "Tenant")
os.Exit(1)
}
if kubeVersion, err = utils.GetK8sVersion(); err != nil {
setupLog.Error(err, "unable to get kubernetes version")
os.Exit(1)
}
if err = indexer.AddToManager(ctx, setupLog, manager); err != nil {
setupLog.Error(err, "unable to setup indexers")
os.Exit(1)
}
// webhooks: the order matters, don't change it and just append
webhooksList := append(
make([]webhook.Webhook, 0),
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass()),
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg), namespacewebhook.UserMetadataHandler())),
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
route.PVC(pvc.Handler()),
route.Service(service.Handler()),
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler()),
route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()),
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
)
var kubeVersion *utilVersion.Version
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
if !nodeWebhookSupported {
setupLog.Info("Disabling node labels verification webhook as current Kubernetes version doesn't have fix for CVE-2021-25735")
}
if kubeVersion, err = utils.GetK8sVersion(); err != nil {
setupLog.Error(err, "unable to get kubernetes version")
os.Exit(1)
}
if err = webhook.Register(manager, webhooksList...); err != nil {
setupLog.Error(err, "unable to setup webhooks")
os.Exit(1)
}
// webhooks: the order matters, don't change it and just append
webhooksList := append(
make([]webhook.Webhook, 0),
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass()),
route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.PatchHandler(), namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg), namespacewebhook.UserMetadataHandler())),
route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
route.PVC(pvc.Handler()),
route.Service(service.Handler()),
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler()),
route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()),
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
)
rbacManager := &rbaccontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("Rbac"),
Configuration: cfg,
}
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
if !nodeWebhookSupported {
setupLog.Info("Disabling node labels verification webhook as current Kubernetes version doesn't have fix for CVE-2021-25735")
}
if err = manager.Add(rbacManager); err != nil {
setupLog.Error(err, "unable to create cluster roles")
os.Exit(1)
}
if err = webhook.Register(manager, webhooksList...); err != nil {
setupLog.Error(err, "unable to setup webhooks")
os.Exit(1)
}
if err = rbacManager.SetupWithManager(ctx, manager, configurationName); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Rbac")
os.Exit(1)
}
rbacManager := &rbaccontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("Rbac"),
Configuration: cfg,
}
if err = (&servicelabelscontroller.ServicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("ServiceLabels"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServiceLabels")
os.Exit(1)
}
if err = manager.Add(rbacManager); err != nil {
setupLog.Error(err, "unable to create cluster roles")
os.Exit(1)
}
if err = (&servicelabelscontroller.EndpointsLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("EndpointLabels"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "EndpointLabels")
os.Exit(1)
}
if err = rbacManager.SetupWithManager(ctx, manager, configurationName); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Rbac")
os.Exit(1)
}
if err = (&servicelabelscontroller.EndpointSlicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("EndpointSliceLabels"),
VersionMinor: kubeVersion.Minor(),
VersionMajor: kubeVersion.Major(),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "EndpointSliceLabels")
}
if err = (&servicelabelscontroller.ServicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("ServiceLabels"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServiceLabels")
os.Exit(1)
}
if err = (&configcontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
}).SetupWithManager(manager, configurationName); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
os.Exit(1)
}
} else {
setupLog.Info("skip registering a tenant controller, missing CA secret")
if err = (&servicelabelscontroller.EndpointsLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("EndpointLabels"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "EndpointLabels")
os.Exit(1)
}
if err = (&servicelabelscontroller.EndpointSlicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("EndpointSliceLabels"),
VersionMinor: kubeVersion.Minor(),
VersionMajor: kubeVersion.Major(),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "EndpointSliceLabels")
}
if err = (&configcontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
}).SetupWithManager(manager, configurationName); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
os.Exit(1)
}
setupLog.Info("starting manager")

View File

@@ -12,6 +12,8 @@ import (
"encoding/pem"
"math/big"
"time"
"github.com/pkg/errors"
)
type CA interface {
@@ -27,38 +29,6 @@ type CapsuleCA struct {
key *rsa.PrivateKey
}
func (c CapsuleCA) ValidateCert(certificate *x509.Certificate) (err error) {
pool := x509.NewCertPool()
pool.AddCert(c.certificate)
_, err = certificate.Verify(x509.VerifyOptions{
Roots: pool,
CurrentTime: time.Time{},
})
return
}
func (c CapsuleCA) isAlreadyValid(now time.Time) bool {
return now.After(c.certificate.NotBefore)
}
func (c CapsuleCA) isExpired(now time.Time) bool {
return now.Before(c.certificate.NotAfter)
}
func (c CapsuleCA) ExpiresIn(now time.Time) (time.Duration, error) {
if !c.isExpired(now) {
return time.Nanosecond, CaExpiredError{}
}
if !c.isAlreadyValid(now) {
return time.Nanosecond, CaNotYetValidError{}
}
return time.Duration(c.certificate.NotAfter.Unix()-now.Unix()) * time.Second, nil
}
func (c CapsuleCA) CACertificatePem() (b *bytes.Buffer, err error) {
var crtBytes []byte
crtBytes, err = x509.CreateCertificate(rand.Reader, c.certificate, c.certificate, &c.key.PublicKey, c.key)
@@ -85,6 +55,24 @@ func (c CapsuleCA) CAPrivateKeyPem() (b *bytes.Buffer, err error) {
})
}
func ValidateCertificate(cert *x509.Certificate, key *rsa.PrivateKey, expirationThreshold time.Duration) error {
if !key.PublicKey.Equal(cert.PublicKey) {
return errors.New("certificate signed by wrong public key")
}
now := time.Now()
if now.Before(cert.NotBefore) {
return errors.New("certificate is not valid yet")
}
if now.After(cert.NotAfter.Add(-expirationThreshold)) {
return errors.New("certificate expired or going to expire soon")
}
return nil
}
func GenerateCertificateAuthority() (s *CapsuleCA, err error) {
s = &CapsuleCA{
certificate: &x509.Certificate{
@@ -114,31 +102,46 @@ func GenerateCertificateAuthority() (s *CapsuleCA, err error) {
return
}
func NewCertificateAuthorityFromBytes(certBytes, keyBytes []byte) (s *CapsuleCA, err error) {
func GetCertificateFromBytes(certBytes []byte) (*x509.Certificate, error) {
var b *pem.Block
b, _ = pem.Decode(certBytes)
var cert *x509.Certificate
return x509.ParseCertificate(b.Bytes)
}
if cert, err = x509.ParseCertificate(b.Bytes); err != nil {
return
}
func GetPrivateKeyFromBytes(keyBytes []byte) (*rsa.PrivateKey, error) {
var b *pem.Block
b, _ = pem.Decode(keyBytes)
var key *rsa.PrivateKey
return x509.ParsePKCS1PrivateKey(b.Bytes)
}
if key, err = x509.ParsePKCS1PrivateKey(b.Bytes); err != nil {
return
func GetCertificateWithPrivateKeyFromBytes(certBytes, keyBytes []byte) (*x509.Certificate, *rsa.PrivateKey, error) {
cert, err := GetCertificateFromBytes(certBytes)
if err != nil {
return nil, nil, err
}
s = &CapsuleCA{
key, err := GetPrivateKeyFromBytes(keyBytes)
if err != nil {
return nil, nil, err
}
return cert, key, nil
}
func NewCertificateAuthorityFromBytes(certBytes, keyBytes []byte) (*CapsuleCA, error) {
cert, key, err := GetCertificateWithPrivateKeyFromBytes(certBytes, keyBytes)
if err != nil {
return nil, err
}
return &CapsuleCA{
certificate: cert,
key: key,
}
return
}, nil
}
// nolint:nakedret

View File

@@ -74,40 +74,3 @@ func TestCapsuleCa_GenerateCertificate(t *testing.T) {
})
}
}
func TestCapsuleCa_IsValid(t *testing.T) {
type testCase struct {
notBefore time.Time
notAfter time.Time
returnError bool
}
tc := map[string]testCase{
"ok": {time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 1), false},
"expired": {time.Now().AddDate(1, 0, 0), time.Now(), true},
"notValid": {time.Now().AddDate(0, 0, 1), time.Now().AddDate(0, 0, 2), true},
}
for name, c := range tc {
t.Run(name, func(t *testing.T) {
var ca *CapsuleCA
var err error
ca, err = GenerateCertificateAuthority()
assert.Nil(t, err)
ca.certificate.NotAfter = c.notAfter
ca.certificate.NotBefore = c.notBefore
var w time.Duration
w, err = ca.ExpiresIn(time.Now())
if c.returnError {
assert.Error(t, err)
return
}
assert.Nil(t, err)
assert.WithinDuration(t, ca.certificate.NotAfter, time.Now().Add(w), time.Minute)
})
}
}

View File

@@ -6,6 +6,7 @@ package configuration
import (
"context"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
@@ -62,21 +63,6 @@ func (c capsuleConfiguration) ForceTenantPrefix() bool {
return c.retrievalFn().Spec.ForceTenantPrefix
}
func (c capsuleConfiguration) CASecretName() (name string) {
name = CASecretName
if c.retrievalFn().Annotations == nil {
return
}
v, ok := c.retrievalFn().Annotations[capsulev1alpha1.CASecretNameAnnotation]
if ok {
return v
}
return
}
func (c capsuleConfiguration) TLSSecretName() (name string) {
name = TLSSecretName
@@ -92,6 +78,21 @@ func (c capsuleConfiguration) TLSSecretName() (name string) {
return
}
func (c capsuleConfiguration) GenerateCertificates() bool {
annotationValue, ok := c.retrievalFn().Annotations[capsulev1alpha1.GenerateCertificatesAnnotationName]
if ok {
value, err := strconv.ParseBool(annotationValue)
if err != nil {
return false
}
return value
}
return false
}
func (c capsuleConfiguration) MutatingWebhookConfigurationName() (name string) {
name = MutatingWebhookConfigurationName
@@ -107,6 +108,10 @@ func (c capsuleConfiguration) MutatingWebhookConfigurationName() (name string) {
return
}
func (c capsuleConfiguration) TenantCRDName() string {
return TenantCRDName
}
func (c capsuleConfiguration) ValidatingWebhookConfigurationName() (name string) {
name = ValidatingWebhookConfigurationName

View File

@@ -10,19 +10,20 @@ import (
)
const (
CASecretName = "capsule-ca"
TLSSecretName = "capsule-tls"
MutatingWebhookConfigurationName = "capsule-mutating-webhook-configuration"
ValidatingWebhookConfigurationName = "capsule-validating-webhook-configuration"
TenantCRDName = "tenants.capsule.clastix.io"
)
type Configuration interface {
ProtectedNamespaceRegexp() (*regexp.Regexp, error)
ForceTenantPrefix() bool
CASecretName() string
GenerateCertificates() bool
TLSSecretName() string
MutatingWebhookConfigurationName() string
ValidatingWebhookConfigurationName() string
TenantCRDName() string
UserGroups() []string
ForbiddenUserNodeLabels() *capsulev1beta1.ForbiddenListSpec
ForbiddenUserNodeAnnotations() *capsulev1beta1.ForbiddenListSpec

View File

@@ -0,0 +1,82 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package namespace
import (
"context"
"fmt"
"net/http"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
"github.com/clastix/capsule/pkg/webhook/utils"
)
type patchHandler struct{}
func PatchHandler() capsulewebhook.Handler {
return &patchHandler{}
}
func (r *patchHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return nil
}
}
func (r *patchHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return nil
}
}
func (r *patchHandler) OnUpdate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
// Decode Namespace
ns := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, ns); err != nil {
return utils.ErroredResponse(err)
}
// Get Tenant Label
ln, err := capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{})
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
// Extract Tenant from namespace
e := fmt.Sprintf("namespace/%s can not be patched", ns.Name)
if label, ok := ns.ObjectMeta.Labels[ln]; ok {
// retrieving the selected Tenant
tnt := &capsulev1beta1.Tenant{}
if err = c.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NamespacePatch", e)
response := admission.Denied(e)
return &response
}
} else {
recorder.Eventf(ns, corev1.EventTypeWarning, "NamespacePatch", e)
response := admission.Denied(e)
return &response
}
return nil
}
}

View File

@@ -5,7 +5,6 @@ package webhook
import (
"context"
"io/ioutil"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/client-go/tools/record"
@@ -16,12 +15,6 @@ import (
)
func Register(manager controllerruntime.Manager, webhookList ...Webhook) error {
// skipping webhook setup if certificate is missing
certData, _ := ioutil.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt")
if len(certData) == 0 {
return nil
}
recorder := manager.GetEventRecorderFor("tenant-webhook")
server := manager.GetWebhookServer()

11
scripts/helm-docs.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
## Reference: https://github.com/norwoodj/helm-docs
set -eux
CHART_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo "$CHART_DIR"
echo "Running Helm-Docs"
docker run \
-v "$CHART_DIR:/helm-docs" \
-u $(id -u) \
jnorwood/helm-docs:latest