diff --git a/Makefile b/Makefile
index 7ccfe6c29..a068b8915 100644
--- a/Makefile
+++ b/Makefile
@@ -166,7 +166,8 @@ dev-setup:
{'op': 'replace', 'path': '/webhooks/5/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/pods\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/6/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/persistentvolumeclaims\",'caBundle':\"$${CA_BUNDLE}\"}},\
{'op': 'replace', 'path': '/webhooks/7/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/services\",'caBundle':\"$${CA_BUNDLE}\"}},\
- {'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}}\
+ {'op': 'replace', 'path': '/webhooks/8/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}},\
+ {'op': 'replace', 'path': '/webhooks/9/clientConfig', 'value':{'url':\"$${WEBHOOK_URL}/tenants\",'caBundle':\"$${CA_BUNDLE}\"}}\
]" && \
kubectl patch crd tenants.capsule.clastix.io \
--type='json' -p="[\
diff --git a/api/v1beta2/tenant_types.go b/api/v1beta2/tenant_types.go
index 93e01b759..01c8e1a28 100644
--- a/api/v1beta2/tenant_types.go
+++ b/api/v1beta2/tenant_types.go
@@ -17,6 +17,8 @@ type TenantSpec struct {
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *api.ServiceOptions `json:"serviceOptions,omitempty"`
+ // Specifies options for the Pods, such additional metadata to enforce on each pod part of the tenant
+ PodOptions *api.PodOptions `json:"podOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant.
// Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses.
// A default value can be specified, and all the PersistentVolumeClaim resources created will inherit the declared class.
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 7245688f4..119085f48 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -716,6 +716,11 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(api.ServiceOptions)
(*in).DeepCopyInto(*out)
}
+ if in.PodOptions != nil {
+ in, out := &in.PodOptions, &out.PodOptions
+ *out = new(api.PodOptions)
+ (*in).DeepCopyInto(*out)
+ }
if in.StorageClasses != nil {
in, out := &in.StorageClasses, &out.StorageClasses
*out = new(api.DefaultAllowedListSpec)
diff --git a/charts/capsule/crds/tenant-crd.yaml b/charts/capsule/crds/tenant-crd.yaml
index 314016fbd..8ae0d6ae1 100644
--- a/charts/capsule/crds/tenant-crd.yaml
+++ b/charts/capsule/crds/tenant-crd.yaml
@@ -632,6 +632,22 @@ spec:
- name
type: object
type: array
+ podOptions:
+ description: Specifies options for the Pod, such as additional metadata. Optional.
+ properties:
+ additionalMetadata:
+ description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ type: object
preventDeletion:
description: Prevent accidental deletion of the Tenant. When enabled, the deletion request will be declined.
type: boolean
@@ -1737,6 +1753,22 @@ spec:
- name
type: object
type: array
+ podOptions:
+ description: Specifies options for the Pod, such as additional metadata. Optional.
+ properties:
+ additionalMetadata:
+ description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ type: object
priorityClasses:
description: Specifies the allowed priorityClasses assigned to the
Tenant. Capsule assures that all Pods resources created in the Tenant
@@ -2869,6 +2901,22 @@ spec:
- name
type: object
type: array
+ podOptions:
+ description: Specifies options for the Pod, such as additional metadata. Optional.
+ properties:
+ additionalMetadata:
+ description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ type: object
preventDeletion:
description: Prevent accidental deletion of the Tenant. When enabled,
the deletion request will be declined.
diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml
index 75c547236..7e1e00da5 100644
--- a/config/crd/bases/capsule.clastix.io_tenants.yaml
+++ b/config/crd/bases/capsule.clastix.io_tenants.yaml
@@ -2859,6 +2859,24 @@ spec:
- name
type: object
type: array
+ podOptions:
+ description: Specifies options for the Pods, such additional metadata
+ to enforce on each pod part of the tenant
+ properties:
+ additionalMetadata:
+ description: Specifies additional labels and annotations the Capsule
+ operator places on any Service resource in the Tenant. Optional.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ type: object
preventDeletion:
description: Prevent accidental deletion of the Tenant. When enabled,
the deletion request will be declined.
diff --git a/config/install.yaml b/config/install.yaml
index d753fe848..6ffd10757 100644
--- a/config/install.yaml
+++ b/config/install.yaml
@@ -2437,6 +2437,22 @@ spec:
- name
type: object
type: array
+ podOptions:
+ description: Specifies options for the Pods, such additional metadata to enforce on each pod part of the tenant
+ properties:
+ additionalMetadata:
+ description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ type: object
preventDeletion:
description: Prevent accidental deletion of the Tenant. When enabled, the deletion request will be declined.
type: boolean
diff --git a/controllers/podlabels/abstract.go b/controllers/podlabels/abstract.go
new file mode 100644
index 000000000..00d442668
--- /dev/null
+++ b/controllers/podlabels/abstract.go
@@ -0,0 +1,117 @@
+package podlabels
+
+import (
+ "context"
+ "fmt"
+ "github.com/go-logr/logr"
+ "github.com/projectcapsule/capsule/pkg/utils"
+ corev1 "k8s.io/api/core/v1"
+ apierr "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/fields"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ "github.com/pkg/errors"
+ capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
+)
+
+type abstractPodLabelsReconciler struct {
+ obj client.Object
+ client client.Client
+ log logr.Logger
+}
+
+func (r *abstractPodLabelsReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
+ tenant, err := r.getTenant(ctx, request.NamespacedName, r.client)
+ if err != nil {
+ r.log.Error(err, fmt.Sprintf("Cannot get tenant for %T %s/%s", r.obj, request.Namespace, request.Name))
+
+ noTenantObjError := &NonTenantObjectError{}
+ noPodMetaError := &NoPodMetadataError{}
+
+ if errors.As(err, &noTenantObjError) || errors.As(err, &noPodMetaError) {
+ return reconcile.Result{}, nil
+ }
+
+ r.log.Error(err, fmt.Sprintf("Cannot sync %T %s/%s labels", r.obj, r.obj.GetNamespace(), r.obj.GetName()))
+
+ return reconcile.Result{}, err
+ }
+
+ err = r.client.Get(ctx, request.NamespacedName, r.obj)
+ if err != nil {
+ if apierr.IsNotFound(err) {
+ return reconcile.Result{}, nil
+ }
+
+ return reconcile.Result{}, err
+ }
+ _, err = controllerutil.CreateOrUpdate(ctx, r.client, r.obj, func() (err error) {
+ r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.PodOptions.AdditionalMetadata.Labels))
+ r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.PodOptions.AdditionalMetadata.Annotations))
+ return nil
+ })
+ return reconcile.Result{}, err
+}
+
+func (r *abstractPodLabelsReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*capsulev1beta2.Tenant, error) {
+ ns := &corev1.Namespace{}
+ tenant := &capsulev1beta2.Tenant{}
+
+ if err := client.Get(ctx, types.NamespacedName{Name: namespacedName.Namespace}, ns); err != nil {
+ return nil, err
+ }
+
+ capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
+ if _, ok := ns.GetLabels()[capsuleLabel]; !ok {
+ return nil, NewNonTenantObject(namespacedName.Name)
+ }
+
+ if err := client.Get(ctx, types.NamespacedName{Name: ns.Labels[capsuleLabel]}, tenant); err != nil {
+ return nil, err
+ }
+
+ if tenant.Spec.PodOptions == nil || tenant.Spec.PodOptions.AdditionalMetadata == nil {
+ return nil, NewNoPodMetadata(namespacedName.Name)
+ }
+
+ return tenant, nil
+}
+
+func (r *abstractPodLabelsReconciler) sync(available map[string]string, tenantSpec map[string]string) map[string]string {
+ if tenantSpec != nil {
+ if available == nil {
+ available = tenantSpec
+ } else {
+ for key, value := range tenantSpec {
+ if available[key] != value {
+ available[key] = value
+ }
+ }
+ }
+ }
+
+ return available
+}
+
+func (r *abstractPodLabelsReconciler) forOptionPerInstanceName(ctx context.Context) builder.ForOption {
+ return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
+ return r.IsNamespaceInTenant(ctx, object.GetNamespace())
+ }))
+}
+
+func (r *abstractPodLabelsReconciler) IsNamespaceInTenant(ctx context.Context, namespace string) bool {
+ tl := &capsulev1beta2.TenantList{}
+ if err := r.client.List(ctx, tl, client.MatchingFieldsSelector{
+ Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
+ }); err != nil {
+ return false
+ }
+
+ return len(tl.Items) > 0
+}
diff --git a/controllers/podlabels/errors.go b/controllers/podlabels/errors.go
new file mode 100644
index 000000000..06ab8771c
--- /dev/null
+++ b/controllers/podlabels/errors.go
@@ -0,0 +1,27 @@
+package podlabels
+
+import "fmt"
+
+type NonTenantObjectError struct {
+ objectName string
+}
+
+func NewNonTenantObject(objectName string) error {
+ return &NonTenantObjectError{objectName: objectName}
+}
+
+func (n NonTenantObjectError) Error() string {
+ return fmt.Sprintf("Skipping labels sync for %s as it doesn't belong to tenant", n.objectName)
+}
+
+type NoPodMetadataError struct {
+ objectName string
+}
+
+func NewNoPodMetadata(objectName string) error {
+ return &NoPodMetadataError{objectName: objectName}
+}
+
+func (n NoPodMetadataError) Error() string {
+ return fmt.Sprintf("Skipping labels sync for %s because no AdditionalLabels or AdditionalAnnotations presents in Tenant spec", n.objectName)
+}
diff --git a/controllers/podlabels/pod.go b/controllers/podlabels/pod.go
new file mode 100644
index 000000000..458c4ed0f
--- /dev/null
+++ b/controllers/podlabels/pod.go
@@ -0,0 +1,26 @@
+package podlabels
+
+import (
+ "context"
+ "github.com/go-logr/logr"
+ corev1 "k8s.io/api/core/v1"
+ ctrl "sigs.k8s.io/controller-runtime"
+)
+
+type PodLabelsReconciler struct {
+ abstractPodLabelsReconciler
+
+ Log logr.Logger
+}
+
+func (r *PodLabelsReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
+ r.abstractPodLabelsReconciler = abstractPodLabelsReconciler{
+ obj: &corev1.Pod{},
+ client: mgr.GetClient(),
+ log: r.Log,
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(r.abstractPodLabelsReconciler.obj, r.abstractPodLabelsReconciler.forOptionPerInstanceName(ctx)).
+ Complete(r)
+}
diff --git a/docs/content/general/crds-apis.md b/docs/content/general/crds-apis.md
index 696a41eba..b222ebd2e 100644
--- a/docs/content/general/crds-apis.md
+++ b/docs/content/general/crds-apis.md
@@ -2963,6 +2963,13 @@ TenantSpec defines the desired state of Tenant.
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.
Name | +Type | +Description | +Required | +
---|---|---|---|
additionalMetadata | +object | +
+ Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional. + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
annotations | +map[string]string | +
+ + |
+ false | +
labels | +map[string]string | +
+ + |
+ false | +