mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-14 18:10:03 +00:00
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:
committed by
GitHub
parent
2b707423ff
commit
de459fb5da
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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:
|
// 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,
|
||||||
|
|||||||
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/>
|
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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
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