feat(config): add combined users property as successor for usergroups (#1767)

* feat(config): add combined users property as successor for usergroups and usernames configuration

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(crds): add proper deprecation notices on properties and via admission warnings

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: add local monitoring environment

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-12-04 12:18:07 +01:00
committed by GitHub
parent dd39e1a6d5
commit 584d372521
54 changed files with 6700 additions and 126 deletions

View File

@@ -1,49 +0,0 @@
name: e2e-internal
permissions: {}
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
branches:
- "main"
paths:
- '.github/workflows/e2e.yml'
- '.github/workflows/e2e-internal.yml'
- 'api/**'
- 'controllers/**'
- 'internal/**'
- 'pkg/**'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
- 'main.go'
- 'Makefile'
jobs:
internal-e2e:
name: Trigger internal E2E Testing
runs-on:
labels: ubuntu-latest
if: github.repository_owner == 'projectcapsule'
steps:
- name: Trigger internal e2e repo
env:
GH_TOKEN: ${{ secrets.INTERNAL_E2E_PAT }}
run: |
if [ -z "${GH_TOKEN}" ]; then
echo "GH_TOKEN is empty; secrets are not available. Skipping."
exit 1
fi
curl -fsS -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/projectcapsule/enterprise-e2e/dispatches \
-d "$(jq -n --arg ref "main" \
--arg pr_number "${{ github.event.pull_request.number }}" \
--arg sha "${{ github.sha }}" \
--arg repo "${{ github.repository }}" \
'{event_type:"internal-e2e", client_payload:{ref:$ref, pr_number:$pr_number, sha:$sha, repo:$repo}}')"

View File

@@ -23,18 +23,45 @@ concurrency:
jobs:
e2e:
name: E2E Testing
name: E2E Testing (CE)
runs-on:
labels: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version-file: 'go.mod'
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v3.14.2
- name: e2e
run: sudo make e2e
run-e2e:
name: E2E Testing
strategy:
fail-fast: false
matrix:
k8s-version:
- '1.30.0'
- '1.31.0'
- '1.32.0'
- '1.33.0'
runs-on:
labels: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
repository: ${{ github.event.client_payload.repo }}
ref: ${{ github.event.client_payload.sha }}
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version-file: 'go.mod'
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
- name: e2e (Enterprise)
run: KUBERNETES_SUPPORTED_VERSION=${{ matrix.k8s-version }} sudo make e2e

View File

@@ -23,6 +23,7 @@ jobs:
go-version-file: 'go.mod'
- name: Generate manifests
run: |
make generate
make manifests
if [[ $(git diff --stat) != '' ]]; then
echo -e '\033[0;31mManifests outdated! (Run make manifests locally and commit)\033[0m ❌'

View File

@@ -70,9 +70,13 @@ $ make deploy
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
$ LAPTOP_HOST_IP="<YOUR_LAPTOP_IP>" make dev-setup
# Monitoring Setup (Grafana/Prometheus/Pyroscope)
$ LAPTOP_HOST_IP="<YOUR_LAPTOP_IP>" make dev-setup-monitoring
```
### Explenation
### Setup
We recommend to setup the development environment with the make `dev-setup` target. However here is a step by step guide to setup the development environment for understanding.

View File

@@ -97,9 +97,7 @@ helm-test: kind
@$(KIND) delete cluster --name capsule-charts
helm-test-exec: ct helm-controller-version ko-build-all
$(MAKE) docker-build-capsule-trace
$(MAKE) e2e-load-image CLUSTER_NAME=capsule-charts IMAGE=$(CAPSULE_IMG) VERSION=v0.0.0
$(MAKE) e2e-load-image CLUSTER_NAME=capsule-charts IMAGE=$(CAPSULE_IMG) VERSION=tracing
@$(KUBECTL) create ns capsule-system || true
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/grafana/grafana-operator/releases/download/v5.18.0/crds.yaml
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.crds.yaml
@@ -160,6 +158,26 @@ dev-setup:
./charts/capsule
$(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true
setup-monitoring: dev-setup-fluxcd
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring | envsubst | kubectl apply -f -
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring/dashboards | kubectl apply -f -
@$(MAKE) wait-for-helmreleases
@printf "\n\033[32mAccess Grafana:\033[0m\n\n"
@printf " \033[1mkubectl port-forward svc/kube-prometheus-stack-grafana 9090:80 -n monitoring-system\033[0m\n\n"
dev-setup-monitoring: setup-monitoring
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/host-proxy | envsubst | kubectl apply -f -
dev-setup-fluxcd:
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f -
wait-for-helmreleases:
@ echo "Waiting for all HelmReleases to have observedGeneration >= 0..."
@while [ "$$($(KUBECTL) get helmrelease -A -o jsonpath='{range .items[?(@.status.observedGeneration<0)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | wc -l)" -ne 0 ]; do \
sleep 5; \
done
####################
# -- Docker
####################

View File

@@ -11,8 +11,15 @@ import (
// CapsuleConfigurationSpec defines the Capsule configuration.
type CapsuleConfigurationSpec struct {
// Define entities which are considered part of the Capsule construct
// Users not mentioned here will be ignored by Capsule
Users api.UserListSpec `json:"users,omitempty"`
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
//
// Names of the users considered as Capsule users.
UserNames []string `json:"userNames,omitempty"`
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
//
// Names of the groups considered as Capsule users.
// +kubebuilder:default={capsule.clastix.io}
UserGroups []string `json:"userGroups,omitempty"`

View File

@@ -11,8 +11,9 @@ type NamespaceOptions struct {
// +kubebuilder:validation:Minimum=1
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Deprecated: Use additionalMetadataList instead (https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist)
//
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
// Deprecated: Use additionalMetadataList instead
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional.
AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"`

View File

@@ -37,11 +37,13 @@ type TenantSpec struct {
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`

View File

@@ -98,6 +98,11 @@ func (in *CapsuleConfigurationList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec) {
*out = *in
if in.Users != nil {
in, out := &in.Users, &out.Users
*out = make(api.UserListSpec, len(*in))
copy(*out, *in)
}
if in.UserNames != nil {
in, out := &in.UserNames, &out.UserNames
*out = make([]string, len(*in))

View File

@@ -116,7 +116,7 @@ The following Values have changed key or Value:
| manager.options.allowServiceAccountPromotion | bool | `false` | ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts. |
| manager.options.annotations | object | `{}` | Additional annotations to add to the CapsuleConfiguration resource |
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. |
| manager.options.capsuleUserGroups | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. |
| manager.options.createConfiguration | bool | `true` | Create Configuration |
| 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 |
@@ -125,7 +125,8 @@ The following Values have changed key or Value:
| manager.options.logLevel | string | `"3"` | Set the log verbosity of the capsule with a value from 1 to 5 |
| manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant |
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
| manager.options.userNames | list | `[]` | Names of the users considered as Capsule users. |
| manager.options.userNames | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. |
| manager.options.users | list | `[{"kind":"Group","name":"projectcapsule.dev"}]` | Define entities which are considered part of the Capsule construct. Users not mentioned here will be ignored by Capsule |
| manager.options.workers | int | `1` | Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA). |
| manager.rbac.create | bool | `true` | Specifies whether RBAC resources should be created. |
| manager.rbac.existingClusterRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. |
@@ -166,6 +167,13 @@ The following Values have changed key or Value:
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| webhooks.exclusive | bool | `false` | When `crds.exclusive` is `true` the webhooks will be installed |
| webhooks.hooks.config.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.config.failurePolicy | string | `"Ignore"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.config.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.config.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.config.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.config.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.config.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.hooks.cordoning.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.cordoning.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.cordoning.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |

View File

@@ -158,15 +158,43 @@ spec:
userGroups:
default:
- capsule.clastix.io
description: Names of the groups considered as Capsule users.
description: |-
Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
Names of the groups considered as Capsule users.
items:
type: string
type: array
userNames:
description: Names of the users considered as Capsule users.
description: |-
Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
Names of the users considered as Capsule users.
items:
type: string
type: array
users:
description: |-
Define entities which are considered part of the Capsule construct
Users not mentioned here will be ignored by Capsule
items:
properties:
kind:
description: Kind of entity. Possible values are "User", "Group",
and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of the entity.
type: string
required:
- kind
- name
type: object
type: array
required:
- enableTLSReconciler
type: object

View File

@@ -129,7 +129,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
imagePullPolicies:
@@ -160,7 +163,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
allowedHostnames:
@@ -176,7 +182,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
hostnameCollisionScope:
@@ -855,7 +864,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
resourceQuotas:
@@ -1030,7 +1042,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
required:
@@ -1185,7 +1200,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
cordoned:
@@ -1203,7 +1221,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
matchExpressions:
description: matchExpressions is a list of label selector requirements.
@@ -1271,7 +1292,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
default:
type: string
@@ -1352,7 +1376,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
default:
type: string
@@ -1412,7 +1439,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
type: object
hostnameCollisionScope:
@@ -1436,8 +1466,9 @@ spec:
type: object
limitRanges:
description: |-
Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -1527,8 +1558,9 @@ spec:
properties:
additionalMetadata:
description: |-
Deprecated: Use additionalMetadataList instead (https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist)
Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
Deprecated: Use additionalMetadataList instead
properties:
annotations:
additionalProperties:
@@ -1642,8 +1674,9 @@ spec:
type: object
networkPolicies:
description: |-
Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -2292,7 +2325,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
default:
type: string
@@ -2439,7 +2475,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
default:
type: string
@@ -2572,7 +2611,10 @@ spec:
type: string
type: array
allowedRegex:
description: Match elements by regex (DEPRECATED)
description: |-
Deprecated: will be removed in a future release
Match elements by regex.
type: string
default:
type: string

View File

@@ -14,6 +14,10 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
administrators:
{{- toYaml .Values.manager.options.administrators | nindent 4 }}
users:
{{- toYaml .Values.manager.options.users | nindent 4 }}
enableTLSReconciler: {{ .Values.tls.enableController }}
overrides:
mutatingWebhookConfigurationName: {{ include "capsule.fullname" . }}-mutating-webhook-configuration

View File

@@ -87,7 +87,7 @@ spec:
# piping stderr to stdout means kubectl's errors are surfaced
# in the pod's logs.
kubectl apply --server-side=true --overwrite=true --force-conflicts=true -f /data/ 2>&1
kubectl apply --server-side=true --overwrite=true --force-conflicts=true --field-manager='capsule/crd-lifecycle' -f /data/ 2>&1
volumeMounts:
{{- range $path, $_ := .Files.Glob "crds/**.yaml" }}
- name: {{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }}

View File

@@ -603,4 +603,40 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.config }}
{{- if .enabled }}
- name: config.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/config/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
namespaceSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .objectSelector }}
objectSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .matchConditions }}
matchConditions:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1beta2
operations:
- UPDATE
resources:
- capsuleconfigurations
scope: 'Cluster'
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -336,11 +336,8 @@
"type": "string"
},
"capsuleUserGroups": {
"description": "Names of the groups considered as Capsule users.",
"type": "array",
"items": {
"type": "string"
}
"description": "DEPRECATED: use users properties. Names of the users considered as Capsule users.",
"type": "array"
},
"createConfiguration": {
"description": "Create Configuration",
@@ -399,9 +396,24 @@
"type": "string"
},
"userNames": {
"description": "Names of the users considered as Capsule users.",
"description": "DEPRECATED: use users properties. Names of the users considered as Capsule users.",
"type": "array"
},
"users": {
"description": "Define entities which are considered part of the Capsule construct. Users not mentioned here will be ignored by Capsule",
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"workers": {
"description": "Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA).",
"type": "integer"
@@ -739,6 +751,39 @@
"hooks": {
"type": "object",
"properties": {
"config": {
"type": "object",
"properties": {
"enabled": {
"description": "Enable the Hook",
"type": "boolean"
},
"failurePolicy": {
"description": "[FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)",
"type": "string"
},
"matchConditions": {
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "array"
},
"matchPolicy": {
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "string"
},
"namespaceSelector": {
"description": "[NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)",
"type": "object"
},
"objectSelector": {
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
"type": "object"
},
"reinvocationPolicy": {
"description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)",
"type": "string"
}
}
},
"cordoning": {
"type": "object",
"properties": {

View File

@@ -178,6 +178,11 @@ manager:
workers: 1
# -- Set the log verbosity of the capsule with a value from 1 to 5
logLevel: '3'
# -- Define entities which are considered part of the Capsule construct.
# Users not mentioned here will be ignored by Capsule
users:
- kind: "Group"
name: "projectcapsule.dev"
# -- Define entities which can act as Administrators in the capsule construct
# These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label
# for interacting with namespaces. Because if that label is not defined, it's assumed that namespace interaction was not targeted towards a tenant and will therefor
@@ -185,10 +190,6 @@ manager:
administrators: []
# - kind: User
# name: alice
# -- Names of the users considered as Capsule users.
userNames: []
# -- Names of the groups considered as Capsule users.
capsuleUserGroups: ["projectcapsule.dev"]
# -- Define groups which when found in the request of a user will be ignored by the Capsule
# this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
ignoreUserWithGroups: []
@@ -210,6 +211,13 @@ manager:
forbiddenAnnotations:
denied: []
deniedRegex: ""
# -- DEPRECATED: use users properties.
# Names of the users considered as Capsule users.
userNames: []
# -- DEPRECATED: use users properties.
# Names of the users considered as Capsule users.
capsuleUserGroups: []
# -- A list of extra arguments for the capsule controller
extraArgs:
@@ -654,6 +662,22 @@ webhooks:
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
config:
# -- Enable the Hook
enabled: true
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
failurePolicy: Ignore
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchPolicy: Exact
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
objectSelector: {}
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
namespaceSelector: {}
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
tenantResourceObjects:
# -- Enable the Hook
enabled: true

View File

@@ -43,6 +43,7 @@ import (
utilscontroller "github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
"github.com/projectcapsule/capsule/internal/webhook"
cfgvalidation "github.com/projectcapsule/capsule/internal/webhook/cfg"
"github.com/projectcapsule/capsule/internal/webhook/defaults"
"github.com/projectcapsule/capsule/internal/webhook/dra"
"github.com/projectcapsule/capsule/internal/webhook/gateway"
@@ -88,11 +89,11 @@ func printVersion() {
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goRuntime.GOOS, goRuntime.GOARCH))
}
//nolint:maintidx
//nolint:maintidx,cyclop
func main() {
controllerConfig := utilscontroller.ControllerOptions{}
var enableLeaderElection, version bool
var enableLeaderElection, enablePprof, version bool
var metricsAddr, ns string
@@ -108,6 +109,7 @@ func main() {
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&version, "version", false, "Print the Capsule version and exit")
flag.StringVar(&controllerConfig.ConfigurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use")
flag.BoolVar(&enablePprof, "enable-pprof", false, "Enables Pprof endpoint for profiling (not recommend in production)")
opts := zap.Options{
EncoderConfigOptions: append([]zap.EncoderConfigOption{}, func(config *zapcore.EncoderConfig) {
@@ -139,7 +141,7 @@ func main() {
os.Exit(1)
}
manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
ctrlOpts := ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
@@ -155,7 +157,13 @@ func main() {
return client.New(config, options)
},
})
}
if enablePprof {
ctrlOpts.PprofBindAddress = ":8082"
}
manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrlOpts)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
@@ -311,6 +319,9 @@ func main() {
route.TenantAssignment(
misc.TenantAssignmentHandler(),
),
route.ConfigValidation(
cfgvalidation.WarningHandler(),
),
)
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)

View File

@@ -69,7 +69,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should fail using a User non matching the capsule-user-group flag", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"test"}
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "test"}}
})
ns := NewNamespace("")
@@ -78,7 +80,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should succeed and be available in Tenant namespaces list with multiple groups", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"test", "alice"}
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: "alice"}, {Kind: api.GroupOwner, Name: "test"}}
})
ns := NewNamespace("")
@@ -89,7 +93,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should succeed and be available in Tenant namespaces list with default single group", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"projectcapsule.dev"}
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "projectcapsule.dev"}}
})
ns := NewNamespace("")
@@ -100,7 +106,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should fail when group is ignored", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserGroups = []string{"projectcapsule.dev"}
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.Users = []api.UserSpec{{Kind: api.GroupOwner, Name: "projectcapsule.dev"}}
configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"}
})
@@ -111,9 +119,10 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should succeed and be available in Tenant namespaces list with default single user", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}}
configuration.Spec.IgnoreUserWithGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
})
ns := NewNamespace("")
@@ -123,9 +132,10 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should succeed and be available in Tenant namespaces list with default single user", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.IgnoreUserWithGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}}
})
ns := NewNamespace("")
@@ -135,8 +145,9 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
It("should fail when group is ignored", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.UserNames = []string{}
configuration.Spec.UserGroups = []string{}
configuration.Spec.UserNames = []string{tnt.Spec.Owners[0].Name}
configuration.Spec.Users = []api.UserSpec{{Kind: api.UserOwner, Name: tnt.Spec.Owners[0].Name}}
configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"}
})

View File

@@ -0,0 +1,33 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml
patches:
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-cross-namespace-refs=true
target:
kind: Deployment
name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-remote-bases=true
target:
kind: Deployment
name: "kustomize-controller"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --default-service-account=default
target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
- patch: |
- op: replace
path: /spec/replicas
value: 0
target:
kind: Deployment
name: "(notification-controller|image-reflector-controller|image-automation-controller)"

View File

@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: capsule
namespace: monitoring-system
labels:
app: capsule
spec:
replicas: 1
selector:
matchLabels:
app: capsule
template:
metadata:
labels:
app: capsule
annotations:
prometheus.io/scrape: 'true'
prometheus.io/path: '/metrics'
prometheus.io/port: '8080'
profiles.grafana.com/memory.scrape: "true"
profiles.grafana.com/memory.port: "8082"
profiles.grafana.com/cpu.scrape: "true"
profiles.grafana.com/cpu.port: "8082"
profiles.grafana.com/goroutine.scrape: "true"
profiles.grafana.com/goroutine.port: "8082"
spec:
containers:
- name: tcp-proxy
image: alpine/socat
ports:
- containerPort: 8080
- containerPort: 8082
command: ["sh", "-c"]
args:
- |
set -e
echo "Starting TCP proxy to ${LAPTOP_HOST_IP}..."
# Forward 8080 -> TARGET_HOST:8080
socat TCP-LISTEN:8080,fork,reuseaddr TCP:${LAPTOP_HOST_IP}:8080 &
# Forward 8082 -> TARGET_HOST:8082
socat TCP-LISTEN:8082,fork,reuseaddr TCP:${LAPTOP_HOST_IP}:8082 &
wait

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deploy.yaml
- servicemonitor.yaml

View File

@@ -0,0 +1,33 @@
---
apiVersion: v1
kind: Service
metadata:
name: capsule
namespace: monitoring-system
labels:
app: capsule
release: kube-prometheus-stack
spec:
selector:
app: capsule
ports:
- name: metrics
port: 8080
targetPort: 8080
protocol: TCP
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: capsule
namespace: monitoring-system
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: capsule
endpoints:
- port: metrics
path: /metrics
interval: 5s

View File

@@ -0,0 +1,515 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Profiling performance-related metrics based on controller-runtime.",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 10,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 6,
"panels": [],
"title": "Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "controller_runtime_active_workers{job=~\"$job\"}",
"interval": "",
"legendFormat": "{{controller}}",
"range": true,
"refId": "A"
}
],
"title": "Active Workers",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (result) (rate(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval]))",
"interval": "",
"legendFormat": "{{result}}",
"range": true,
"refId": "A"
}
],
"title": "Reoncile Rate",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 10
},
"id": 8,
"panels": [],
"repeat": "Webhook",
"title": "Webhook \"$Webhook\" Status",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 11
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "rate(controller_runtime_webhook_latency_seconds_count{job=~\"$job\", webhook=\"$Webhook\"}[$__rate_interval])",
"interval": "",
"legendFormat": "Request Rate",
"range": true,
"refId": "A"
}
],
"title": "Webhook Request Rate $Webhook",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 20
},
"id": 12,
"options": {
"calculate": false,
"calculation": {},
"cellGap": 2,
"cellValues": {},
"color": {
"exponent": 0.5,
"fill": "#b4ff00",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 128
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": false
},
"rowsFrame": {
"layout": "auto"
},
"showValue": "never",
"tooltip": {
"mode": "single",
"showColorScale": false,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false,
"unit": "short"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "rate(controller_runtime_webhook_latency_seconds_bucket{job=~\"$job\", webhook=\"$Webhook\"}[$__rate_interval])",
"format": "heatmap",
"interval": "",
"legendFormat": "{{le}}",
"range": true,
"refId": "A"
}
],
"title": "Reconcile Time Buckets",
"type": "heatmap"
}
],
"preload": false,
"refresh": "30s",
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": [
"capsule"
],
"value": [
"capsule"
]
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(controller_runtime_webhook_requests_total,job)",
"includeAll": true,
"label": "Job",
"multi": true,
"name": "job",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(controller_runtime_webhook_requests_total,job)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"type": "query"
},
{
"current": {
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(controller_runtime_webhook_requests_total{job=~\"$job\"},webhook)",
"includeAll": true,
"multi": true,
"name": "Webhook",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(controller_runtime_webhook_requests_total{job=~\"$job\"},webhook)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Controller Runtime Webhooks Detail",
"uid": "0L6Y8KEnk",
"version": 1
}

View File

@@ -0,0 +1,546 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Profiling performance-related metrics based on controller-runtime.",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 22,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 6,
"panels": [],
"title": "Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true,
"sortBy": "Last *",
"sortDesc": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "controller_runtime_active_workers{job=~\"$job\"}",
"interval": "",
"legendFormat": "{{controller}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum (controller_runtime_active_workers{job=~\"$job\"})",
"hide": false,
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Active Workers",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true,
"sortBy": "Last *",
"sortDesc": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (result) (\n increase(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval])\n)",
"instant": false,
"interval": "",
"legendFormat": "{{result}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(\n increase(controller_runtime_reconcile_total{job=~\"$job\"}[$__rate_interval])\n)",
"hide": false,
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Reoncile Rate",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 10
},
"id": 8,
"panels": [],
"repeat": "Controller",
"title": "Controller \"$Controller\" Status",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 11
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (result) (\n increase(controller_runtime_reconcile_total{job=~\"$job\", controller=~\"$Controller\"}[$__rate_interval])\n)\n",
"interval": "",
"legendFormat": "{{result}}",
"range": true,
"refId": "A"
}
],
"title": "Reconcile Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 20
},
"id": 12,
"options": {
"calculate": false,
"calculation": {},
"cellGap": 2,
"cellValues": {},
"color": {
"exponent": 0.5,
"fill": "#b4ff00",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 128
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": false
},
"rowsFrame": {
"layout": "auto"
},
"showValue": "never",
"tooltip": {
"mode": "single",
"showColorScale": false,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false,
"unit": "short"
}
},
"pluginVersion": "12.3.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "rate(controller_runtime_reconcile_time_seconds_bucket{job=~'$job', controller=~'$Controller'}[$__rate_interval])",
"format": "heatmap",
"interval": "",
"legendFormat": "{{le}}",
"range": true,
"refId": "A"
}
],
"title": "Reconcile Time Buckets",
"type": "heatmap"
}
],
"preload": false,
"refresh": "10s",
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": "All",
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(controller_runtime_active_workers,job)",
"includeAll": true,
"label": "Job",
"multi": true,
"name": "job",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(controller_runtime_active_workers,job)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"type": "query"
},
{
"current": {
"text": "All",
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(controller_runtime_active_workers{job=~\"$job\"},controller)",
"includeAll": true,
"multi": true,
"name": "Controller",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(controller_runtime_active_workers{job=~\"$job\"},controller)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-15m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Controller Runtime Controllers Detail",
"uid": "5J4pyKEnk",
"version": 1
}

View File

@@ -0,0 +1,22 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: monitoring-system
commonLabels:
release: prometheus
generatorOptions:
annotations:
k8s-sidecar-target-directory: /tmp/dashboards/Controller Development
labels:
grafana_dashboard: "1"
disableNameSuffixHash: true
configMapGenerator:
- name: ctrl-runtime
files:
- ctrl-runtime.json
- name: ctrl-runtime-admission
files:
- ctrl-runtime-admission.json
- name: perf
files:
- performance.json

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- release.flux.yaml

View File

@@ -0,0 +1,100 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kube-prometheus-stack
namespace: flux-system
spec:
serviceAccountName: kustomize-controller
interval: 30s
timeout: 10m
targetNamespace: monitoring-system
releaseName: "kube-prometheus-stack"
chart:
spec:
chart: kube-prometheus-stack
version: "79.11.0"
sourceRef:
kind: HelmRepository
name: kube-prometheus-stack
interval: 24h
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
remediateLastFailure: true
retries: -1
driftDetection:
mode: enabled
values:
grafana:
additionalDataSources:
- name: Pyroscope
type: grafana-pyroscope-datasource
uid: pyroscope
url: http://pyroscope.{{ $.Release.Namespace }}.svc.cluster.local.:4040/
adminPassword: admin
global:
dnsService: "kube-dns"
dnsNamespace: "kube-system"
assertNoLeakedSecrets: false
deploymentStrategy:
type: Recreate
persistence:
enabled: false
initChownData:
enabled: false
plugins:
- grafana-llm-app
- grafana-resourcesexporter-app
- grafana-pyroscope-app
- grafana-exploretraces-app
env:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin"
GF_DIAGNOSTICS_PROFILING_ENABLED: "true"
GF_DIAGNOSTICS_PROFILING_ADDR: "0.0.0.0"
GF_DIAGNOSTICS_PROFILING_PORT: "9094"
sidecar:
enableUniqueFilenames: true
datasources:
enabled: true
dashboards:
enabled: true
folderAnnotation: "k8s-sidecar-target-directory"
annotations:
k8s-sidecar-target-directory: /tmp/dashboards/Kube Prometheus Stack
provider:
foldersFromFilesStructure: true
# https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/
grafana.ini:
analytics:
reporting_enabled: false
check_for_updates: false
check_for_plugin_updates: false
security:
disable_gravatar: true
cookie_secure: true
cookie_samesite: lax
strict_transport_security: true
strict_transport_security_preload: true
strict_transport_security_subdomains: true
content_security_policy: true
auth:
disable_login_form: false
users:
allow_sign_up: true
auto_assign_org: true
server:
root_url: "http://localhost:9090"
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: kube-prometheus-stack
namespace: flux-system
spec:
interval: 24h0m0s
url: https://prometheus-community.github.io/helm-charts

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ns.yaml
- kube-prometheus-stack/
- pyroscope/

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring-system

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- release.flux.yaml

View File

@@ -0,0 +1,57 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: pyroscope
namespace: flux-system
spec:
serviceAccountName: kustomize-controller
interval: 30s
timeout: 10m
targetNamespace: monitoring-system
releaseName: "pyroscope"
chart:
spec:
chart: pyroscope
version: "1.16.0"
sourceRef:
kind: HelmRepository
name: pyroscope
interval: 24h
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
remediateLastFailure: true
retries: -1
driftDetection:
mode: enabled
values:
global:
dnsService: "kube-dns"
dnsNamespace: "kube-system"
clusterLabelOverride: "kind"
pyroscope:
persistence:
size: 10Gi
podAnnotations:
profiles.grafana.com/memory.scrape: "false"
profiles.grafana.com/goroutine.scrape: "false"
profiles.grafana.com/cpu.scrape: "false"
alloy:
controller:
podAnnotations:
profiles.grafana.com/memory.scrape: "false"
profiles.grafana.com/goroutine.scrape: "false"
profiles.grafana.com/cpu.scrape: "false"
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: pyroscope
namespace: flux-system
spec:
interval: 24h0m0s
url: https://grafana.github.io/helm-charts

View File

@@ -21,8 +21,9 @@ import (
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
func (r *Manager) syncLimitRanges(ctx context.Context, tenant *capsulev1beta2.Tenant) error { //nolint:dupl
// getting requested LimitRange keys
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items))
keys := make([]string, 0, len(tenant.Spec.LimitRanges.Items)) //nolint:staticcheck
//nolint:staticcheck
for i := range tenant.Spec.LimitRanges.Items {
keys = append(keys, strconv.Itoa(i))
}
@@ -45,6 +46,7 @@ func (r *Manager) syncLimitRange(ctx context.Context, tenant *capsulev1beta2.Ten
return err
}
//nolint:staticcheck
for i, spec := range tenant.Spec.LimitRanges.Items { //nolint:dupl
target := &corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -285,14 +285,16 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
return result, err
}
// Ensuring LimitRange resources
r.Log.V(4).Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
r.Log.V(4).Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items)) //nolint:staticcheck
if err = r.syncLimitRanges(ctx, instance); err != nil {
err = fmt.Errorf("cannot sync limitrange items: %w", err)
return result, err
}
// Ensuring ResourceQuota resources
r.Log.V(4).Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
@@ -301,6 +303,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
return result, err
}
// Ensuring RoleBinding resources
r.Log.V(4).Info("Ensuring RoleBindings for Owners and Tenant")

View File

@@ -21,8 +21,9 @@ import (
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
func (r *Manager) syncNetworkPolicies(ctx context.Context, tenant *capsulev1beta2.Tenant) error { //nolint:dupl
// getting requested NetworkPolicy keys
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items))
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies.Items)) //nolint:staticcheck
//nolint:staticcheck
for i := range tenant.Spec.NetworkPolicies.Items {
keys = append(keys, strconv.Itoa(i))
}
@@ -45,6 +46,7 @@ func (r *Manager) syncNetworkPolicy(ctx context.Context, tenant *capsulev1beta2.
return err
}
//nolint:staticcheck
for i, spec := range tenant.Spec.NetworkPolicies.Items { //nolint:dupl
target := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -0,0 +1,71 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package cfg
import (
"context"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
"github.com/projectcapsule/capsule/internal/webhook/utils"
)
type warningHandler struct{}
func WarningHandler() capsulewebhook.Handler {
return &warningHandler{}
}
func (h *warningHandler) OnCreate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.handle(decoder, req)
}
}
func (h *warningHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
}
}
func (h *warningHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, req admission.Request) *admission.Response {
return h.handle(decoder, req)
}
}
func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request) *admission.Response {
config := &capsulev1beta2.CapsuleConfiguration{}
if err := decoder.Decode(req, config); err != nil {
return utils.ErroredResponse(err)
}
response := &admission.Response{
AdmissionResponse: admissionv1.AdmissionResponse{
UID: req.UID,
Allowed: true,
},
}
//nolint:staticcheck
if len(config.Spec.UserGroups) > 0 {
response.Warnings = append(response.Warnings,
"The field `userGroups` is deprecated and will be removed in a future release. Please migrate to the `users` field. See: https://projectcapsule.dev/docs/operating/setup/configuration/#users.",
)
}
//nolint:staticcheck
if len(config.Spec.UserNames) > 0 {
response.Warnings = append(response.Warnings,
"The field `userNames` is deprecated and will be removed in a future release. Please migrate to the `users` field. See: https://projectcapsule.dev/docs/operating/setup/configuration/#users.",
)
}
return response
}

View File

@@ -108,6 +108,7 @@ func appendHostnameError(spec api.AllowedListSpec) (append string) {
append = fmt.Sprintf(", specify one of the following (%s)", strings.Join(spec.Exact, ", "))
}
//nolint:staticcheck
if len(spec.Regex) > 0 {
append += fmt.Sprintf(", or matching the regex %s", spec.Regex)
}

View File

@@ -114,6 +114,7 @@ func (r *hostnames) validateHostnames(tenant capsulev1beta2.Tenant, hostnames se
var notMatchingHostnames []string
//nolint:staticcheck
if allowedRegex := tenant.Spec.IngressOptions.AllowedHostnames.Regex; len(allowedRegex) > 0 {
for currentHostname := range hostnames {
matched, _ = regexp.MatchString(allowedRegex, currentHostname)

View File

@@ -43,6 +43,7 @@ func (f registryClassForbiddenError) Error() (err string) {
extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(f.spec.Exact, ", ")))
}
//nolint:staticcheck
if len(f.spec.Regex) > 0 {
extra = append(extra, fmt.Sprintf(" use one matching the following regex (%s)", f.spec.Regex))
}

View File

@@ -0,0 +1,24 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package route
import (
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
)
type configValidating struct {
handlers []capsulewebhook.Handler
}
func ConfigValidation(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
return &configValidating{handlers: handler}
}
func (w *configValidating) GetHandlers() []capsulewebhook.Handler {
return w.handlers
}
func (w *configValidating) GetPath() string {
return "/config/validating"
}

View File

@@ -49,6 +49,7 @@ func (h *containerRegistryRegexHandler) OnUpdate(_ client.Client, decoder admiss
}
}
//nolint:staticcheck
func (h *containerRegistryRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response {
tenant := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {

View File

@@ -49,6 +49,7 @@ func (h *hostnameRegexHandler) OnUpdate(_ client.Client, decoder admission.Decod
}
}
//nolint:staticcheck
func (h *hostnameRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response {
tenant := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {

View File

@@ -49,6 +49,7 @@ func (h *ingressClassRegexHandler) OnUpdate(_ client.Client, decoder admission.D
}
}
//nolint:staticcheck
func (h *ingressClassRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response {
tenant := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {

View File

@@ -49,6 +49,7 @@ func (h *storageClassRegexHandler) OnUpdate(_ client.Client, decoder admission.D
}
}
//nolint:staticcheck
func (h *storageClassRegexHandler) validate(decoder admission.Decoder, req admission.Request) *admission.Response {
tenant := &capsulev1beta2.Tenant{}
if err := decoder.Decode(req, tenant); err != nil {

View File

@@ -63,32 +63,53 @@ func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, decoder admission.De
},
}
//nolint:staticcheck
if len(tnt.Spec.LimitRanges.Items) > 0 {
response.Warnings = append(response.Warnings, "Limitranges are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.")
response.Warnings = append(response.Warnings,
"The field `limitRanges` is deprecated and will be removed in a future release. Please migrate to TenantReplications. See: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.",
)
}
//nolint:staticcheck
if len(tnt.Spec.NetworkPolicies.Items) > 0 {
response.Warnings = append(response.Warnings, "NetworkPolicies are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.")
response.Warnings = append(response.Warnings,
"The field `networkPolicies` is deprecated and will be removed in a future release. Please migrate to TenantReplications. See: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.",
)
}
//nolint:staticcheck
if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil {
response.Warnings = append(response.Warnings, "additionalMetadata is deprecated and will be removed int the future. You need to consider to migrate to AdditionalMetadataList: https://projectcapsule.dev/docs/tenants/enforcement/#additionalmetadatalist.")
response.Warnings = append(response.Warnings,
"The field `additionalMetadata` is deprecated and will be removed in a future release. Please migrate to `additionalMetadataList`. See: https://projectcapsule.dev/docs/tenants/metadata/#additionalmetadatalist.",
)
}
//nolint:staticcheck
if tnt.Spec.StorageClasses != nil && tnt.Spec.StorageClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select StorageClasses is deprecated and will be removed int the future.")
response.Warnings = append(response.Warnings,
"The `regex` selector for StorageClasses is deprecated and will be removed in a future release.",
)
}
//nolint:staticcheck
if tnt.Spec.GatewayOptions.AllowedClasses != nil && tnt.Spec.GatewayOptions.AllowedClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select GatewayClasses is deprecated and will be removed int the future.")
response.Warnings = append(response.Warnings,
"The `regex` selector for GatewayClasses is deprecated and will be removed in a future release.",
)
}
//nolint:staticcheck
if tnt.Spec.PriorityClasses != nil && tnt.Spec.PriorityClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select PriorityClasses is deprecated and will be removed int the future.")
response.Warnings = append(response.Warnings,
"The `regex` selector for PriorityClasses is deprecated and will be removed in a future release.",
)
}
//nolint:staticcheck
if tnt.Spec.RuntimeClasses != nil && tnt.Spec.RuntimeClasses.Regex != "" {
response.Warnings = append(response.Warnings, "Using the regex property to select RuntimeClasses is deprecated and will be removed int the future.")
response.Warnings = append(response.Warnings,
"The `regex` selector for RuntimeClasses is deprecated and will be removed in a future release.",
)
}
return response

View File

@@ -29,6 +29,7 @@ func AllowedValuesErrorMessage(allowed api.SelectorAllowedListSpec, err string)
extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(allowed.Exact, ", ")))
}
//nolint:staticcheck
if len(allowed.Regex) > 0 {
extra = append(extra, fmt.Sprintf("use one matching the following regex (%s)", allowed.Regex))
}

View File

@@ -58,7 +58,9 @@ func (in *SelectorAllowedListSpec) SelectorMatch(obj client.Object) bool {
type AllowedListSpec struct {
// Match exact elements which are allowed as class names within this tenant
Exact []string `json:"allowed,omitempty"`
// Match elements by regex (DEPRECATED)
// Deprecated: will be removed in a future release
//
// Match elements by regex.
Regex string `json:"allowedRegex,omitempty"`
}

View File

@@ -88,6 +88,7 @@ func (o OwnerStatusListSpec) IsOwner(name string, groups []string) bool {
return false
}
//nolint:dupl
func (o OwnerStatusListSpec) FindOwner(name string, kind OwnerKind) (CoreOwnerSpec, bool) {
// Sort in-place by (Kind.String(), Name).
sort.Sort(GetByKindAndName(o))

View File

@@ -8,21 +8,23 @@ import (
)
// +kubebuilder:object:generate=true
type UserListSpec []UserSpec
func (u UserListSpec) IsPresent(name string, groups []string) bool {
groupSet := make(map[string]struct{}, len(groups))
for _, g := range groups {
groupSet[g] = struct{}{}
}
for _, user := range u {
switch user.Kind {
case UserOwner, ServiceAccountOwner:
if name == user.Name {
if user.Name == name {
return true
}
case GroupOwner:
for _, group := range groups {
if group == user.Name {
return true
}
if _, ok := groupSet[user.Name]; ok {
return true
}
}
}
@@ -30,17 +32,60 @@ func (u UserListSpec) IsPresent(name string, groups []string) bool {
return false
}
func (o UserListSpec) FindUser(name string, kind OwnerKind) (owner UserSpec) {
//nolint:dupl
func (o UserListSpec) FindUser(name string, kind OwnerKind) (UserSpec, bool) {
sort.Sort(ByKindName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
targetKind := kind.String()
n := len(o)
idx := sort.Search(n, func(i int) bool {
ki := o[i].Kind.String()
switch {
case ki > targetKind:
return true
case ki < targetKind:
return false
default:
return o[i].Name >= name
}
})
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
if idx < n &&
o[idx].Kind.String() == targetKind &&
o[idx].Name == name {
return o[idx], true
}
return owner
return UserSpec{}, false
}
func (o UserListSpec) GetByKinds(kinds []OwnerKind) []string {
if len(o) == 0 || len(kinds) == 0 {
return nil
}
kindSet := make(map[OwnerKind]struct{}, len(kinds))
for _, k := range kinds {
kindSet[k] = struct{}{}
}
names := make([]string, 0, len(o))
for _, u := range o {
if _, ok := kindSet[u.Kind]; ok {
names = append(names, u.Name)
}
}
if len(names) == 0 {
return nil
}
sort.Strings(names)
return names
}
type ByKindName UserListSpec

325
pkg/api/users_list_test.go Normal file
View File

@@ -0,0 +1,325 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"math/rand"
"reflect"
"sort"
"testing"
"time"
"github.com/projectcapsule/capsule/pkg/api"
)
func linearFindUser(list api.UserListSpec, name string, kind api.OwnerKind) (api.UserSpec, bool) {
for _, u := range list {
if u.Kind == kind && u.Name == name {
return u, true
}
}
return api.UserSpec{}, false
}
func slowIsPresent(u api.UserListSpec, name string, groups []string) bool {
for _, user := range u {
switch user.Kind {
case api.UserOwner, api.ServiceAccountOwner:
if name == user.Name {
return true
}
case api.GroupOwner:
for _, group := range groups {
if group == user.Name {
return true
}
}
}
}
return false
}
func TestByKindNameOrdering_UserListSpec(t *testing.T) {
u := api.UserListSpec{
api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner},
api.UserSpec{Name: "z", Kind: api.UserOwner},
api.UserSpec{Name: "a", Kind: api.GroupOwner},
api.UserSpec{Name: "a", Kind: api.UserOwner},
}
// Sort using production ordering
got := append(api.UserListSpec(nil), u...)
sort.Sort(api.ByKindName(got))
// Manually sorted expectation using the same logic.
want := append(api.UserListSpec(nil), u...)
sort.Slice(want, func(i, j int) bool {
if want[i].Kind.String() != want[j].Kind.String() {
return want[i].Kind.String() < want[j].Kind.String()
}
return want[i].Name < want[j].Name
})
if len(got) != len(want) {
t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
}
for i := range got {
if !reflect.DeepEqual(got[i], want[i]) {
t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i])
}
}
}
func TestFindUser_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(42))
ownerKinds := []api.OwnerKind{
api.GroupOwner,
api.UserOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxLength = 40
numLookupsPerList = 80
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var list api.UserListSpec
n := rnd.Intn(maxLength)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
list = append(list, api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: k,
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
var qName string
var qKind api.OwnerKind
if len(list) > 0 && rnd.Float64() < 0.6 {
// 60% of lookups: pick a real element, must be found
pick := list[rnd.Intn(len(list))]
qName = pick.Name
qKind = pick.Kind
} else {
// 40%: random query, may or may not exist
qName = randomName(rnd, 3+rnd.Intn(4))
qKind = ownerKinds[rnd.Intn(len(ownerKinds))]
}
listCopy := append(api.UserListSpec(nil), list...) // FindUser sorts in-place
gotUser, gotFound := listCopy.FindUser(qName, qKind)
wantUser, wantFound := linearFindUser(list, qName, qKind)
if gotFound != wantFound {
t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v",
listIdx, lookupIdx, qName, qKind, gotFound, wantFound)
}
if gotFound && !reflect.DeepEqual(gotUser, wantUser) {
t.Fatalf("list=%d lookup=%d: user mismatch for (%q,%v):\n got= %+v\nwant= %+v",
listIdx, lookupIdx, qName, qKind, gotUser, wantUser)
}
}
}
}
func TestIsPresent_RandomizedMatchesSlowImplementation(t *testing.T) {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
ownerKinds := []api.OwnerKind{
api.UserOwner,
api.GroupOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 30
numLookupsPerList = 80
maxGroupsPerUser = 10
)
for listIdx := 0; listIdx < numLists; listIdx++ {
// Generate a random user list (possibly with duplicates).
var users api.UserListSpec
nOwners := rnd.Intn(maxOwnersPerList)
for i := 0; i < nOwners; i++ {
kind := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // length 36
Kind: kind,
})
}
for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
// Generate a random userName and groups,
// sometimes biased to hit existing owners/groups.
var userName string
var groups []string
// 50% of the time: pick an existing owner name as userName
if len(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
userName = pick.Name
} else {
userName = randomName(rnd, 3+rnd.Intn(4))
}
// Random groups, sometimes including owner names
nGroups := rnd.Intn(maxGroupsPerUser)
for i := 0; i < nGroups; i++ {
if len(users) > 0 && rnd.Float64() < 0.5 {
pick := users[rnd.Intn(len(users))]
groups = append(groups, pick.Name)
} else {
groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
}
}
got := users.IsPresent(userName, groups)
want := slowIsPresent(users, userName, groups)
if got != want {
t.Fatalf("list=%d lookup=%d: mismatch\n users=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v",
listIdx, lookupIdx, users, userName, groups, got, want)
}
}
}
}
func TestGetByKinds_Basic(t *testing.T) {
users := api.UserListSpec{
api.UserSpec{Name: "alice", Kind: api.UserOwner},
api.UserSpec{Name: "svc-1", Kind: api.ServiceAccountOwner},
api.UserSpec{Name: "team-a", Kind: api.GroupOwner},
api.UserSpec{Name: "bob", Kind: api.UserOwner},
api.UserSpec{Name: "team-b", Kind: api.GroupOwner},
}
eqStrings := func(got, want []string) bool {
if got == nil && want == nil {
return true
}
if len(got) != len(want) {
return false
}
sort.Strings(got)
sort.Strings(want)
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}
// Single kind: UserOwner
gotUsers := users.GetByKinds([]api.OwnerKind{api.UserOwner})
wantUsers := []string{"alice", "bob"}
if !eqStrings(gotUsers, wantUsers) {
t.Fatalf("GetByKinds([UserOwner]) = %v, want %v", gotUsers, wantUsers)
}
// Single kind: GroupOwner
gotGroups := users.GetByKinds([]api.OwnerKind{api.GroupOwner})
wantGroups := []string{"team-a", "team-b"}
if !eqStrings(gotGroups, wantGroups) {
t.Fatalf("GetByKinds([GroupOwner]) = %v, want %v", gotGroups, wantGroups)
}
// Multiple kinds: UserOwner + ServiceAccountOwner
gotUsersAndSAs := users.GetByKinds([]api.OwnerKind{api.UserOwner, api.ServiceAccountOwner})
wantUsersAndSAs := []string{"alice", "bob", "svc-1"}
if !eqStrings(gotUsersAndSAs, wantUsersAndSAs) {
t.Fatalf("GetByKinds([UserOwner,ServiceAccountOwner]) = %v, want %v",
gotUsersAndSAs, wantUsersAndSAs)
}
// No kinds → nil
gotNone := users.GetByKinds(nil)
if gotNone != nil {
t.Fatalf("GetByKinds(nil) = %v, want nil", gotNone)
}
// Kind not present at all
gotUnknown := users.GetByKinds([]api.OwnerKind{api.OwnerKind("does-not-exist")})
if gotUnknown != nil {
t.Fatalf("GetByKinds([unknown]) = %v, want nil", gotUnknown)
}
}
func TestGetByKinds_Randomized(t *testing.T) {
rnd := rand.New(rand.NewSource(123))
ownerKinds := []api.OwnerKind{
api.UserOwner,
api.GroupOwner,
api.ServiceAccountOwner,
}
const (
numLists = 200
maxOwnersPerList = 50
)
for listIdx := 0; listIdx < numLists; listIdx++ {
var users api.UserListSpec
n := rnd.Intn(maxOwnersPerList)
for i := 0; i < n; i++ {
k := ownerKinds[rnd.Intn(len(ownerKinds))]
users = append(users, api.UserSpec{
Name: randomName(rnd, 3+rnd.Intn(4)), // reuse your helper
Kind: k,
})
}
// Try several random kind-subsets per list
for subsetIdx := 0; subsetIdx < 10; subsetIdx++ {
// Build a random subset of kinds
var kinds []api.OwnerKind
for _, k := range ownerKinds {
if rnd.Float64() < 0.5 {
kinds = append(kinds, k)
}
}
got := users.GetByKinds(kinds)
// Reference implementation: filter + sort
kindSet := make(map[api.OwnerKind]struct{}, len(kinds))
for _, k := range kinds {
kindSet[k] = struct{}{}
}
var want []string
if len(kinds) > 0 {
for _, u := range users {
if _, ok := kindSet[u.Kind]; ok {
want = append(want, u.Name)
}
}
}
if len(want) == 0 {
if got != nil {
t.Fatalf("list=%d subset=%d: expected nil, got %v (kinds=%v, users=%v)",
listIdx, subsetIdx, got, kinds, users)
}
continue
}
sort.Strings(want)
sort.Strings(got)
if !reflect.DeepEqual(got, want) {
t.Fatalf("list=%d subset=%d: GetByKinds mismatch\n kinds=%v\n got= %v\n want= %v",
listIdx, subsetIdx, kinds, got, want)
}
}
}
}

View File

@@ -30,7 +30,7 @@ func NewCapsuleConfiguration(ctx context.Context, client client.Client, name str
if apierrors.IsNotFound(err) {
return &capsulev1beta2.CapsuleConfiguration{
Spec: capsulev1beta2.CapsuleConfigurationSpec{
UserGroups: []string{"projectcapsule.dev"},
Users: []capsuleapi.UserSpec{{Name: "projectcapsule.dev", Kind: capsuleapi.GroupOwner}},
ForceTenantPrefix: false,
ProtectedNamespaceRegexpString: "",
},
@@ -86,12 +86,14 @@ func (c *capsuleConfiguration) ValidatingWebhookConfigurationName() (name string
return c.retrievalFn().Spec.CapsuleResources.ValidatingWebhookConfigurationName
}
//nolint:staticcheck
func (c *capsuleConfiguration) UserGroups() []string {
return c.retrievalFn().Spec.UserGroups
return append(c.retrievalFn().Spec.UserGroups, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.GroupOwner})...)
}
//nolint:staticcheck
func (c *capsuleConfiguration) UserNames() []string {
return c.retrievalFn().Spec.UserNames
return append(c.retrievalFn().Spec.UserNames, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.UserOwner, capsuleapi.ServiceAccountOwner})...)
}
func (c *capsuleConfiguration) IgnoreUserWithGroups() []string {

View File

@@ -73,6 +73,7 @@ func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.T
func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
annotations := make(map[string]string)
//nolint:staticcheck
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
maps.Copy(annotations, md.AdditionalMetadata.Annotations)
}
@@ -86,6 +87,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
annotations[meta.AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",")
}
//nolint:staticcheck
if len(ic.Regex) > 0 {
annotations[meta.AvailableIngressClassesRegexpAnnotation] = ic.Regex
}
@@ -96,6 +98,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
annotations[meta.AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",")
}
//nolint:staticcheck
if len(sc.Regex) > 0 {
annotations[meta.AvailableStorageClassesRegexpAnnotation] = sc.Regex
}
@@ -106,6 +109,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
annotations[meta.AllowedRegistriesAnnotation] = strings.Join(cr.Exact, ",")
}
//nolint:staticcheck
if len(cr.Regex) > 0 {
annotations[meta.AllowedRegistriesRegexpAnnotation] = cr.Regex
}
@@ -128,6 +132,7 @@ func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
func BuildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
labels := make(map[string]string)
//nolint:staticcheck
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
maps.Copy(labels, md.AdditionalMetadata.Labels)
}