mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-02 17:50:17 +00:00
Compare commits
28 Commits
v0.1.2-rc0
...
v0.1.2-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
261876b59b | ||
|
|
ab750141c6 | ||
|
|
e237249815 | ||
|
|
e15191c2a0 | ||
|
|
741db523e5 | ||
|
|
7b3f850035 | ||
|
|
72733415f0 | ||
|
|
cac2920827 | ||
|
|
e0b339d68a | ||
|
|
4f55dd8db8 | ||
|
|
fd738341ed | ||
|
|
fce1658827 | ||
|
|
93547c128f | ||
|
|
f1dc028649 | ||
|
|
37381184d2 | ||
|
|
82b58d7d53 | ||
|
|
60e826dc83 | ||
|
|
6e8ddd102f | ||
|
|
b64aaebc89 | ||
|
|
9a85631bb8 | ||
|
|
51ed42981f | ||
|
|
cf313d415b | ||
|
|
526a6053a5 | ||
|
|
0dd13a96fc | ||
|
|
1c8a5d8f5a | ||
|
|
b9fc50861b | ||
|
|
29d29ccd4b | ||
|
|
f207546af0 |
8
.github/workflows/docker-ci.yml
vendored
8
.github/workflows/docker-ci.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/e2e.yml
vendored
5
.github/workflows/e2e.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -28,4 +28,5 @@ bin
|
||||
**/*.crt
|
||||
**/*.key
|
||||
.DS_Store
|
||||
*.tgz
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -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"
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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__": "@",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
{
|
||||
|
||||
@@ -21,3 +21,4 @@
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
README.md.gotmpl
|
||||
|
||||
@@ -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:
|
||||
|
||||
132
charts/capsule/README.md.gotmpl
Normal file
132
charts/capsule/README.md.gotmpl
Normal 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 Helm’s 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 chart’s values.yaml file. Any values in `values.yaml` that weren’t 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.
|
||||
@@ -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
|
||||
*/}}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -}}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: []
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
10
controllers/tls/errors.go
Normal 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
347
controllers/tls/manager.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
5
docs/content/guides/charmed.md
Normal file
5
docs/content/guides/charmed.md
Normal 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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
34
docs/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
2
go.sum
@@ -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
227
main.go
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
82
pkg/webhook/namespace/patch.go
Normal file
82
pkg/webhook/namespace/patch.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
11
scripts/helm-docs.sh
Normal 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
|
||||
Reference in New Issue
Block a user