diff --git a/api/v1alpha1/tenantcontrolplane_status.go b/api/v1alpha1/tenantcontrolplane_status.go
index a8c4223..12ea8ec 100644
--- a/api/v1alpha1/tenantcontrolplane_status.go
+++ b/api/v1alpha1/tenantcontrolplane_status.go
@@ -189,12 +189,14 @@ type KubernetesStatus struct {
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
var (
+ VersionUnknown KubernetesVersionStatus = "Unknown"
VersionProvisioning KubernetesVersionStatus = "Provisioning"
VersionSleeping KubernetesVersionStatus = "Sleeping"
+ VersionWriteLimited KubernetesVersionStatus = "WriteLimited"
VersionCARotating KubernetesVersionStatus = "CertificateAuthorityRotating"
VersionUpgrading KubernetesVersionStatus = "Upgrading"
VersionMigrating KubernetesVersionStatus = "Migrating"
diff --git a/api/v1alpha1/tenantcontrolplane_types.go b/api/v1alpha1/tenantcontrolplane_types.go
index 16c91f0..0a8a89b 100644
--- a/api/v1alpha1/tenantcontrolplane_types.go
+++ b/api/v1alpha1/tenantcontrolplane_types.go
@@ -297,6 +297,20 @@ type AddonsSpec struct {
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.
// +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"
@@ -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"
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.
// 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.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index b9efa16..72fd59f 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1285,6 +1285,21 @@ func (in *NetworkProfileSpec) DeepCopy() *NetworkProfileSpec {
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.
func (in *PublicKeyPrivateKeyPairStatus) DeepCopyInto(out *PublicKeyPrivateKeyPairStatus) {
*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.
func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) {
*out = *in
+ out.WritePermissions = in.WritePermissions
in.ControlPlane.DeepCopyInto(&out.ControlPlane)
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
in.NetworkProfile.DeepCopyInto(&out.NetworkProfile)
diff --git a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
index e74efe9..44a035c 100644
--- a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
+++ b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
@@ -6955,6 +6955,22 @@ versions:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
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:
- controlPlane
- kubernetes
@@ -7703,6 +7719,7 @@ versions:
default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum:
+ - Unknown
- Provisioning
- CertificateAuthorityRotating
- Upgrading
@@ -7710,6 +7727,7 @@ versions:
- Ready
- NotReady
- Sleeping
+ - WriteLimited
type: string
version:
description: Version is the running Kubernetes version of the Tenant Control Plane.
diff --git a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
index 5b60d25..49abfd5 100644
--- a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
+++ b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
@@ -6963,6 +6963,22 @@ spec:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
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:
- controlPlane
- kubernetes
@@ -7711,6 +7727,7 @@ spec:
default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum:
+ - Unknown
- Provisioning
- CertificateAuthorityRotating
- Upgrading
@@ -7718,6 +7735,7 @@ spec:
- Ready
- NotReady
- Sleeping
+ - WriteLimited
type: string
version:
description: Version is the running Kubernetes version of the Tenant Control Plane.
diff --git a/cmd/manager/cmd.go b/cmd/manager/cmd.go
index 82e17cc..c004a7e 100644
--- a/cmd/manager/cmd.go
+++ b/cmd/manager/cmd.go
@@ -219,6 +219,9 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
routes.TenantControlPlaneMigrate{}: {
handlers.Freeze{},
},
+ routes.TenantControlPlaneWritePermission{}: {
+ handlers.WritePermission{},
+ },
routes.TenantControlPlaneDefaults{}: {
handlers.TenantControlPlaneDefaults{
DefaultDatastore: datastore,
diff --git a/controllers/soot/controllers/write_permissions.go b/controllers/soot/controllers/write_permissions.go
new file mode 100644
index 0000000..9e4ea44
--- /dev/null
+++ b/controllers/soot/controllers/write_permissions.go
@@ -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",
+ },
+ }
+}
diff --git a/controllers/soot/manager.go b/controllers/soot/manager.go
index f0064a8..385c384 100644
--- a/controllers/soot/manager.go
+++ b/controllers/soot/manager.go
@@ -253,6 +253,19 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
//
// 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{
WebhookNamespace: m.MigrateServiceNamespace,
WebhookServiceName: m.MigrateServiceName,
@@ -370,6 +383,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
m.sootMap[request.NamespacedName.String()] = sootItem{
triggers: []chan event.GenericEvent{
+ writePermissions.TriggerChannel,
migrate.TriggerChannel,
konnectivityAgent.TriggerChannel,
kubeProxy.TriggerChannel,
diff --git a/docs/content/guides/write-permissions.md b/docs/content/guides/write-permissions.md
new file mode 100644
index 0000000..ab51f41
--- /dev/null
+++ b/docs/content/guides/write-permissions.md
@@ -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.
diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md
index 030f78d..668a2c1 100644
--- a/docs/content/reference/api.md
+++ b/docs/content/reference/api.md
@@ -28523,6 +28523,18 @@ DataStoreUsername by concatenating the namespace and name of the TenantControlPl
NetworkProfile specifies how the network is
| Name | +Type | +Description | +Required | +
|---|---|---|---|
| blockCreation | +boolean | +
+ + |
+ false | +
| blockDeletion | +boolean | +
+ + |
+ false | +
| blockUpdate | +boolean | +
+ + |
+ false | +