pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go (176 lines of code) (raw):
package fleetresourcehandler
import (
"context"
"fmt"
"net/http"
"regexp"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1"
fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/pkg/utils"
"go.goms.io/fleet/pkg/webhook/validation"
)
const (
// ValidationPath is the webhook service path which admission requests are routed to for validating custom resource definition resources.
ValidationPath = "/validate-fleetresourcehandler"
groupMatch = `^[^.]*\.(.*)`
)
const (
// allowed messages.
allowedMessageMemberCluster = "upstream member cluster resource is allowed to be created/deleted by any user"
allowedMessageNonReservedNamespace = "namespace name doesn't begin with fleet-/kube- prefix so we allow all operations on this namespace"
allowedMessageFleetReservedNamespacedResource = "namespace name of resource object doesn't begin with fleet-/kube- prefix so we allow all operations on request objects in these namespace"
)
// Add registers the webhook for K8s built-in object types.
func Add(mgr manager.Manager, whiteListedUsers []string, isFleetV1Beta1API bool) error {
hookServer := mgr.GetWebhookServer()
handler := &fleetResourceValidator{
client: mgr.GetClient(),
whiteListedUsers: whiteListedUsers,
isFleetV1Beta1API: isFleetV1Beta1API,
decoder: admission.NewDecoder(mgr.GetScheme()),
}
hookServer.Register(ValidationPath, &webhook.Admission{Handler: handler})
return nil
}
type fleetResourceValidator struct {
client client.Client
whiteListedUsers []string
isFleetV1Beta1API bool
decoder webhook.AdmissionDecoder
}
// Handle receives the request then allows/denies the request to modify fleet resources.
func (v *fleetResourceValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
// special case for Kind:Namespace resources req.Name and req.Namespace has the same value the ObjectMeta.Name of Namespace.
if req.Kind.Kind == "Namespace" {
req.Namespace = ""
}
namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace}
var response admission.Response
if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update || req.Operation == admissionv1.Delete {
switch {
case req.Kind == utils.CRDMetaGVK:
klog.V(2).InfoS("handling CRD resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleCRD(req)
case req.Kind == utils.MCV1Alpha1MetaGVK:
klog.V(2).InfoS("handling v1alpha1 member cluster resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleV1Alpha1MemberCluster(req)
case req.Kind == utils.MCMetaGVK:
klog.V(2).InfoS("handling member cluster resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleMemberCluster(req)
case req.Kind == utils.NamespaceMetaGVK:
klog.V(2).InfoS("handling namespace resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleNamespace(req)
case req.Kind == utils.IMCV1Alpha1MetaGVK || req.Kind == utils.WorkV1Alpha1MetaGVK || req.Kind == utils.IMCMetaGVK || req.Kind == utils.WorkMetaGVK || req.Kind == utils.EndpointSliceExportMetaGVK || req.Kind == utils.EndpointSliceImportMetaGVK || req.Kind == utils.InternalServiceExportMetaGVK || req.Kind == utils.InternalServiceImportMetaGVK:
klog.V(2).InfoS("handling fleet owned namespaced resource in fleet reserved namespaces", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleFleetReservedNamespacedResource(ctx, req)
case req.Kind == utils.EventMetaGVK:
klog.V(3).InfoS("handling event resource", "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleEvent(ctx, req)
case req.Namespace != "":
klog.V(2).InfoS("handling namespaced resource in fleet reserved namespaces", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = validation.ValidateUserForResource(req, v.whiteListedUsers)
default:
klog.V(3).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = admission.Allowed(fmt.Sprintf("user: %s in groups: %v is allowed to modify resource with GVK: %s", req.UserInfo.Username, req.UserInfo.Groups, req.Kind.String()))
}
}
return response
}
// handleCRD allows/denies the request to modify CRD object after validation.
func (v *fleetResourceValidator) handleCRD(req admission.Request) admission.Response {
var group string
// This regex works because every CRD name in kubernetes follows this pattern <plural>.<group>.
match := regexp.MustCompile(groupMatch).FindStringSubmatch(req.Name)
if len(match) > 1 {
group = match[1]
}
return validation.ValidateUserForFleetCRD(req, v.whiteListedUsers, group)
}
// handleV1Alpha1MemberCluster allows/denies the request to modify v1alpha1 member cluster object after validation.
func (v *fleetResourceValidator) handleV1Alpha1MemberCluster(req admission.Request) admission.Response {
var currentMC fleetv1alpha1.MemberCluster
if err := v.decodeRequestObject(req, ¤tMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if req.Operation == admissionv1.Update {
var oldMC fleetv1alpha1.MemberCluster
if err := v.decoder.DecodeRaw(req.OldObject, &oldMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return validation.ValidateV1Alpha1MemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers)
}
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}
// handleMemberCluster allows/denies the request to modify member cluster object after validation.
func (v *fleetResourceValidator) handleMemberCluster(req admission.Request) admission.Response {
var currentMC clusterv1beta1.MemberCluster
if err := v.decodeRequestObject(req, ¤tMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if req.Operation == admissionv1.Update {
var oldMC clusterv1beta1.MemberCluster
if err := v.decoder.DecodeRaw(req.OldObject, &oldMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
isFleetMC := utils.IsFleetAnnotationPresent(oldMC.Annotations)
if isFleetMC {
return validation.ValidateFleetMemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers)
}
return validation.ValidatedUpstreamMemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers)
}
isFleetMC := utils.IsFleetAnnotationPresent(currentMC.Annotations)
if isFleetMC {
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}
klog.V(3).InfoS(allowedMessageMemberCluster,
"user", req.UserInfo.Username, "groups", req.UserInfo.Groups, "operation", req.Operation, "kind", req.RequestKind.Kind, "subResource", req.SubResource, "namespacedName", types.NamespacedName{Name: req.Name, Namespace: req.Namespace})
return admission.Allowed(allowedMessageMemberCluster)
}
// handleFleetReservedNamespacedResource allows/denies the request to modify object after validation.
func (v *fleetResourceValidator) handleFleetReservedNamespacedResource(ctx context.Context, req admission.Request) admission.Response {
var response admission.Response
if utils.IsFleetMemberNamespace(req.Namespace) {
// check to see if valid users other than member agent is making the request.
response = validation.ValidateUserForResource(req, v.whiteListedUsers)
// check to see if member agent is making the request only on Update.
if !response.Allowed {
// if namespace name is just "fleet-member", mcName variable becomes empty and the request is allowed since that namespaces is not watched by member agents.
mcName := parseMemberClusterNameFromNamespace(req.Namespace)
return validation.ValidateMCIdentity(ctx, v.client, req, mcName, v.isFleetV1Beta1API)
}
return response
} else if utils.IsReservedNamespace(req.Namespace) {
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}
klog.V(3).InfoS(allowedMessageFleetReservedNamespacedResource,
"user", req.UserInfo.Username, "groups", req.UserInfo.Groups, "operation", req.Operation, "kind", req.RequestKind.Kind, "subResource", req.SubResource, "namespacedName", types.NamespacedName{Name: req.Name, Namespace: req.Namespace})
return admission.Allowed(allowedMessageFleetReservedNamespacedResource)
}
// handleEvent allows/denies request to modify event after validation.
func (v *fleetResourceValidator) handleEvent(_ context.Context, _ admission.Request) admission.Response {
// currently allowing all events will handle events after v1alpha1 resources are removed.
return admission.Allowed("all events are allowed")
}
// handlerNamespace allows/denies request to modify namespace after validation.
func (v *fleetResourceValidator) handleNamespace(req admission.Request) admission.Response {
if utils.IsReservedNamespace(req.Name) {
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}
klog.V(3).InfoS(allowedMessageNonReservedNamespace,
"user", req.UserInfo.Username, "groups", req.UserInfo.Groups, "operation", req.Operation, "kind", req.RequestKind.Kind, "subResource", req.SubResource, "namespacedName", types.NamespacedName{Name: req.Name, Namespace: req.Namespace})
return admission.Allowed(allowedMessageNonReservedNamespace)
}
// decodeRequestObject decodes the request object into the passed runtime object.
func (v *fleetResourceValidator) decodeRequestObject(req admission.Request, obj runtime.Object) error {
if req.Operation == admissionv1.Delete {
// req.Object is not populated for delete: https://github.com/kubernetes-sigs/controller-runtime/issues/1762.
if err := v.decoder.DecodeRaw(req.OldObject, obj); err != nil {
klog.ErrorS(err, "failed to decode old request object for delete operation", "userName", req.UserInfo.Username, "groups", req.UserInfo.Groups)
return err
}
} else {
if err := v.decoder.Decode(req, obj); err != nil {
klog.ErrorS(err, "failed to decode request object for create/update operation", "userName", req.UserInfo.Username, "groups", req.UserInfo.Groups)
return err
}
}
return nil
}
// parseMemberClusterNameFromNamespace returns member cluster name from fleet member cluster namespace.
// returns empty string if namespace is not a fleet member cluster namespace.
func parseMemberClusterNameFromNamespace(namespace string) string {
var mcName string
startIndex := len(utils.NamespaceNameFormat) - 2
if len(namespace) > startIndex {
mcName = namespace[startIndex:]
}
return mcName
}