feat!: write permissions (#937)

* fix: decoding object only if requested

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* feat(api): limiting write permissions

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* feat: write permissions handlers, routes, and controller

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* docs: write permissions

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
This commit is contained in:
Dario Tranchitella
2025-10-03 14:30:58 +02:00
committed by GitHub
parent 2b707423ff
commit de459fb5da
16 changed files with 561 additions and 26 deletions

View File

@@ -189,12 +189,14 @@ type KubernetesStatus struct {
Ingress *KubernetesIngressStatus `json:"ingress,omitempty"` Ingress *KubernetesIngressStatus `json:"ingress,omitempty"`
} }
// +kubebuilder:validation:Enum=Provisioning;CertificateAuthorityRotating;Upgrading;Migrating;Ready;NotReady;Sleeping // +kubebuilder:validation:Enum=Unknown;Provisioning;CertificateAuthorityRotating;Upgrading;Migrating;Ready;NotReady;Sleeping;WriteLimited
type KubernetesVersionStatus string type KubernetesVersionStatus string
var ( var (
VersionUnknown KubernetesVersionStatus = "Unknown"
VersionProvisioning KubernetesVersionStatus = "Provisioning" VersionProvisioning KubernetesVersionStatus = "Provisioning"
VersionSleeping KubernetesVersionStatus = "Sleeping" VersionSleeping KubernetesVersionStatus = "Sleeping"
VersionWriteLimited KubernetesVersionStatus = "WriteLimited"
VersionCARotating KubernetesVersionStatus = "CertificateAuthorityRotating" VersionCARotating KubernetesVersionStatus = "CertificateAuthorityRotating"
VersionUpgrading KubernetesVersionStatus = "Upgrading" VersionUpgrading KubernetesVersionStatus = "Upgrading"
VersionMigrating KubernetesVersionStatus = "Migrating" VersionMigrating KubernetesVersionStatus = "Migrating"

View File

@@ -297,6 +297,20 @@ type AddonsSpec struct {
KubeProxy *AddonSpec `json:"kubeProxy,omitempty"` KubeProxy *AddonSpec `json:"kubeProxy,omitempty"`
} }
type Permissions struct {
BlockCreate bool `json:"blockCreation,omitempty"`
BlockUpdate bool `json:"blockUpdate,omitempty"`
BlockDelete bool `json:"blockDeletion,omitempty"`
}
func (p *Permissions) HasAnyLimitation() bool {
if p.BlockCreate || p.BlockUpdate || p.BlockDelete {
return true
}
return false
}
// TenantControlPlaneSpec defines the desired state of TenantControlPlane. // TenantControlPlaneSpec defines the desired state of TenantControlPlane.
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported"
@@ -306,6 +320,13 @@ type AddonsSpec struct {
// +kubebuilder:validation:XValidation:rule="self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime" // +kubebuilder:validation:XValidation:rule="self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
type TenantControlPlaneSpec struct { type TenantControlPlaneSpec struct {
// WritePermissions allows to select which operations (create, delete, update) must be blocked:
// by default, all actions are allowed, and API Server can write to its Datastore.
//
// By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
// this phase can be used to prevent Datastore quota exhaustion or for your own business logic
// (e.g.: blocking creation and update, but allowing deletion to "clean up" space).
WritePermissions Permissions `json:"writePermissions,omitempty"`
// DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane. // DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
// When Kamaji runs with the default DataStore flag, all empty values will inherit the default value. // When Kamaji runs with the default DataStore flag, all empty values will inherit the default value.
// By leaving it empty and running Kamaji with no default DataStore flag, it is possible to achieve automatic assignment to a specific DataStore object. // By leaving it empty and running Kamaji with no default DataStore flag, it is possible to achieve automatic assignment to a specific DataStore object.

View File

@@ -1285,6 +1285,21 @@ func (in *NetworkProfileSpec) DeepCopy() *NetworkProfileSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Permissions) DeepCopyInto(out *Permissions) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Permissions.
func (in *Permissions) DeepCopy() *Permissions {
if in == nil {
return nil
}
out := new(Permissions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PublicKeyPrivateKeyPairStatus) DeepCopyInto(out *PublicKeyPrivateKeyPairStatus) { func (in *PublicKeyPrivateKeyPairStatus) DeepCopyInto(out *PublicKeyPrivateKeyPairStatus) {
*out = *in *out = *in
@@ -1449,6 +1464,7 @@ func (in *TenantControlPlaneList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) { func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) {
*out = *in *out = *in
out.WritePermissions = in.WritePermissions
in.ControlPlane.DeepCopyInto(&out.ControlPlane) in.ControlPlane.DeepCopyInto(&out.ControlPlane)
in.Kubernetes.DeepCopyInto(&out.Kubernetes) in.Kubernetes.DeepCopyInto(&out.Kubernetes)
in.NetworkProfile.DeepCopyInto(&out.NetworkProfile) in.NetworkProfile.DeepCopyInto(&out.NetworkProfile)

View File

@@ -6955,6 +6955,22 @@ versions:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.' description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string type: string
type: object type: object
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).
properties:
blockCreation:
type: boolean
blockDeletion:
type: boolean
blockUpdate:
type: boolean
type: object
required: required:
- controlPlane - controlPlane
- kubernetes - kubernetes
@@ -7703,6 +7719,7 @@ versions:
default: Provisioning default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade. description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum: enum:
- Unknown
- Provisioning - Provisioning
- CertificateAuthorityRotating - CertificateAuthorityRotating
- Upgrading - Upgrading
@@ -7710,6 +7727,7 @@ versions:
- Ready - Ready
- NotReady - NotReady
- Sleeping - Sleeping
- WriteLimited
type: string type: string
version: version:
description: Version is the running Kubernetes version of the Tenant Control Plane. description: Version is the running Kubernetes version of the Tenant Control Plane.

View File

@@ -6963,6 +6963,22 @@ spec:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.' description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string type: string
type: object type: object
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).
properties:
blockCreation:
type: boolean
blockDeletion:
type: boolean
blockUpdate:
type: boolean
type: object
required: required:
- controlPlane - controlPlane
- kubernetes - kubernetes
@@ -7711,6 +7727,7 @@ spec:
default: Provisioning default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade. description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum: enum:
- Unknown
- Provisioning - Provisioning
- CertificateAuthorityRotating - CertificateAuthorityRotating
- Upgrading - Upgrading
@@ -7718,6 +7735,7 @@ spec:
- Ready - Ready
- NotReady - NotReady
- Sleeping - Sleeping
- WriteLimited
type: string type: string
version: version:
description: Version is the running Kubernetes version of the Tenant Control Plane. description: Version is the running Kubernetes version of the Tenant Control Plane.

View File

@@ -219,6 +219,9 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
routes.TenantControlPlaneMigrate{}: { routes.TenantControlPlaneMigrate{}: {
handlers.Freeze{}, handlers.Freeze{},
}, },
routes.TenantControlPlaneWritePermission{}: {
handlers.WritePermission{},
},
routes.TenantControlPlaneDefaults{}: { routes.TenantControlPlaneDefaults{}: {
handlers.TenantControlPlaneDefaults{ handlers.TenantControlPlaneDefaults{
DefaultDatastore: datastore, DefaultDatastore: datastore,

View File

@@ -0,0 +1,207 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/cmd/kubeadm/app/util/errors"
"k8s.io/utils/ptr"
controllerruntime "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"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
sooterrors "github.com/clastix/kamaji/controllers/soot/controllers/errors"
"github.com/clastix/kamaji/controllers/utils"
"github.com/clastix/kamaji/internal/utilities"
)
type WritePermissions struct {
Logger logr.Logger
Client client.Client
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
WebhookNamespace string
WebhookServiceName string
WebhookCABundle []byte
TriggerChannel chan event.GenericEvent
}
func (r *WritePermissions) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
tcp, err := r.GetTenantControlPlaneFunc()
if err != nil {
if errors.Is(err, sooterrors.ErrPausedReconciliation) {
r.Logger.Info(err.Error())
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
// Cannot detect the status of the TenantControlPlane, enqueuing back
if tcp.Status.Kubernetes.Version.Status == nil {
return reconcile.Result{RequeueAfter: time.Second}, nil
}
switch {
case ptr.Deref(tcp.Status.Kubernetes.Version.Status, kamajiv1alpha1.VersionUnknown) == kamajiv1alpha1.VersionWriteLimited &&
tcp.Spec.WritePermissions.HasAnyLimitation():
err = r.createOrUpdate(ctx, tcp.Spec.WritePermissions)
default:
err = r.cleanup(ctx)
}
if err != nil {
r.Logger.Error(err, "reconciliation failed")
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
func (r *WritePermissions) createOrUpdate(ctx context.Context, writePermissions kamajiv1alpha1.Permissions) error {
obj := r.object().DeepCopy()
_, err := utilities.CreateOrUpdateWithConflict(ctx, r.Client, obj, func() error {
obj.Webhooks = []admissionregistrationv1.ValidatingWebhook{
{
Name: "leases.write-permissions.kamaji.clastix.io",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: ptr.To(fmt.Sprintf("https://%s.%s.svc:443/write-permission", r.WebhookServiceName, r.WebhookNamespace)),
CABundle: r.WebhookCABundle,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Delete,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
},
},
FailurePolicy: ptr.To(admissionregistrationv1.Fail),
MatchPolicy: ptr.To(admissionregistrationv1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: metav1.LabelSelectorOpIn,
Values: []string{
"kube-node-lease",
},
},
},
},
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNoneOnDryRun),
AdmissionReviewVersions: []string{"v1"},
},
{
Name: "catchall.write-permissions.kamaji.clastix.io",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: ptr.To(fmt.Sprintf("https://%s.%s.svc:443/write-permission", r.WebhookServiceName, r.WebhookNamespace)),
CABundle: r.WebhookCABundle,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: func() []admissionregistrationv1.OperationType {
var ops []admissionregistrationv1.OperationType
if writePermissions.BlockCreate {
ops = append(ops, admissionregistrationv1.Create)
}
if writePermissions.BlockUpdate {
ops = append(ops, admissionregistrationv1.Update)
}
if writePermissions.BlockDelete {
ops = append(ops, admissionregistrationv1.Delete)
}
return ops
}(),
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*"},
Scope: ptr.To(admissionregistrationv1.AllScopes),
},
},
},
FailurePolicy: ptr.To(admissionregistrationv1.Fail),
MatchPolicy: ptr.To(admissionregistrationv1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{
"kube-system",
"kube-node-lease",
},
},
},
},
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNoneOnDryRun),
TimeoutSeconds: nil,
AdmissionReviewVersions: []string{"v1"},
},
}
return nil
})
return err
}
func (r *WritePermissions) cleanup(ctx context.Context) error {
if err := r.Client.Delete(ctx, r.object()); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return fmt.Errorf("unable to clean-up ValidationWebhook required for write permissions: %w", err)
}
return nil
}
func (r *WritePermissions) SetupWithManager(mgr manager.Manager) error {
r.TriggerChannel = make(chan event.GenericEvent)
return controllerruntime.NewControllerManagedBy(mgr).
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
For(&admissionregistrationv1.ValidatingWebhookConfiguration{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.object().GetName()
}))).
WatchesRawSource(source.Channel(r.TriggerChannel, &handler.EnqueueRequestForObject{})).
Complete(r)
}
func (r *WritePermissions) object() *admissionregistrationv1.ValidatingWebhookConfiguration {
return &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "kamaji-write-permissions",
},
}
}

View File

@@ -253,6 +253,19 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
// //
// Register all the controllers of the soot here: // Register all the controllers of the soot here:
// //
writePermissions := &controllers.WritePermissions{
Logger: mgr.GetLogger().WithName("writePermissions"),
Client: mgr.GetClient(),
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
WebhookNamespace: m.MigrateServiceNamespace,
WebhookServiceName: m.MigrateServiceName,
WebhookCABundle: m.MigrateCABundle,
TriggerChannel: nil,
}
if err = writePermissions.SetupWithManager(mgr); err != nil {
return reconcile.Result{}, err
}
migrate := &controllers.Migrate{ migrate := &controllers.Migrate{
WebhookNamespace: m.MigrateServiceNamespace, WebhookNamespace: m.MigrateServiceNamespace,
WebhookServiceName: m.MigrateServiceName, WebhookServiceName: m.MigrateServiceName,
@@ -370,6 +383,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
m.sootMap[request.NamespacedName.String()] = sootItem{ m.sootMap[request.NamespacedName.String()] = sootItem{
triggers: []chan event.GenericEvent{ triggers: []chan event.GenericEvent{
writePermissions.TriggerChannel,
migrate.TriggerChannel, migrate.TriggerChannel,
konnectivityAgent.TriggerChannel, konnectivityAgent.TriggerChannel,
kubeProxy.TriggerChannel, kubeProxy.TriggerChannel,

View File

@@ -0,0 +1,115 @@
# Write Permissions
Using the _Write Permissions_ section, operators can limit write operations for a given Tenant Control Plane,
where no further write actions can be made by its tenants.
This feature ensures consistency during maintenance, migrations, incident recovery, quote enforcement,
or when freezing workloads for auditing and compliance purposes.
Write Operations can limit the following actions:
- Create
- Update
- Delete
By default, all write operations are allowed.
## Enabling a Read-Only mode
You can enable ReadOnly mode by setting all the boolean fields of `TenantControlPlane.spec.writePermissions` to `true`.
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: my-control-plane
spec:
writePermissions:
blockCreate: true
blockUpdate: true
blockDelete: true
```
Once applied, the Tenant Control Plane will switch into `WriteLimited` status.
## Enforcing a quota mode
If your Tenant Control Plane has a Datastore quota, this feature allows freezing write and update operations,
but still allowing its tenants to perform a clean-up by deleting exceeding resources.
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: my-control-plane
spec:
writePermissions:
blockCreate: true
blockUpdate: true
blockDelete: false
```
!!! note "Datastore quota"
Kamaji does **not** enforce storage quota for a given Tenant Control Plane:
you have to implement it according to your business logic.
## Monitoring the status
You can verify the status of your Tenant Control Plane with `kubectl get tcp`:
```json
$: kubectl get tcp k8s-133
NAME VERSION INSTALLED VERSION STATUS CONTROL-PLANE ENDPOINT KUBECONFIG DATASTORE AGE
k8s-133 v1.33.0 v1.33.0 WriteLimited 172.18.255.100:6443 k8s-133-admin-kubeconfig default 50d
```
The `STATUS` field will display `WriteLimited` when write permissions are limited.
## How it works
When a Tenant Control Plane write status is _limited_, Kamaji creates a `ValidatingWebhookConfiguration` in the Tenant Cluster:
```
$: kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
kamaji-write-permissions 2 59m
```
The webhook intercepts all API requests to the Tenant Control Plane and programmatically denies any attempts to modify resources.
As a result, all changes initiated by tenants (such as `kubectl apply`, `kubectl delete`, or CRD updates) could be blocked.
!!! warning "Operators and Controller"
When the write status is limited, all actions are intercepted by the webhook.
If a Pod must be rescheduled, the webhook will deny it.
## Behaviour with limited write operations
If a tenant user tries to perform non-allowed write operations, such as:
- creating resources when `TenantControlPlane.spec.writePermissions.blockCreate` is set to `true`
- updating resources when `TenantControlPlane.spec.writePermissions.blockUpdate` is set to `true`
- deleting resources when `TenantControlPlane.spec.writePermissions.blockDelete` is set to `true`
the following error is returned:
```
Error from server (Forbidden): admission webhook "catchall.write-permissions.kamaji.clastix.io" denied the request:
the current Control Plane has limited write permissions, current changes are blocked:
removing the webhook may lead to an inconsistent state upon its completion
```
This guarantees the cluster remains in a frozen, consistent state, preventing partial updates or drift.
## Use Cases
Typical scenarios where ReadOnly mode is useful:
- **Planned Maintenance**: freeze workloads before performing upgrades or infrastructure changes.
- **Disaster Recovery**: lock the Tenant Control Plane to prevent accidental modifications during incident handling.
- **Auditing & Compliance**: ensure workloads cannot be altered during a compliance check or certification process.
- **Quota Enforcement**: preventing Datastore quote over commit in terms of storage size.
!!! info "Migrating the DataStore"
In a similar manner, when migrating a Tenant Control Plane to a different store, similar enforcement is put in place.
This is managed automatically by Kamaji: there's no need to toggle on and off the ReadOnly mode.

View File

@@ -28523,6 +28523,18 @@ DataStoreUsername by concatenating the namespace and name of the TenantControlPl
NetworkProfile specifies how the network is<br/> NetworkProfile specifies how the network is<br/>
</td> </td>
<td>false</td> <td>false</td>
</tr><tr>
<td><b><a href="#tenantcontrolplanespecwritepermissions">writePermissions</a></b></td>
<td>object</td>
<td>
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).<br/>
</td>
<td>false</td>
</tr></tbody> </tr></tbody>
</table> </table>
@@ -41987,6 +41999,50 @@ Example: {"192.168.1.0/24", "10.0.0.0/8"}<br/>
</table> </table>
<span id="tenantcontrolplanespecwritepermissions">`TenantControlPlane.spec.writePermissions`</span>
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>
<tbody><tr>
<td><b>blockCreation</b></td>
<td>boolean</td>
<td>
<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>blockDeletion</b></td>
<td>boolean</td>
<td>
<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>blockUpdate</b></td>
<td>boolean</td>
<td>
<br/>
</td>
<td>false</td>
</tr></tbody>
</table>
<span id="tenantcontrolplanestatus">`TenantControlPlane.status`</span> <span id="tenantcontrolplanestatus">`TenantControlPlane.status`</span>
@@ -44111,7 +44167,7 @@ KubernetesVersion contains the information regarding the running Kubernetes vers
<td> <td>
Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.<br/> Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.<br/>
<br/> <br/>
<i>Enum</i>: Provisioning, CertificateAuthorityRotating, Upgrading, Migrating, Ready, NotReady, Sleeping<br/> <i>Enum</i>: Unknown, Provisioning, CertificateAuthorityRotating, Upgrading, Migrating, Ready, NotReady, Sleeping, WriteLimited<br/>
<i>Default</i>: Provisioning<br/> <i>Default</i>: Provisioning<br/>
</td> </td>
<td>false</td> <td>false</td>

View File

@@ -74,6 +74,7 @@ nav:
- guides/backup-and-restore.md - guides/backup-and-restore.md
- guides/certs-lifecycle.md - guides/certs-lifecycle.md
- guides/pausing.md - guides/pausing.md
- guides/write-permissions.md
- guides/datastore-migration.md - guides/datastore-migration.md
- guides/gitops.md - guides/gitops.md
- guides/console.md - guides/console.md

View File

@@ -36,7 +36,9 @@ func (r *KubernetesDeploymentResource) isStatusEqual(tenantControlPlane *kamajiv
} }
func (r *KubernetesDeploymentResource) ShouldStatusBeUpdated(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { func (r *KubernetesDeploymentResource) ShouldStatusBeUpdated(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !r.isStatusEqual(tenantControlPlane) || tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version return !r.isStatusEqual(tenantControlPlane) ||
tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version ||
*r.computeStatus(tenantControlPlane) != ptr.Deref(tenantControlPlane.Status.Kubernetes.Version.Status, kamajiv1alpha1.VersionUnknown)
} }
func (r *KubernetesDeploymentResource) ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool { func (r *KubernetesDeploymentResource) ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool {
@@ -78,19 +80,29 @@ func (r *KubernetesDeploymentResource) GetName() string {
return "deployment" return "deployment"
} }
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { func (r *KubernetesDeploymentResource) computeStatus(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) *kamajiv1alpha1.KubernetesVersionStatus {
switch { switch {
case ptr.Deref(tenantControlPlane.Spec.ControlPlane.Deployment.Replicas, 2) == 0: case ptr.Deref(tenantControlPlane.Spec.ControlPlane.Deployment.Replicas, 2) == 0:
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionSleeping return &kamajiv1alpha1.VersionSleeping
case !r.isProgressingUpgrade():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionReady
tenantControlPlane.Status.Kubernetes.Version.Version = tenantControlPlane.Spec.Kubernetes.Version
case r.isUpgrading(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionUpgrading
case r.isProvisioning(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionProvisioning
case r.isNotReady(): case r.isNotReady():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionNotReady return &kamajiv1alpha1.VersionNotReady
case tenantControlPlane.Spec.WritePermissions.HasAnyLimitation():
return &kamajiv1alpha1.VersionWriteLimited
case !r.isProgressingUpgrade():
return &kamajiv1alpha1.VersionReady
case r.isUpgrading(tenantControlPlane):
return &kamajiv1alpha1.VersionUpgrading
case r.isProvisioning(tenantControlPlane):
return &kamajiv1alpha1.VersionProvisioning
default:
return &kamajiv1alpha1.VersionUnknown
}
}
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Kubernetes.Version.Status = r.computeStatus(tenantControlPlane)
if *tenantControlPlane.Status.Kubernetes.Version.Status == kamajiv1alpha1.VersionReady {
tenantControlPlane.Status.Kubernetes.Version.Version = tenantControlPlane.Spec.Kubernetes.Version
} }
tenantControlPlane.Status.Kubernetes.Deployment = kamajiv1alpha1.KubernetesDeploymentStatus{ tenantControlPlane.Status.Kubernetes.Deployment = kamajiv1alpha1.KubernetesDeploymentStatus{

View File

@@ -25,18 +25,21 @@ type handlersChainer struct {
//nolint:gocognit //nolint:gocognit
func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handlers.Handler) admission.HandlerFunc { func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handlers.Handler) admission.HandlerFunc {
return func(ctx context.Context, req admission.Request) admission.Response { return func(ctx context.Context, req admission.Request) admission.Response {
decodedObj, oldDecodedObj := object.DeepCopyObject(), object.DeepCopyObject() var decodedObj, oldDecodedObj runtime.Object
if object != nil {
decodedObj, oldDecodedObj = object.DeepCopyObject(), object.DeepCopyObject()
switch req.Operation { switch req.Operation {
case admissionv1.Delete: case admissionv1.Delete:
// When deleting the OldObject struct field contains the object being deleted: // When deleting the OldObject struct field contains the object being deleted:
// https://github.com/kubernetes/kubernetes/pull/76346 // https://github.com/kubernetes/kubernetes/pull/76346
if err := h.decoder.DecodeRaw(req.OldObject, decodedObj); err != nil { if err := h.decoder.DecodeRaw(req.OldObject, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode deleted object into %T", object))) return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode deleted object into %T", object)))
} }
default: default:
if err := h.decoder.Decode(req, decodedObj); err != nil { if err := h.decoder.Decode(req, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode into %T", object))) return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode into %T", object)))
}
} }
} }

View File

@@ -0,0 +1,32 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
type WritePermission struct{}
func (f WritePermission) OnCreate(runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) OnDelete(runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) OnUpdate(runtime.Object, runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) response(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
return nil, fmt.Errorf("the current Control Plane has limited write permissions, current changes are blocked: " +
"removing the webhook may lead to an inconsistent state upon its completion")
}

View File

@@ -4,7 +4,6 @@
package routes package routes
import ( import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@@ -15,5 +14,5 @@ func (t TenantControlPlaneMigrate) GetPath() string {
} }
func (t TenantControlPlaneMigrate) GetObject() runtime.Object { func (t TenantControlPlaneMigrate) GetObject() runtime.Object {
return &corev1.Namespace{} return nil
} }

View File

@@ -0,0 +1,18 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package routes
import (
"k8s.io/apimachinery/pkg/runtime"
)
type TenantControlPlaneWritePermission struct{}
func (t TenantControlPlaneWritePermission) GetPath() string {
return "/write-permission"
}
func (t TenantControlPlaneWritePermission) GetObject() runtime.Object {
return nil
}