mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-28 16:50:29 +00:00
Compare commits
7 Commits
edge-25.9.
...
edge-25.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc85d8b73c | ||
|
|
de459fb5da | ||
|
|
2b707423ff | ||
|
|
285cef0f02 | ||
|
|
f6686f6efa | ||
|
|
2a809a79c4 | ||
|
|
f477df2a84 |
@@ -123,6 +123,8 @@ Since Kamaji is just focusing on the Control Plane a [Kamaji's Cluster API Contr
|
||||
- YouTube ▶️ [Rancher & Kamaji: solving multitenancy challenges in the Kubernetes world](https://www.youtube.com/watch?v=VXHNrMmlF8U)
|
||||
- YouTube ▶️ [Enabling Self-Service Kubernetes clusters with Kamaji and Paralus](https://www.youtube.com/watch?v=JWA2LwZazM0)
|
||||
- YouTube ▶️ [Hosted Control Plane on Kubernetes (HPC) with Kamaji and K0mostron by Hervé Leclerc, ALTER WAY](https://www.youtube.com/watch?v=vmRdE2ngn78)
|
||||
- Medium 📖 [Set up Virtual Control Planes with Kamaji on Minikube, by Ben Soer](https://medium.com/@bensoer/set-up-virtual-control-planes-with-kamaji-on-minikube-a540be0275aa)
|
||||
- Hands-On tutorial 📖 [How to build your own managed Kubernetes service on Hetzner Cloud, by Hans Jörg Wieland](https://wieland.tech/blog/kamaji-cluster-api-and-etcd)
|
||||
|
||||
### 🏷️ Versioning
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
207
controllers/soot/controllers/write_permissions.go
Normal file
207
controllers/soot/controllers/write_permissions.go
Normal 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",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
115
docs/content/guides/write-permissions.md
Normal file
115
docs/content/guides/write-permissions.md
Normal 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.
|
||||
@@ -28523,6 +28523,18 @@ DataStoreUsername by concatenating the namespace and name of the TenantControlPl
|
||||
NetworkProfile specifies how the network is<br/>
|
||||
</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>
|
||||
</table>
|
||||
|
||||
@@ -41987,6 +41999,50 @@ Example: {"192.168.1.0/24", "10.0.0.0/8"}<br/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -44111,7 +44167,7 @@ KubernetesVersion contains the information regarding the running Kubernetes vers
|
||||
<td>
|
||||
Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.<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/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
|
||||
@@ -74,6 +74,7 @@ nav:
|
||||
- guides/backup-and-restore.md
|
||||
- guides/certs-lifecycle.md
|
||||
- guides/pausing.md
|
||||
- guides/write-permissions.md
|
||||
- guides/datastore-migration.md
|
||||
- guides/gitops.md
|
||||
- guides/console.md
|
||||
|
||||
7
go.mod
7
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/JamesStewy/go-mysqldump v0.2.2
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/clastix/kamaji-telemetry v1.0.0
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.0+incompatible
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-pg/pg/v10 v10.15.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
@@ -14,8 +14,8 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/juju/mutex/v2 v2.0.0
|
||||
github.com/nats-io/nats.go v1.46.0
|
||||
github.com/onsi/ginkgo/v2 v2.25.3
|
||||
github.com/nats-io/nats.go v1.46.1
|
||||
github.com/onsi/ginkgo/v2 v2.26.0
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
@@ -163,6 +163,7 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -58,8 +58,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
|
||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
|
||||
github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -83,6 +83,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||
github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo=
|
||||
github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -112,6 +118,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
@@ -158,6 +166,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A=
|
||||
@@ -205,6 +215,10 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
@@ -233,8 +247,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.46.0 h1:iUcX+MLT0HHXskGkz+Sg20sXrPtJLsOojMDTDzOHSb8=
|
||||
github.com/nats-io/nats.go v1.46.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nats.go v1.46.1 h1:bqQ2ZcxVd2lpYI97xYASeRTY3I5boe/IVmuUDPitHfo=
|
||||
github.com/nats-io/nats.go v1.46.1/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
@@ -243,8 +257,8 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -318,6 +332,14 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -407,6 +429,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbR
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
||||
@@ -36,7 +36,9 @@ func (r *KubernetesDeploymentResource) isStatusEqual(tenantControlPlane *kamajiv
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -78,19 +80,29 @@ func (r *KubernetesDeploymentResource) GetName() string {
|
||||
return "deployment"
|
||||
}
|
||||
|
||||
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
|
||||
func (r *KubernetesDeploymentResource) computeStatus(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) *kamajiv1alpha1.KubernetesVersionStatus {
|
||||
switch {
|
||||
case ptr.Deref(tenantControlPlane.Spec.ControlPlane.Deployment.Replicas, 2) == 0:
|
||||
tenantControlPlane.Status.Kubernetes.Version.Status = &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
|
||||
return &kamajiv1alpha1.VersionSleeping
|
||||
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{
|
||||
|
||||
@@ -5,6 +5,7 @@ package konnectivity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -102,6 +103,16 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
|
||||
return func() error {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
// Retrieving the TenantControlPlane CA:
|
||||
// this is required to trigger a new generation in case of Certificate Authority rotation.
|
||||
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
|
||||
secretCA := &corev1.Secret{}
|
||||
if err := r.Client.Get(ctx, namespacedName, secretCA); err != nil {
|
||||
logger.Error(err, "cannot retrieve the CA secret")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
@@ -119,23 +130,20 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
|
||||
isRotationRequested := utilities.IsRotationRequested(r.resource)
|
||||
|
||||
if checksum := tenantControlPlane.Status.Addons.Konnectivity.Certificate.Checksum; !isRotationRequested && (len(checksum) > 0 && checksum == utilities.CalculateMapChecksum(r.resource.Data)) {
|
||||
isCAValid, err := crypto.VerifyCertificate(r.resource.Data[corev1.TLSCertKey], secretCA.Data[kubeadmconstants.CACertName], x509.ExtKeyUsageServerAuth)
|
||||
if err != nil {
|
||||
logger.Info(fmt.Sprintf("certificate-authority verify failed: %s", err.Error()))
|
||||
}
|
||||
|
||||
isValid, err := crypto.IsValidCertificateKeyPairBytes(r.resource.Data[corev1.TLSCertKey], r.resource.Data[corev1.TLSPrivateKeyKey], r.CertExpirationThreshold)
|
||||
if err != nil {
|
||||
logger.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", konnectivityCertAndKeyBaseName, err.Error()))
|
||||
}
|
||||
if isValid {
|
||||
if isCAValid && isValid {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
|
||||
secretCA := &corev1.Secret{}
|
||||
if err := r.Client.Get(ctx, namespacedName, secretCA); err != nil {
|
||||
logger.Error(err, "cannot retrieve the CA secret")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
ca := kubeadm.CertificatePrivateKeyPair{
|
||||
Name: kubeadmconstants.CACertAndKeyBaseName,
|
||||
Certificate: secretCA.Data[kubeadmconstants.CACertName],
|
||||
|
||||
@@ -25,18 +25,21 @@ type handlersChainer struct {
|
||||
//nolint:gocognit
|
||||
func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handlers.Handler) admission.HandlerFunc {
|
||||
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 {
|
||||
case admissionv1.Delete:
|
||||
// When deleting the OldObject struct field contains the object being deleted:
|
||||
// https://github.com/kubernetes/kubernetes/pull/76346
|
||||
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)))
|
||||
}
|
||||
default:
|
||||
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)))
|
||||
switch req.Operation {
|
||||
case admissionv1.Delete:
|
||||
// When deleting the OldObject struct field contains the object being deleted:
|
||||
// https://github.com/kubernetes/kubernetes/pull/76346
|
||||
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)))
|
||||
}
|
||||
default:
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
internal/webhook/handlers/write_permission.go
Normal file
32
internal/webhook/handlers/write_permission.go
Normal 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")
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -15,5 +14,5 @@ func (t TenantControlPlaneMigrate) GetPath() string {
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneMigrate) GetObject() runtime.Object {
|
||||
return &corev1.Namespace{}
|
||||
return nil
|
||||
}
|
||||
|
||||
18
internal/webhook/routes/tcp_writepermission.go
Normal file
18
internal/webhook/routes/tcp_writepermission.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user