mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 18:09:58 +00:00
feat: refactor core webhooks (#1756)
* feat(webhook): add watchdog webhook to core Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix(controller): ensure managed metadata for namespaces on update Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore(controller): refactor core webhooks to generics Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: fix helm plugin installation Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: rename webhook to tenant-label Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
@@ -54,6 +54,8 @@ release:
|
||||
footer: |
|
||||
**Full Changelog**: https://github.com/projectcapsule/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
[Check out what's new in this release](https://projectcapsule.dev/docs/whats-new/)
|
||||
|
||||
**Docker Images**
|
||||
- `ghcr.io/projectcapsule/{{ .ProjectName }}:{{ .Version }}`
|
||||
- `ghcr.io/projectcapsule/{{ .ProjectName }}:latest`
|
||||
|
||||
2
Makefile
2
Makefile
@@ -337,7 +337,7 @@ $(LOCALBIN):
|
||||
|
||||
HELM_SCHEMA_VERSION := ""
|
||||
helm-plugin-schema:
|
||||
@$(HELM) plugin install https://github.com/losisin/helm-values-schema-json.git --version $(HELM_SCHEMA_VERSION) || true
|
||||
@$(HELM) plugin install https://github.com/losisin/helm-values-schema-json.git --version $(HELM_SCHEMA_VERSION) --verify=false || true
|
||||
|
||||
HELM_DOCS := $(LOCALBIN)/helm-docs
|
||||
HELM_DOCS_VERSION := v1.14.1
|
||||
|
||||
@@ -253,6 +253,14 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.services.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.services.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.services.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.tenantLabel.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.tenantLabel.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.tenantLabel.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.tenantLabel.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.tenantLabel.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.tenantLabel.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.tenantLabel.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
|
||||
| webhooks.hooks.tenantLabel.rules | list | `[{"apiGroups":["*"],"apiVersions":["*"],"operations":["CREATE","UPDATE"],"resources":["*"],"scope":"Namespaced"}]` | [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules) |
|
||||
| webhooks.hooks.tenantResourceObjects.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.tenantResourceObjects.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.tenantResourceObjects.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
|
||||
@@ -313,5 +313,33 @@ webhooks:
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.webhooks.hooks.tenantLabel }}
|
||||
{{- if .enabled }}
|
||||
- name: assignment.misc.projectcapsule.dev
|
||||
admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/misc/tenant-label" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
reinvocationPolicy: {{ .reinvocationPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
namespaceSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .objectSelector }}
|
||||
objectSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .matchConditions }}
|
||||
matchConditions:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- toYaml .rules | nindent 4 }}
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1338,6 +1338,91 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tenantLabel": {
|
||||
"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",
|
||||
"properties": {
|
||||
"matchExpressions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"rules": {
|
||||
"description": "[Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiGroups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"apiVersions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tenantResourceObjects": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -405,6 +405,36 @@ webhooks:
|
||||
|
||||
# Admission Webhook Configuration
|
||||
hooks:
|
||||
tenantLabel:
|
||||
# -- Enable the Hook
|
||||
enabled: true
|
||||
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
|
||||
failurePolicy: Fail
|
||||
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchPolicy: Equivalent
|
||||
# -- [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:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
# -- [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
|
||||
# -- [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
apiVersions:
|
||||
- '*'
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- '*'
|
||||
scope: Namespaced
|
||||
|
||||
resourcepools:
|
||||
pools:
|
||||
|
||||
35
cmd/main.go
35
cmd/main.go
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/projectcapsule/capsule/internal/webhook/defaults"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/gateway"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/ingress"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/misc"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/namespace"
|
||||
namespacemutation "github.com/projectcapsule/capsule/internal/webhook/namespace/mutation"
|
||||
namespacevalidation "github.com/projectcapsule/capsule/internal/webhook/namespace/validation"
|
||||
@@ -236,15 +237,35 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
webhooksList := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(cfg), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Pod(
|
||||
pod.Handler(
|
||||
pod.ImagePullPolicy(),
|
||||
pod.ContainerRegistry(cfg),
|
||||
pod.PriorityClass(),
|
||||
pod.RuntimeClass(),
|
||||
),
|
||||
),
|
||||
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.PVC(pvc.Validating(), pvc.PersistentVolumeReuse()),
|
||||
route.Service(service.Handler()),
|
||||
route.PVC(
|
||||
pvc.Handler(
|
||||
pvc.Validating(),
|
||||
pvc.PersistentVolumeReuse(),
|
||||
),
|
||||
),
|
||||
route.Service(
|
||||
service.Handler(
|
||||
service.Validating(),
|
||||
),
|
||||
),
|
||||
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
route.ServiceAccounts(serviceaccounts.Handler(cfg)),
|
||||
route.ServiceAccounts(
|
||||
serviceaccounts.Handler(
|
||||
serviceaccounts.Validating(cfg),
|
||||
),
|
||||
),
|
||||
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
|
||||
route.Gateway(gateway.Class(cfg)),
|
||||
route.Defaults(defaults.Handler(cfg, kubeVersion)),
|
||||
@@ -262,6 +283,7 @@ func main() {
|
||||
tenantvalidation.ServiceAccountNameHandler(),
|
||||
tenantvalidation.ForbiddenAnnotationsRegexHandler(),
|
||||
tenantvalidation.ProtectedHandler(),
|
||||
tenantvalidation.WarningHandler(),
|
||||
),
|
||||
route.NamespaceValidation(
|
||||
namespace.NamespaceHandler(
|
||||
@@ -277,14 +299,17 @@ func main() {
|
||||
namespace.NamespaceHandler(
|
||||
cfg,
|
||||
namespacemutation.OwnerReferenceHandler(cfg),
|
||||
namespacemutation.CordoningLabelHandler(cfg),
|
||||
namespacemutation.MetadataHandler(cfg),
|
||||
namespacemutation.CordoningLabelHandler(cfg),
|
||||
),
|
||||
),
|
||||
route.ResourcePoolMutation((resourcepool.PoolMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
|
||||
route.ResourcePoolValidation((resourcepool.PoolValidationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
|
||||
route.ResourcePoolClaimMutation((resourcepool.ClaimMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepoolclaims")))),
|
||||
route.ResourcePoolClaimValidation((resourcepool.ClaimValidationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepoolclaims")))),
|
||||
route.TenantAssignment(
|
||||
misc.TenantAssignmentHandler(),
|
||||
),
|
||||
)
|
||||
|
||||
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasttemplate"
|
||||
"golang.org/x/sync/errgroup"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -21,7 +19,7 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
||||
@@ -140,8 +138,6 @@ func (r *Manager) reconcileMetadata(
|
||||
managed *capsulev1beta2.TenantStatusNamespaceMetadata,
|
||||
err error,
|
||||
) {
|
||||
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
|
||||
originLabels := ns.GetLabels()
|
||||
if originLabels == nil {
|
||||
originLabels = make(map[string]string)
|
||||
@@ -152,28 +148,9 @@ func (r *Manager) reconcileMetadata(
|
||||
originAnnotations = make(map[string]string)
|
||||
}
|
||||
|
||||
managedAnnotations := buildNamespaceAnnotationsForTenant(tnt)
|
||||
managedLabels := buildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
var ok bool
|
||||
|
||||
ok, err = utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return managed, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
applyTemplateMap(md.Labels, tnt, ns)
|
||||
applyTemplateMap(md.Annotations, tnt, ns)
|
||||
|
||||
utils.MapMergeNoOverrite(managedLabels, md.Labels)
|
||||
utils.MapMergeNoOverrite(managedAnnotations, md.Annotations)
|
||||
}
|
||||
managedLabels, managedAnnotations, err := tenant.BuildNamespaceMetadataForTenant(ns, tnt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
||||
@@ -217,8 +194,8 @@ func (r *Manager) reconcileMetadata(
|
||||
originAnnotations = managedAnnotations
|
||||
}
|
||||
|
||||
originLabels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
originLabels[capsuleLabel] = tnt.GetName()
|
||||
tenant.AddNamespaceNameLabels(originLabels, ns)
|
||||
tenant.AddTenantNameLabel(originLabels, ns, tnt)
|
||||
|
||||
ns.SetLabels(originLabels)
|
||||
ns.SetAnnotations(originAnnotations)
|
||||
@@ -226,75 +203,6 @@ func (r *Manager) reconcileMetadata(
|
||||
return managed, err
|
||||
}
|
||||
|
||||
func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
annotations := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(annotations, md.AdditionalMetadata.Annotations)
|
||||
}
|
||||
|
||||
if tnt.Spec.NodeSelector != nil {
|
||||
annotations = utils.BuildNodeSelector(tnt, annotations)
|
||||
}
|
||||
|
||||
if ic := tnt.Spec.IngressOptions.AllowedClasses; ic != nil {
|
||||
if len(ic.Exact) > 0 {
|
||||
annotations[meta.AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",")
|
||||
}
|
||||
|
||||
if len(ic.Regex) > 0 {
|
||||
annotations[meta.AvailableIngressClassesRegexpAnnotation] = ic.Regex
|
||||
}
|
||||
}
|
||||
|
||||
if sc := tnt.Spec.StorageClasses; sc != nil {
|
||||
if len(sc.Exact) > 0 {
|
||||
annotations[meta.AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",")
|
||||
}
|
||||
|
||||
if len(sc.Regex) > 0 {
|
||||
annotations[meta.AvailableStorageClassesRegexpAnnotation] = sc.Regex
|
||||
}
|
||||
}
|
||||
|
||||
if cr := tnt.Spec.ContainerRegistries; cr != nil {
|
||||
if len(cr.Exact) > 0 {
|
||||
annotations[meta.AllowedRegistriesAnnotation] = strings.Join(cr.Exact, ",")
|
||||
}
|
||||
|
||||
if len(cr.Regex) > 0 {
|
||||
annotations[meta.AllowedRegistriesRegexpAnnotation] = cr.Regex
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
meta.ForbiddenNamespaceLabelsAnnotation,
|
||||
meta.ForbiddenNamespaceLabelsRegexpAnnotation,
|
||||
meta.ForbiddenNamespaceAnnotationsAnnotation,
|
||||
meta.ForbiddenNamespaceAnnotationsRegexpAnnotation,
|
||||
} {
|
||||
if value, ok := tnt.Annotations[key]; ok {
|
||||
annotations[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return annotations
|
||||
}
|
||||
|
||||
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
labels[meta.CordonedLabel] = "true"
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
|
||||
@@ -309,20 +217,3 @@ func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// applyTemplateMap applies templating to all values in the provided map in place.
|
||||
func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
|
||||
for k, v := range m {
|
||||
if !strings.Contains(v, "{{ ") && !strings.Contains(v, " }}") {
|
||||
continue
|
||||
}
|
||||
|
||||
t := fasttemplate.New(v, "{{ ", " }}")
|
||||
tmplString := t.ExecuteString(map[string]interface{}{
|
||||
"tenant.name": tnt.Name,
|
||||
"namespace": ns.Name,
|
||||
})
|
||||
|
||||
m[k] = tmplString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type Func func(ctx context.Context, req admission.Request) *admission.Response
|
||||
@@ -19,8 +21,20 @@ type Handler interface {
|
||||
OnUpdate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) Func
|
||||
}
|
||||
|
||||
type HanderWithTenant interface {
|
||||
OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
OnDelete(c client.Client, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
}
|
||||
|
||||
type TypedHandler[T client.Object] interface {
|
||||
OnCreate(c client.Client, obj T, decoder admission.Decoder, recorder record.EventRecorder) Func
|
||||
OnUpdate(c client.Client, obj T, old T, decoder admission.Decoder, recorder record.EventRecorder) Func
|
||||
OnDelete(c client.Client, obj T, decoder admission.Decoder, recorder record.EventRecorder) Func
|
||||
}
|
||||
|
||||
type TypedHandlerWithTenant[T client.Object] interface {
|
||||
OnCreate(c client.Client, obj T, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
OnUpdate(c client.Client, obj T, old T, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
OnDelete(c client.Client, obj T, decoder admission.Decoder, recorder record.EventRecorder, tnt *capsulev1beta2.Tenant) Func
|
||||
}
|
||||
|
||||
84
internal/webhook/misc/tenant_assignment.go
Normal file
84
internal/webhook/misc/tenant_assignment.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package misc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type tenantAssignmentHandler struct{}
|
||||
|
||||
func TenantAssignmentHandler() capsulewebhook.Handler {
|
||||
return &tenantAssignmentHandler{}
|
||||
}
|
||||
|
||||
func (r *tenantAssignmentHandler) OnCreate(c client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handle(ctx, c, decoder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tenantAssignmentHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tenantAssignmentHandler) OnUpdate(c client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handle(ctx, c, decoder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tenantAssignmentHandler) handle(ctx context.Context, c client.Client, decoder admission.Decoder, req admission.Request) *admission.Response {
|
||||
if req.Namespace == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
if err := decoder.Decode(req, obj); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, req.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = map[string]string{}
|
||||
}
|
||||
|
||||
if currentValue, exists := labels[meta.ManagedByCapsuleLabel]; exists && currentValue == tnt.GetName() {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels[meta.ManagedByCapsuleLabel] = tnt.GetName()
|
||||
obj.SetLabels(labels)
|
||||
|
||||
marshaledObj, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaledObj)
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type metadataHandler struct {
|
||||
@@ -31,39 +31,32 @@ func MetadataHandler(cfg configuration.Configuration) capsulewebhook.TypedHandle
|
||||
|
||||
func (h *metadataHandler) OnCreate(client client.Client, ns *corev1.Namespace, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tenant, errResponse := utils.GetNamespaceTenant(ctx, client, ns, req, h.cfg, recorder)
|
||||
tnt, errResponse := utils.GetNamespaceTenant(ctx, client, ns, req, h.cfg, recorder)
|
||||
if errResponse != nil {
|
||||
return errResponse
|
||||
}
|
||||
|
||||
if tenant == nil {
|
||||
if tnt == nil {
|
||||
response := admission.Denied("Unable to assign namespace to tenant.")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// sync namespace metadata
|
||||
instance := tenant.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
|
||||
if len(instance.Metadata.Labels) == 0 && len(instance.Metadata.Annotations) == 0 {
|
||||
return nil
|
||||
labels, annotations, err := tenant.BuildNamespaceMetadataForTenant(ns, tnt)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
for k, v := range instance.Metadata.Labels {
|
||||
labels[k] = v
|
||||
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
||||
if managedMetadataOnly {
|
||||
labels = mergeStringMap(ns.GetLabels(), labels)
|
||||
annotations = mergeStringMap(ns.GetAnnotations(), annotations)
|
||||
}
|
||||
|
||||
tenant.AddNamespaceNameLabels(labels, ns)
|
||||
tenant.AddTenantNameLabel(labels, ns, tnt)
|
||||
|
||||
ns.SetLabels(labels)
|
||||
|
||||
annotations := ns.GetAnnotations()
|
||||
for k, v := range instance.Metadata.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
|
||||
ns.SetAnnotations(annotations)
|
||||
|
||||
marshaled, err := json.Marshal(ns)
|
||||
@@ -85,8 +78,68 @@ func (h *metadataHandler) OnDelete(client.Client, *corev1.Namespace, admission.D
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metadataHandler) OnUpdate(client.Client, *corev1.Namespace, *corev1.Namespace, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
func (h *metadataHandler) OnUpdate(c client.Client, newNs *corev1.Namespace, oldNs *corev1.Namespace, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, errResponse := utils.GetNamespaceTenant(ctx, c, oldNs, req, h.cfg, recorder)
|
||||
if errResponse != nil {
|
||||
return errResponse
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
response := admission.Denied("Unable to assign namespace to tenant.")
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
o, err := json.Marshal(newNs.DeepCopy())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
labels, annotations, err := tenant.BuildNamespaceMetadataForTenant(newNs, tnt)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
||||
if !managedMetadataOnly {
|
||||
labels = mergeStringMap(newNs.GetLabels(), labels)
|
||||
annotations = mergeStringMap(newNs.GetAnnotations(), annotations)
|
||||
}
|
||||
|
||||
tenant.AddNamespaceNameLabels(labels, oldNs)
|
||||
tenant.AddTenantNameLabel(labels, oldNs, tnt)
|
||||
|
||||
newNs.SetLabels(labels)
|
||||
newNs.SetAnnotations(annotations)
|
||||
|
||||
obj, err := json.Marshal(newNs)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
response := admission.PatchResponseFromRaw(o, obj)
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func mergeStringMap(dst, src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return dst
|
||||
}
|
||||
|
||||
if dst == nil {
|
||||
dst = make(map[string]string, len(src))
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
@@ -46,11 +47,10 @@ func (h *ownerReferenceHandler) OnCreate(c client.Client, ns *corev1.Namespace,
|
||||
return &response
|
||||
}
|
||||
|
||||
if err := tenant.AddTenantLabelForNamespace(ns, tnt); err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
labels := ns.GetLabels()
|
||||
tenant.AddNamespaceNameLabels(labels, ns)
|
||||
tenant.AddTenantNameLabel(labels, ns, tnt)
|
||||
ns.SetLabels(labels)
|
||||
|
||||
response := patchResponseForOwnerRef(c, tnt.DeepCopy(), ns, recorder)
|
||||
|
||||
@@ -66,7 +66,7 @@ func (h *ownerReferenceHandler) OnDelete(client.Client, *corev1.Namespace, admis
|
||||
|
||||
func (h *ownerReferenceHandler) OnUpdate(c client.Client, newNs *corev1.Namespace, oldNs *corev1.Namespace, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := tenant.GetTenantByOwnerreferences(ctx, c, oldNs.OwnerReferences)
|
||||
tnt, err := resolveTenantForNamespaceUpdate(ctx, c, h.cfg, oldNs, newNs, req.UserInfo)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
@@ -75,11 +75,8 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, newNs *corev1.Namespac
|
||||
return nil
|
||||
}
|
||||
|
||||
o, err := json.Marshal(newNs.DeepCopy())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
if err := assignToTenant(c, tnt, oldNs, recorder); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var refs []metav1.OwnerReference
|
||||
@@ -98,28 +95,78 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, newNs *corev1.Namespac
|
||||
|
||||
newNs.OwnerReferences = refs
|
||||
|
||||
if err := tenant.AddTenantLabelForNamespace(newNs, tnt); err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
labels := newNs.GetLabels()
|
||||
tenant.AddNamespaceNameLabels(labels, oldNs)
|
||||
tenant.AddTenantNameLabel(labels, oldNs, tnt)
|
||||
newNs.SetLabels(labels)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
obj, err := json.Marshal(newNs)
|
||||
marshaled, err := json.Marshal(newNs)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
response := admission.PatchResponseFromRaw(o, obj)
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
func resolveTenantForNamespaceUpdate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
cfg configuration.Configuration,
|
||||
oldNs, newNs *corev1.Namespace,
|
||||
userInfo authenticationv1.UserInfo,
|
||||
) (*capsulev1beta2.Tenant, error) {
|
||||
// 1) try old ownerRefs
|
||||
if tnt, err := tenant.GetTenantByOwnerreferences(ctx, c, oldNs.OwnerReferences); err != nil {
|
||||
return nil, err
|
||||
} else if tnt != nil {
|
||||
return tnt, nil
|
||||
}
|
||||
|
||||
// 2) try new ownerRefs
|
||||
if tnt, err := tenant.GetTenantByOwnerreferences(ctx, c, newNs.OwnerReferences); err != nil {
|
||||
return nil, err
|
||||
} else if tnt != nil {
|
||||
return tnt, nil
|
||||
}
|
||||
|
||||
// 3) fall back to labels + user
|
||||
return tenant.GetTenantByLabelsAndUser(ctx, c, cfg, newNs, userInfo)
|
||||
}
|
||||
|
||||
func assignToTenant(
|
||||
c client.Client,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
ns *corev1.Namespace,
|
||||
recorder record.EventRecorder,
|
||||
) error {
|
||||
has, err := controllerutil.HasOwnerReference(ns.OwnerReferences, tnt, c.Scheme())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := controllerutil.SetOwnerReference(tnt, ns, c.Scheme()); err != nil {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "Error", "Namespace %s cannot be assigned to the desired Tenant", ns.GetName())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeNormal, "NamespaceCreationWebhook", "Namespace %s has been assigned to the desired Tenant", ns.GetName())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func patchResponseForOwnerRef(
|
||||
c client.Client,
|
||||
tenant *capsulev1beta2.Tenant,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
ns *corev1.Namespace,
|
||||
recorder record.EventRecorder,
|
||||
) admission.Response {
|
||||
@@ -128,14 +175,10 @@ func patchResponseForOwnerRef(
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
if err = controllerutil.SetOwnerReference(tenant, ns, c.Scheme()); err != nil {
|
||||
recorder.Eventf(tenant, corev1.EventTypeWarning, "Error", "Namespace %s cannot be assigned to the desired Tenant", ns.GetName())
|
||||
|
||||
if err := assignToTenant(c, tnt, ns, recorder); err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
recorder.Eventf(tenant, corev1.EventTypeNormal, "NamespaceCreationWebhook", "Namespace %s has been assigned to the desired Tenant", ns.GetName())
|
||||
|
||||
obj, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
@@ -13,61 +13,62 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type containerRegistryHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func ContainerRegistry(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
func ContainerRegistry(configuration configuration.Configuration) capsulewebhook.TypedHandlerWithTenant[*corev1.Pod] {
|
||||
return &containerRegistryHandler{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *containerRegistryHandler) OnCreate(
|
||||
c client.Client,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.validate(ctx, c, decoder, recorder, req)
|
||||
return h.validate(req, pod, tnt, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *containerRegistryHandler) OnUpdate(
|
||||
c client.Client,
|
||||
old *corev1.Pod,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.validate(req, pod, tnt, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnDelete(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ust be validate on update events since updates to pods on spec.containers[*].image and spec.initContainers[*].image are allowed.
|
||||
func (h *containerRegistryHandler) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.validate(ctx, c, decoder, recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
pod *corev1.Pod,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
recorder record.EventRecorder,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, pod.GetNamespace())
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tnt.Spec.ContainerRegistries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
20
internal/webhook/pod/handler.go
Normal file
20
internal/webhook/pod/handler.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pod
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
)
|
||||
|
||||
func Handler(handlers ...webhook.TypedHandlerWithTenant[*corev1.Pod]) webhook.Handler {
|
||||
return &utils.TypedTenantHandler[*corev1.Pod]{
|
||||
Factory: func() *corev1.Pod {
|
||||
return &corev1.Pod{}
|
||||
},
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
@@ -13,55 +13,57 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type imagePullPolicy struct{}
|
||||
|
||||
func ImagePullPolicy() capsulewebhook.Handler {
|
||||
func ImagePullPolicy() capsulewebhook.TypedHandlerWithTenant[*corev1.Pod] {
|
||||
return &imagePullPolicy{}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *imagePullPolicy) OnCreate(
|
||||
c client.Client,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
return h.validate(req, pod, tnt, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *imagePullPolicy) OnUpdate(
|
||||
c client.Client,
|
||||
old *corev1.Pod,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
return h.validate(req, pod, tnt, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *imagePullPolicy) OnDelete(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *imagePullPolicy) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
pod *corev1.Pod,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
recorder record.EventRecorder,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, pod.GetNamespace())
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
policy := NewPullPolicy(tnt)
|
||||
if policy == nil {
|
||||
return nil
|
||||
|
||||
@@ -12,33 +12,25 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type priorityClass struct{}
|
||||
|
||||
func PriorityClass() capsulewebhook.Handler {
|
||||
func PriorityClass() capsulewebhook.TypedHandlerWithTenant[*corev1.Pod] {
|
||||
return &priorityClass{}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *priorityClass) OnCreate(
|
||||
c client.Client,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, pod.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.PriorityClasses
|
||||
|
||||
if allowed == nil {
|
||||
@@ -85,13 +77,26 @@ func (h *priorityClass) OnCreate(c client.Client, decoder admission.Decoder, rec
|
||||
}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *priorityClass) OnUpdate(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *priorityClass) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *priorityClass) OnDelete(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,30 +14,48 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type runtimeClass struct{}
|
||||
|
||||
func RuntimeClass() capsulewebhook.Handler {
|
||||
func RuntimeClass() capsulewebhook.TypedHandlerWithTenant[*corev1.Pod] {
|
||||
return &runtimeClass{}
|
||||
}
|
||||
|
||||
func (h *runtimeClass) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *runtimeClass) OnCreate(
|
||||
c client.Client,
|
||||
pod *corev1.Pod,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return h.validate(ctx, c, decoder, recorder, req)
|
||||
return h.validate(ctx, c, recorder, req, pod, tnt)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *runtimeClass) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *runtimeClass) OnUpdate(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *runtimeClass) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *runtimeClass) OnDelete(
|
||||
client.Client,
|
||||
*corev1.Pod,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
@@ -56,21 +74,14 @@ func (h *runtimeClass) class(ctx context.Context, c client.Client, name string)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (h *runtimeClass) validate(ctx context.Context, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, req admission.Request) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, pod.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *runtimeClass) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
pod *corev1.Pod,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
allowed := tnt.Spec.RuntimeClasses
|
||||
|
||||
runtimeClassName := ""
|
||||
|
||||
20
internal/webhook/pvc/handler.go
Normal file
20
internal/webhook/pvc/handler.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pvc
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
)
|
||||
|
||||
func Handler(handlers ...webhook.TypedHandlerWithTenant[*corev1.PersistentVolumeClaim]) webhook.Handler {
|
||||
return &utils.TypedTenantHandler[*corev1.PersistentVolumeClaim]{
|
||||
Factory: func() *corev1.PersistentVolumeClaim {
|
||||
return &corev1.PersistentVolumeClaim{}
|
||||
},
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
@@ -17,51 +17,37 @@ import (
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
)
|
||||
|
||||
type PV struct {
|
||||
capsuleLabel string
|
||||
type pv struct{}
|
||||
|
||||
func PersistentVolumeReuse() capsulewebhook.TypedHandlerWithTenant[*corev1.PersistentVolumeClaim] {
|
||||
return &pv{}
|
||||
}
|
||||
|
||||
func PersistentVolumeReuse() capsulewebhook.Handler {
|
||||
value, err := capsulev1beta2.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("this shouldn't happen: %s", err.Error()))
|
||||
}
|
||||
|
||||
return &PV{
|
||||
capsuleLabel: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PV) OnCreate(client client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
func (h pv) OnCreate(
|
||||
c client.Client,
|
||||
pvc *corev1.PersistentVolumeClaim,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pvc := corev1.PersistentVolumeClaim{}
|
||||
if err := decoder.Decode(req, &pvc); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, client, pvc.GetNamespace())
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
// PVC is not in a Tenant Namespace, skipping
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
// A PersistentVolume selector cannot help in preventing a cross-tenant mount:
|
||||
// thus, disallowing that in first place.
|
||||
if pvc.Spec.Selector != nil {
|
||||
return utils.ErroredResponse(NewPVSelectorError())
|
||||
}
|
||||
|
||||
// The PVC hasn't any volumeName pre-claimed, it can be skipped
|
||||
if len(pvc.Spec.VolumeName) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checking if the PV is labelled with the Tenant name
|
||||
pv := corev1.PersistentVolume{}
|
||||
if err = client.Get(ctx, types.NamespacedName{Name: pvc.Spec.VolumeName}, &pv); err != nil {
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: pvc.Spec.VolumeName}, &pv); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
err = fmt.Errorf("cannot create a PVC referring to a not yet existing PV")
|
||||
}
|
||||
@@ -73,7 +59,7 @@ func (p PV) OnCreate(client client.Client, decoder admission.Decoder, _ record.E
|
||||
return utils.ErroredResponse(NewMissingPVLabelsError(pv.GetName()))
|
||||
}
|
||||
|
||||
value, ok := pv.GetLabels()[p.capsuleLabel]
|
||||
value, ok := pv.GetLabels()[meta.TenantLabel]
|
||||
if !ok {
|
||||
return utils.ErroredResponse(NewMissingTenantPVLabelsError(pv.GetName()))
|
||||
}
|
||||
@@ -86,13 +72,26 @@ func (p PV) OnCreate(client client.Client, decoder admission.Decoder, _ record.E
|
||||
}
|
||||
}
|
||||
|
||||
func (p PV) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h pv) OnUpdate(
|
||||
client.Client,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p PV) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h pv) OnDelete(
|
||||
client.Client,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,33 +13,25 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type validating struct{}
|
||||
|
||||
func Validating() capsulewebhook.Handler {
|
||||
func Validating() capsulewebhook.TypedHandlerWithTenant[*corev1.PersistentVolumeClaim] {
|
||||
return &validating{}
|
||||
}
|
||||
|
||||
func (h *validating) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnCreate(
|
||||
c client.Client,
|
||||
pvc *corev1.PersistentVolumeClaim,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
if err := decoder.Decode(req, pvc); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, c, pvc.Namespace)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.StorageClasses
|
||||
|
||||
if allowed == nil {
|
||||
@@ -88,13 +80,26 @@ func (h *validating) OnCreate(c client.Client, decoder admission.Decoder, record
|
||||
}
|
||||
}
|
||||
|
||||
func (h *validating) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnUpdate(
|
||||
client.Client,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *validating) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnDelete(
|
||||
client.Client,
|
||||
*corev1.PersistentVolumeClaim,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
|
||||
24
internal/webhook/route/misc.go
Normal file
24
internal/webhook/route/misc.go
Normal 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 miscTenantAssignment struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func TenantAssignment(handlers ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &miscTenantAssignment{handlers: handlers}
|
||||
}
|
||||
|
||||
func (w miscTenantAssignment) GetPath() string {
|
||||
return "/misc/tenant-label"
|
||||
}
|
||||
|
||||
func (w miscTenantAssignment) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package route
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package route
|
||||
|
||||
import (
|
||||
|
||||
20
internal/webhook/service/handler.go
Normal file
20
internal/webhook/service/handler.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
)
|
||||
|
||||
func Handler(handlers ...webhook.TypedHandlerWithTenant[*corev1.Service]) webhook.Handler {
|
||||
return &utils.TypedTenantHandler[*corev1.Service]{
|
||||
Factory: func() *corev1.Service {
|
||||
return &corev1.Service{}
|
||||
},
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
@@ -14,51 +14,60 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type handler struct{}
|
||||
type validating struct{}
|
||||
|
||||
func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
func Validating() capsulewebhook.TypedHandlerWithTenant[*corev1.Service] {
|
||||
return &validating{}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnCreate(
|
||||
c client.Client,
|
||||
svc *corev1.Service,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handleService(ctx, client, decoder, req, recorder)
|
||||
return h.handle(req, recorder, svc, tnt)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnUpdate(
|
||||
c client.Client,
|
||||
old *corev1.Service,
|
||||
svc *corev1.Service,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handleService(ctx, client, decoder, req, recorder)
|
||||
return h.handle(req, recorder, svc, tnt)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnDelete(
|
||||
client.Client,
|
||||
*corev1.Service,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) handleService(ctx context.Context, clt client.Client, decoder admission.Decoder, req admission.Request, recorder record.EventRecorder) *admission.Response {
|
||||
svc := &corev1.Service{}
|
||||
if err := decoder.Decode(req, svc); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, clt, svc.GetNamespace())
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validating) handle(
|
||||
req admission.Request,
|
||||
recorder record.EventRecorder,
|
||||
svc *corev1.Service,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.Spec.ServiceOptions != nil && tnt.Spec.ServiceOptions.AllowedServices != nil && !*tnt.Spec.ServiceOptions.AllowedServices.NodePort {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNodePort", "Service %s/%s cannot be type of NodePort for the current Tenant", req.Namespace, req.Name)
|
||||
|
||||
|
||||
20
internal/webhook/serviceaccounts/handler.go
Normal file
20
internal/webhook/serviceaccounts/handler.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package serviceaccounts
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/internal/webhook/utils"
|
||||
)
|
||||
|
||||
func Handler(handlers ...webhook.TypedHandlerWithTenant[*corev1.ServiceAccount]) webhook.Handler {
|
||||
return &utils.TypedTenantHandler[*corev1.ServiceAccount]{
|
||||
Factory: func() *corev1.ServiceAccount {
|
||||
return &corev1.ServiceAccount{}
|
||||
},
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
@@ -11,61 +11,72 @@ import (
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/users"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
type validating struct {
|
||||
cfg configuration.Configuration
|
||||
}
|
||||
|
||||
func Handler(cfg configuration.Configuration) capsulewebhook.Handler {
|
||||
return &handler{cfg: cfg}
|
||||
func Validating(cfg configuration.Configuration) capsulewebhook.TypedHandlerWithTenant[*corev1.ServiceAccount] {
|
||||
return &validating{cfg: cfg}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnCreate(
|
||||
c client.Client,
|
||||
sa *corev1.ServiceAccount,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handle(ctx, client, decoder, req, recorder)
|
||||
return h.handle(ctx, c, req, sa, tnt)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnUpdate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnUpdate(
|
||||
c client.Client,
|
||||
old *corev1.ServiceAccount,
|
||||
sa *corev1.ServiceAccount,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.handle(ctx, client, decoder, req, recorder)
|
||||
return h.handle(ctx, c, req, sa, tnt)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
func (h *validating) OnDelete(
|
||||
client.Client,
|
||||
*corev1.ServiceAccount,
|
||||
admission.Decoder,
|
||||
record.EventRecorder,
|
||||
*capsulev1beta2.Tenant,
|
||||
) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *handler) handle(ctx context.Context, clt client.Client, decoder admission.Decoder, req admission.Request, recorder record.EventRecorder) *admission.Response {
|
||||
sa := &corev1.ServiceAccount{}
|
||||
if err := decoder.Decode(req, sa); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tnt, err := tenant.TenantByStatusNamespace(ctx, clt, sa.GetNamespace())
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *validating) handle(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
req admission.Request,
|
||||
sa *corev1.ServiceAccount,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
_, hasOwnerPromotion := sa.Labels[meta.OwnerPromotionLabel]
|
||||
if !hasOwnerPromotion {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !r.cfg.AllowServiceAccountPromotion() {
|
||||
if !h.cfg.AllowServiceAccountPromotion() {
|
||||
response := admission.Denied(
|
||||
"service account owner promotion is disabled. Contact your system administrators",
|
||||
)
|
||||
@@ -74,7 +85,7 @@ func (r *handler) handle(ctx context.Context, clt client.Client, decoder admissi
|
||||
}
|
||||
|
||||
// We don't want to allow promoted serviceaccounts to promote other serviceaccounts
|
||||
allowed, err := users.IsTenantOwner(ctx, clt, r.cfg, tnt, req.UserInfo)
|
||||
allowed, err := users.IsTenantOwner(ctx, c, h.cfg, tnt, req.UserInfo)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
85
internal/webhook/tenant/validation/warnings.go
Normal file
85
internal/webhook/tenant/validation/warnings.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validation
|
||||
|
||||
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 {
|
||||
tenant := &capsulev1beta2.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
response := &admission.Response{
|
||||
AdmissionResponse: admissionv1.AdmissionResponse{
|
||||
UID: req.UID,
|
||||
Allowed: true,
|
||||
},
|
||||
}
|
||||
|
||||
if len(tenant.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.")
|
||||
}
|
||||
|
||||
if len(tenant.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.")
|
||||
}
|
||||
|
||||
if tenant.Spec.NamespaceOptions != nil && tenant.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.")
|
||||
}
|
||||
|
||||
if tenant.Spec.StorageClasses != nil && tenant.Spec.StorageClasses.Regex != "" {
|
||||
response.Warnings = append(response.Warnings, "Using the regex property to select StorageClasses is deprecated and will be removed int the future.")
|
||||
}
|
||||
|
||||
if tenant.Spec.GatewayOptions.AllowedClasses != nil && tenant.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.")
|
||||
}
|
||||
|
||||
if tenant.Spec.PriorityClasses != nil && tenant.Spec.PriorityClasses.Regex != "" {
|
||||
response.Warnings = append(response.Warnings, "Using the regex property to select PriorityClasses is deprecated and will be removed int the future.")
|
||||
}
|
||||
|
||||
if tenant.Spec.RuntimeClasses != nil && tenant.Spec.RuntimeClasses.Regex != "" {
|
||||
response.Warnings = append(response.Warnings, "Using the regex property to select RuntimeClasses is deprecated and will be removed int the future.")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
115
internal/webhook/utils/typed_tenant_handler.go
Normal file
115
internal/webhook/utils/typed_tenant_handler.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
"github.com/projectcapsule/capsule/internal/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/utils/tenant"
|
||||
)
|
||||
|
||||
type newObjectFunc[T client.Object] func() T
|
||||
|
||||
type TypedTenantHandler[T client.Object] struct {
|
||||
Factory newObjectFunc[T]
|
||||
Handlers []webhook.TypedHandlerWithTenant[T]
|
||||
}
|
||||
|
||||
func (h *TypedTenantHandler[T]) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := h.resolveTenant(ctx, c, req)
|
||||
if err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj := h.Factory()
|
||||
if err := decoder.Decode(req, obj); err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
for _, hndl := range h.Handlers {
|
||||
if response := hndl.OnCreate(c, obj, decoder, recorder, tnt)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TypedTenantHandler[T]) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := h.resolveTenant(ctx, c, req)
|
||||
if err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newObj := h.Factory()
|
||||
if err := decoder.Decode(req, newObj); err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
oldObj := h.Factory()
|
||||
if err := decoder.DecodeRaw(req.OldObject, oldObj); err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
for _, hndl := range h.Handlers {
|
||||
if response := hndl.OnUpdate(c, oldObj, newObj, decoder, recorder, tnt)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TypedTenantHandler[T]) OnDelete(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
tnt, err := h.resolveTenant(ctx, c, req)
|
||||
if err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj := h.Factory()
|
||||
if err := decoder.Decode(req, obj); err != nil {
|
||||
return ErroredResponse(err)
|
||||
}
|
||||
|
||||
for _, hndl := range h.Handlers {
|
||||
if response := hndl.OnDelete(c, obj, decoder, recorder, tnt)(ctx, req); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TypedTenantHandler[T]) resolveTenant(ctx context.Context, c client.Client, req admission.Request) (*capsulev1beta2.Tenant, error) {
|
||||
if req.Namespace == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return tenant.TenantByStatusNamespace(ctx, c, req.Namespace)
|
||||
}
|
||||
16
pkg/api/meta/ownership.go
Normal file
16
pkg/api/meta/ownership.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
const (
|
||||
CapsuleFieldOwnerPrefix = "capsule"
|
||||
)
|
||||
|
||||
func ControllerFieldOwner() string {
|
||||
return ControllerFieldOwnerPrefix("controller")
|
||||
}
|
||||
|
||||
func ControllerFieldOwnerPrefix(fieldowner string) string {
|
||||
return CapsuleFieldOwnerPrefix + "/" + fieldowner
|
||||
}
|
||||
30
pkg/template/fast.go
Normal file
30
pkg/template/fast.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasttemplate"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
// TemplateForTenantAndNamespace applies templating to all values in the provided map in place.
|
||||
func TemplateForTenantAndNamespace(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
|
||||
for k, v := range m {
|
||||
if !strings.Contains(v, "{{ ") && !strings.Contains(v, " }}") {
|
||||
continue
|
||||
}
|
||||
|
||||
t := fasttemplate.New(v, "{{ ", " }}")
|
||||
tmplString := t.ExecuteString(map[string]interface{}{
|
||||
"tenant.name": tnt.Name,
|
||||
"namespace": ns.Name,
|
||||
})
|
||||
|
||||
m[k] = tmplString
|
||||
}
|
||||
}
|
||||
110
pkg/template/fast_test.go
Normal file
110
pkg/template/fast_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func newTenant(name string) *capsulev1beta2.Tenant {
|
||||
return &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newNamespace(name string) *v1.Namespace {
|
||||
return &v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) {
|
||||
tnt := newTenant("tenant-a")
|
||||
ns := newNamespace("ns-1")
|
||||
|
||||
m := map[string]string{
|
||||
"key1": "tenant={{ tenant.name }}, ns={{ namespace }}",
|
||||
"key2": "plain-value",
|
||||
}
|
||||
|
||||
TemplateForTenantAndNamespace(m, tnt, ns)
|
||||
|
||||
if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" {
|
||||
t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
|
||||
}
|
||||
|
||||
if got := m["key2"]; got != "plain-value" {
|
||||
t.Fatalf("key2: expected %q to remain unchanged, got %q", "plain-value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateForTenantAndNamespace_SkipsValuesWithoutDelimiters(t *testing.T) {
|
||||
tnt := newTenant("tenant-a")
|
||||
ns := newNamespace("ns-1")
|
||||
|
||||
// Note: no space after '{{' and before '}}', so the guard should skip it
|
||||
m := map[string]string{
|
||||
"noTemplate1": "hello {{tenant.name}}",
|
||||
"noTemplate2": "namespace {{namespace}}",
|
||||
}
|
||||
|
||||
original1 := m["noTemplate1"]
|
||||
original2 := m["noTemplate2"]
|
||||
|
||||
TemplateForTenantAndNamespace(m, tnt, ns)
|
||||
|
||||
if got := m["noTemplate1"]; got != original1 {
|
||||
t.Fatalf("noTemplate1: expected %q to remain unchanged, got %q", original1, got)
|
||||
}
|
||||
if got := m["noTemplate2"]; got != original2 {
|
||||
t.Fatalf("noTemplate2: expected %q to remain unchanged, got %q", original2, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) {
|
||||
tnt := newTenant("tenant-x")
|
||||
ns := newNamespace("ns-x")
|
||||
|
||||
m := map[string]string{
|
||||
"onlyTenant": "T={{ tenant.name }}",
|
||||
"onlyNS": "N={{ namespace }}",
|
||||
"none": "static",
|
||||
}
|
||||
|
||||
TemplateForTenantAndNamespace(m, tnt, ns)
|
||||
|
||||
if got := m["onlyTenant"]; got != "T=tenant-x" {
|
||||
t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got)
|
||||
}
|
||||
if got := m["onlyNS"]; got != "N=ns-x" {
|
||||
t.Fatalf("onlyNS: expected %q, got %q", "N=ns-x", got)
|
||||
}
|
||||
if got := m["none"]; got != "static" {
|
||||
t.Fatalf("none: expected %q to remain unchanged, got %q", "static", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) {
|
||||
tnt := newTenant("tenant-a")
|
||||
ns := newNamespace("ns-1")
|
||||
|
||||
m := map[string]string{
|
||||
"unknown": "X={{ unknown.key }}",
|
||||
}
|
||||
|
||||
TemplateForTenantAndNamespace(m, tnt, ns)
|
||||
|
||||
// fasttemplate with missing key returns an empty string for that placeholder
|
||||
if got := m["unknown"]; got != "X=" {
|
||||
t.Fatalf("unknown: expected %q, got %q", "X=", got)
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,137 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/template"
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
func AddTenantLabelForNamespace(ns *corev1.Namespace, tnt *capsulev1beta2.Tenant) error {
|
||||
if ns.Labels == nil {
|
||||
ns.Labels = make(map[string]string)
|
||||
func AddNamespaceNameLabels(labels map[string]string, ns *corev1.Namespace) {
|
||||
labels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
}
|
||||
|
||||
func AddTenantNameLabel(labels map[string]string, ns *corev1.Namespace, tnt *capsulev1beta2.Tenant) {
|
||||
labels[meta.TenantLabel] = tnt.GetName()
|
||||
}
|
||||
|
||||
func BuildInstanceMetadataForNamespace(ns *corev1.Namespace, tnt *capsulev1beta2.Tenant) (labels map[string]string, annotations map[string]string) {
|
||||
annotations = make(map[string]string)
|
||||
labels = make(map[string]string)
|
||||
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
|
||||
if instance == nil {
|
||||
return labels, annotations
|
||||
}
|
||||
|
||||
ns.Labels[meta.TenantLabel] = tnt.GetName()
|
||||
annotations = instance.Metadata.Annotations
|
||||
labels = instance.Metadata.Labels
|
||||
|
||||
return nil
|
||||
return labels, annotations
|
||||
}
|
||||
|
||||
func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.Tenant) (labels map[string]string, annotations map[string]string, err error) {
|
||||
annotations = BuildNamespaceAnnotationsForTenant(tnt)
|
||||
labels = BuildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
var ok bool
|
||||
|
||||
ok, err = utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
template.TemplateForTenantAndNamespace(md.Labels, tnt, ns)
|
||||
template.TemplateForTenantAndNamespace(md.Annotations, tnt, ns)
|
||||
|
||||
utils.MapMergeNoOverrite(labels, md.Labels)
|
||||
utils.MapMergeNoOverrite(annotations, md.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
return labels, annotations, nil
|
||||
}
|
||||
|
||||
func BuildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
annotations := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(annotations, md.AdditionalMetadata.Annotations)
|
||||
}
|
||||
|
||||
if tnt.Spec.NodeSelector != nil {
|
||||
annotations = utils.BuildNodeSelector(tnt, annotations)
|
||||
}
|
||||
|
||||
if ic := tnt.Spec.IngressOptions.AllowedClasses; ic != nil {
|
||||
if len(ic.Exact) > 0 {
|
||||
annotations[meta.AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",")
|
||||
}
|
||||
|
||||
if len(ic.Regex) > 0 {
|
||||
annotations[meta.AvailableIngressClassesRegexpAnnotation] = ic.Regex
|
||||
}
|
||||
}
|
||||
|
||||
if sc := tnt.Spec.StorageClasses; sc != nil {
|
||||
if len(sc.Exact) > 0 {
|
||||
annotations[meta.AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",")
|
||||
}
|
||||
|
||||
if len(sc.Regex) > 0 {
|
||||
annotations[meta.AvailableStorageClassesRegexpAnnotation] = sc.Regex
|
||||
}
|
||||
}
|
||||
|
||||
if cr := tnt.Spec.ContainerRegistries; cr != nil {
|
||||
if len(cr.Exact) > 0 {
|
||||
annotations[meta.AllowedRegistriesAnnotation] = strings.Join(cr.Exact, ",")
|
||||
}
|
||||
|
||||
if len(cr.Regex) > 0 {
|
||||
annotations[meta.AllowedRegistriesRegexpAnnotation] = cr.Regex
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
meta.ForbiddenNamespaceLabelsAnnotation,
|
||||
meta.ForbiddenNamespaceLabelsRegexpAnnotation,
|
||||
meta.ForbiddenNamespaceAnnotationsAnnotation,
|
||||
meta.ForbiddenNamespaceAnnotationsRegexpAnnotation,
|
||||
} {
|
||||
if value, ok := tnt.Annotations[key]; ok {
|
||||
annotations[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return annotations
|
||||
}
|
||||
|
||||
func BuildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
labels[meta.CordonedLabel] = "true"
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user