Skip to content

Commit

Permalink
Add Tenant validator webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
claudiolor committed Feb 26, 2025
1 parent 391dec0 commit 90ef4f6
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 12 deletions.
2 changes: 2 additions & 0 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
routecfgwh "github.com/liqotech/liqo/pkg/webhooks/routeconfiguration"
"github.com/liqotech/liqo/pkg/webhooks/secretcontroller"
shadowpodswh "github.com/liqotech/liqo/pkg/webhooks/shadowpod"
tenantwh "github.com/liqotech/liqo/pkg/webhooks/tenant"
virtualnodewh "github.com/liqotech/liqo/pkg/webhooks/virtualnode"
)

Expand Down Expand Up @@ -199,6 +200,7 @@ func main() {
mgr.GetWebhookServer().Register("/validate/firewallconfigurations", fwcfgwh.NewValidator(mgr.GetClient()))
mgr.GetWebhookServer().Register("/mutate/firewallconfigurations", fwcfgwh.NewMutator())
mgr.GetWebhookServer().Register("/validate/routeconfigurations", routecfgwh.NewValidator(mgr.GetClient()))
mgr.GetWebhookServer().Register("/validate/tenants", tenantwh.NewValidator(mgr.GetClient()))

// Register the secret controller
secretReconciler := secretcontroller.NewSecretReconciler(mgr.GetClient(), mgr.GetScheme(),
Expand Down
1 change: 1 addition & 0 deletions deployments/liqo/files/liqo-webhook-ClusterRole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ rules:
- authentication.liqo.io
resources:
- resourceslices
- tenants
verbs:
- get
- list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ import (
)

var (
// tenantClusterRolesLabelValues is the list "app.kubernetes.io/name" label values assigned to the ClusterRoles to be binded to the control-plane Identity.
// tenantClusterRolesLabelValues is the list "app.kubernetes.io/name" label values assigned to the
// ClusterRoles to be binded to the control-plane Identity.
tenantClusterRolesLabelValues = []string{
"remote-controlplane",
}

// tenantClusterRolesClusterWideLabelValues is the list "app.kubernetes.io/name" label values assigned to the ClusterRoles to be binded to the
// control-plane Identity with cluster-wide scope.
// tenantClusterRolesClusterWideLabelValues is the list "app.kubernetes.io/name" label values assigned to the
// ClusterRoles to be binded to the control-plane Identity with cluster-wide scope.
tenantClusterRolesClusterWideLabelValues = []string{
"virtual-kubelet-remote-clusterwide",
}
Expand Down Expand Up @@ -279,24 +280,24 @@ func (r *TenantReconciler) getClusterRoles(ctx context.Context, rolesAppLabel []
}

func (r *TenantReconciler) ensureSetup(ctx context.Context) error {
if r.tenantClusterRoles == nil || len(r.tenantClusterRoles) == 0 {
if len(r.tenantClusterRoles) == 0 {
cRoles, err := r.getClusterRoles(ctx, tenantClusterRolesLabelValues)

if err != nil {
return fmt.Errorf("Unable to get ClusterRoles to bind on tenant namespace: %w", err)
} else {
r.tenantClusterRoles = cRoles
return fmt.Errorf("unable to get ClusterRoles to bind on tenant namespace: %w", err)
}

r.tenantClusterRoles = cRoles
}

if r.tenantClusterRolesClusterWide == nil || len(r.tenantClusterRolesClusterWide) == 0 {
if len(r.tenantClusterRolesClusterWide) == 0 {
cRoles, err := r.getClusterRoles(ctx, tenantClusterRolesClusterWideLabelValues)

if err != nil {
return fmt.Errorf("Unable to get ClusterRoles to bind cluster-wide: %w", err)
} else {
r.tenantClusterRolesClusterWide = cRoles
return fmt.Errorf("unable to get ClusterRoles to bind cluster-wide: %w", err)
}

r.tenantClusterRolesClusterWide = cRoles
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/utils/getters/k8sGetters.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func GetTenantByClusterID(
}
}

// GetTenantByClusterID returns the Tenant resource for the given cluster id.
// GetTenantByName returns the Tenant resource given its name.
func GetTenantByName(
ctx context.Context, cl client.Client, name string, tenantNamespace string) (*authv1beta1.Tenant, error) {
list := new(authv1beta1.TenantList)
Expand Down
16 changes: 16 additions & 0 deletions pkg/webhooks/tenant/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2019-2025 The Liqo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package tenant contains the logic of the Tenant webhook.
package tenant
179 changes: 179 additions & 0 deletions pkg/webhooks/tenant/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2019-2025 The Liqo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tenant

import (
"context"
"errors"
"fmt"
"net/http"

admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1"
"github.com/liqotech/liqo/pkg/consts"
"github.com/liqotech/liqo/pkg/liqoctl/output"
)

// cluster-role
// +kubebuilder:rbac:groups=authentication.liqo.io,resources=tenants,verbs=get;list;watch;
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;

type tenantDecoder struct {
decoder admission.Decoder
}

type tenantWebhookHandler struct {
client client.Client
tenantDecoder
}

// NewValidator returns a new Tenant validating webhook.
func NewValidator(cl client.Client) *webhook.Admission {
return &webhook.Admission{
Handler: &tenantWebhookHandler{
tenantDecoder: tenantDecoder{
decoder: admission.NewDecoder(runtime.NewScheme()),
},
client: cl,
},
}
}

// DecodeTenant decodes the Tenant from the incoming request.
func (w *tenantDecoder) DecodeTenant(obj runtime.RawExtension) (*authv1beta1.Tenant, error) {
var tenant authv1beta1.Tenant
err := w.decoder.DecodeRaw(obj, &tenant)
return &tenant, err
}

// Handle implements the Tenant validating webhook logic.
func (w *tenantWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
switch req.Operation {
case admissionv1.Create:
return w.handleCreate(ctx, &req)
case admissionv1.Update:
return w.handleUpdate(ctx, &req)
default:
return admission.Allowed("")
}
}

func (w *tenantWebhookHandler) handleCreate(ctx context.Context, req *admission.Request) admission.Response {
tenant, err := w.DecodeTenant(req.Object)
if err != nil {
klog.Errorf("Failed decoding Tenant object: %v", err)
return admission.Errored(http.StatusInternalServerError, err)
}

if status, err := w.tenantConsistencyChecks(ctx, tenant); err != nil {
return admission.Errored(status, err)
}

// Check that there is one single Tenant in the tenant namespace.
tenantsInNamespace, err := w.getTenants(ctx, tenant.Namespace, nil)
if err != nil {
werr := fmt.Errorf("Failed getting Tenants in tenant namespace: %v", output.PrettyErr(err))
klog.Error(werr)
return admission.Errored(http.StatusInternalServerError, werr)
}

if len(tenantsInNamespace) > 0 {
return admission.Denied("a Tenant already exists in the tenant namespace")
}

// Check that the Tenant name is unique in the entire cluster.
tenantsInCluster, err := w.getTenants(ctx, corev1.NamespaceAll, &tenant.Name)
if err != nil {
werr := fmt.Errorf("Failed getting Tenants in cluster: %v", output.PrettyErr(err))
klog.Error(werr)
return admission.Errored(http.StatusInternalServerError, werr)
}

if len(tenantsInCluster) > 0 {
return admission.Denied("a Tenant with the same name already exists in the cluster")
}

return admission.Allowed("")
}

func (w *tenantWebhookHandler) handleUpdate(ctx context.Context, req *admission.Request) admission.Response {
tenant, err := w.DecodeTenant(req.Object)
if err != nil {
klog.Errorf("Failed decoding Tenant object: %v", err)
return admission.Errored(http.StatusInternalServerError, err)
}

if status, err := w.tenantConsistencyChecks(ctx, tenant); err != nil {
return admission.Errored(status, err)
}

// Check that the Tenant name is unique in the tenant namespace.
tenantsInCluster, err := w.getTenants(ctx, corev1.NamespaceAll, &tenant.Name)
if err != nil {
werr := fmt.Errorf("Failed getting Tenants in cluster: %v", output.PrettyErr(err))
klog.Error(werr)
return admission.Errored(http.StatusInternalServerError, werr)
}

// If there already is a Tenant with the given name but it is not the same Tenant, deny the update.
if len(tenantsInCluster) == 1 && tenantsInCluster[0].UID != tenant.UID {
return admission.Denied("a Tenant with the same name already exists in the cluster")
}

return admission.Allowed("")
}

func (w *tenantWebhookHandler) tenantConsistencyChecks(ctx context.Context, tenant *authv1beta1.Tenant) (code int32, err error) {
// Check that the Tenant has been created in the proper tenant namespace.
var ns corev1.Namespace
if err := w.client.Get(ctx, client.ObjectKey{Name: tenant.Namespace}, &ns); err != nil {
werr := fmt.Errorf("Failed getting the tenant namespace: %v", output.PrettyErr(err))
return http.StatusInternalServerError, werr
}

if ns.Labels[consts.TenantNamespaceLabel] != "true" {
return http.StatusForbidden, errors.New("the Tenant must be created in a tenant namespace")
}

// Check that the Tenant has the same cluster ID of its tenant namespace.
if string(tenant.Spec.ClusterID) != ns.Labels[consts.RemoteClusterID] {
return http.StatusForbidden, errors.New("the Tenant must have the same cluster ID of its tenant namespace")
}

return http.StatusOK, nil
}

func (w *tenantWebhookHandler) getTenants(ctx context.Context, namespace string, name *string) ([]authv1beta1.Tenant, error) {
var tenantList authv1beta1.TenantList

opts := []client.ListOption{
client.InNamespace(namespace),
}
if name != nil {
opts = append(opts, client.MatchingFields{"metadata.name": *name})
}
if err := w.client.List(ctx, &tenantList, opts...); err != nil {
return nil, err
}

return tenantList.Items, nil
}

0 comments on commit 90ef4f6

Please sign in to comment.