|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one or more | 
|  | * contributor license agreements.  See the NOTICE file distributed with | 
|  | * this work for additional information regarding copyright ownership. | 
|  | * The ASF licenses this file to You 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 webhooks | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "fmt" | 
|  | "net/http" | 
|  | "strings" | 
|  | ) | 
|  |  | 
|  | import ( | 
|  | v1 "k8s.io/api/admission/v1" | 
|  |  | 
|  | authenticationv1 "k8s.io/api/authentication/v1" | 
|  |  | 
|  | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  | kube_runtime "k8s.io/apimachinery/pkg/runtime" | 
|  |  | 
|  | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" | 
|  | ) | 
|  |  | 
|  | import ( | 
|  | mesh_proto "github.com/apache/dubbo-kubernetes/api/mesh/v1alpha1" | 
|  | "github.com/apache/dubbo-kubernetes/pkg/config/core" | 
|  | core_model "github.com/apache/dubbo-kubernetes/pkg/core/resources/model" | 
|  | core_registry "github.com/apache/dubbo-kubernetes/pkg/core/resources/registry" | 
|  | "github.com/apache/dubbo-kubernetes/pkg/core/validators" | 
|  | k8s_common "github.com/apache/dubbo-kubernetes/pkg/plugins/common/k8s" | 
|  | k8s_model "github.com/apache/dubbo-kubernetes/pkg/plugins/resources/k8s/native/pkg/model" | 
|  | k8s_registry "github.com/apache/dubbo-kubernetes/pkg/plugins/resources/k8s/native/pkg/registry" | 
|  | "github.com/apache/dubbo-kubernetes/pkg/version" | 
|  | ) | 
|  |  | 
|  | func NewValidatingWebhook( | 
|  | converter k8s_common.Converter, | 
|  | coreRegistry core_registry.TypeRegistry, | 
|  | k8sRegistry k8s_registry.TypeRegistry, | 
|  | mode core.CpMode, | 
|  | federatedZone bool, | 
|  | disableOriginLabelValidation bool, | 
|  | ) k8s_common.AdmissionValidator { | 
|  | return &validatingHandler{ | 
|  | coreRegistry:                 coreRegistry, | 
|  | k8sRegistry:                  k8sRegistry, | 
|  | converter:                    converter, | 
|  | mode:                         mode, | 
|  | federatedZone:                federatedZone, | 
|  | disableOriginLabelValidation: disableOriginLabelValidation, | 
|  | } | 
|  | } | 
|  |  | 
|  | type validatingHandler struct { | 
|  | coreRegistry                 core_registry.TypeRegistry | 
|  | k8sRegistry                  k8s_registry.TypeRegistry | 
|  | converter                    k8s_common.Converter | 
|  | decoder                      admission.Decoder | 
|  | mode                         core.CpMode | 
|  | federatedZone                bool | 
|  | disableOriginLabelValidation bool | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) InjectDecoder(d admission.Decoder) { | 
|  | h.decoder = d | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response { | 
|  | _, err := h.coreRegistry.DescriptorFor(core_model.ResourceType(req.Kind.Kind)) | 
|  | if err != nil { | 
|  | // we only care about types in the registry for this handler | 
|  | return admission.Allowed("") | 
|  | } | 
|  |  | 
|  | coreRes, k8sObj, err := h.decode(req) | 
|  | if err != nil { | 
|  | return admission.Errored(http.StatusBadRequest, err) | 
|  | } | 
|  |  | 
|  | if resp := h.isOperationAllowed(req.UserInfo, coreRes); !resp.Allowed { | 
|  | return resp | 
|  | } | 
|  |  | 
|  | switch req.Operation { | 
|  | case v1.Delete: | 
|  | return admission.Allowed("") | 
|  | default: | 
|  | if err := h.validateLabels(coreRes.GetMeta()); err.HasViolations() { | 
|  | return convertValidationErrorOf(err, k8sObj, k8sObj.GetObjectMeta()) | 
|  | } | 
|  |  | 
|  | return admission.Allowed("") | 
|  | } | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) decode(req admission.Request) (core_model.Resource, k8s_model.KubernetesObject, error) { | 
|  | coreRes, err := h.coreRegistry.NewObject(core_model.ResourceType(req.Kind.Kind)) | 
|  | if err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  | k8sObj, err := h.k8sRegistry.NewObject(coreRes.GetSpec()) | 
|  | if err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  |  | 
|  | switch req.Operation { | 
|  | case v1.Delete: | 
|  | if err := h.decoder.DecodeRaw(req.OldObject, k8sObj); err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  | default: | 
|  | if err := h.decoder.Decode(req, k8sObj); err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  | } | 
|  |  | 
|  | if err := h.converter.ToCoreResource(k8sObj, coreRes); err != nil { | 
|  | return nil, nil, err | 
|  | } | 
|  | return coreRes, k8sObj, nil | 
|  | } | 
|  |  | 
|  | // Note that this func does not validate ConfigMap and Secret since this webhook does not support those | 
|  | func (h *validatingHandler) isOperationAllowed(userInfo authenticationv1.UserInfo, r core_model.Resource) admission.Response { | 
|  | if !h.isResourceTypeAllowed(r.Descriptor()) { | 
|  | return resourceTypeIsNotAllowedResponse(r.Descriptor().Name, h.mode) | 
|  | } | 
|  |  | 
|  | if !h.isResourceAllowed(r) { | 
|  | return resourceIsNotAllowedResponse() | 
|  | } | 
|  |  | 
|  | return admission.Allowed("") | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) isResourceTypeAllowed(d core_model.ResourceTypeDescriptor) bool { | 
|  | if d.DDSFlags == core_model.DDSDisabledFlag { | 
|  | return true | 
|  | } | 
|  | if h.mode == core.Global && !d.DDSFlags.Has(core_model.AllowedOnGlobalSelector) { | 
|  | return false | 
|  | } | 
|  | if h.federatedZone && !d.DDSFlags.Has(core_model.AllowedOnZoneSelector) { | 
|  | return false | 
|  | } | 
|  | return true | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) isResourceAllowed(r core_model.Resource) bool { | 
|  | if !h.federatedZone || !r.Descriptor().IsPluginOriginated { | 
|  | return true | 
|  | } | 
|  | if !h.disableOriginLabelValidation { | 
|  | if origin, ok := core_model.ResourceOrigin(r.GetMeta()); !ok || origin != mesh_proto.ZoneResourceOrigin { | 
|  | return false | 
|  | } | 
|  | } | 
|  | return true | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) validateLabels(rm core_model.ResourceMeta) validators.ValidationError { | 
|  | var verr validators.ValidationError | 
|  | if origin, ok := core_model.ResourceOrigin(rm); ok { | 
|  | if err := origin.IsValid(); err != nil { | 
|  | verr.AddViolationAt(validators.Root().Field("labels").Key(mesh_proto.ResourceOriginLabel), err.Error()) | 
|  | } | 
|  | } | 
|  | return verr | 
|  | } | 
|  |  | 
|  | func resourceIsNotAllowedResponse() admission.Response { | 
|  | return admission.Response{ | 
|  | AdmissionResponse: v1.AdmissionResponse{ | 
|  | Allowed: false, | 
|  | Result: &metav1.Status{ | 
|  | Status:  "Failure", | 
|  | Message: fmt.Sprintf("Operation not allowed. Applying policies on Zone CP requires '%s' label to be set to '%s'.", mesh_proto.ResourceOriginLabel, mesh_proto.ZoneResourceOrigin), | 
|  | Reason:  "Forbidden", | 
|  | Code:    403, | 
|  | Details: &metav1.StatusDetails{ | 
|  | Causes: []metav1.StatusCause{ | 
|  | { | 
|  | Type:    "FieldValueInvalid", | 
|  | Message: "cannot be empty", | 
|  | Field:   "metadata.labels[dubbo.io/origin]", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | } | 
|  | } | 
|  |  | 
|  | func resourceTypeIsNotAllowedResponse(resType core_model.ResourceType, cpMode core.CpMode) admission.Response { | 
|  | otherCpMode := "" | 
|  | if cpMode == core.Zone { | 
|  | otherCpMode = core.Global | 
|  | } else if cpMode == core.Global { | 
|  | otherCpMode = core.Zone | 
|  | } | 
|  | return admission.Response{ | 
|  | AdmissionResponse: v1.AdmissionResponse{ | 
|  | Allowed: false, | 
|  | Result: &metav1.Status{ | 
|  | Status: "Failure", | 
|  | Message: fmt.Sprintf("Operation not allowed. %s resources like %s can be updated or deleted only "+ | 
|  | "from the %s control plane and not from a %s control plane.", version.Product, resType, strings.ToUpper(otherCpMode), strings.ToUpper(cpMode)), | 
|  | Reason: "Forbidden", | 
|  | Code:   403, | 
|  | Details: &metav1.StatusDetails{ | 
|  | Causes: []metav1.StatusCause{ | 
|  | { | 
|  | Type:    "FieldValueInvalid", | 
|  | Message: "cannot be empty", | 
|  | Field:   "metadata.annotations[dubbo.io/synced]", | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | }, | 
|  | } | 
|  | } | 
|  |  | 
|  | func (h *validatingHandler) Supports(admission.Request) bool { | 
|  | return true | 
|  | } | 
|  |  | 
|  | func convertValidationErrorOf(dubboErr validators.ValidationError, obj kube_runtime.Object, objMeta metav1.Object) admission.Response { | 
|  | details := &metav1.StatusDetails{ | 
|  | Name: objMeta.GetName(), | 
|  | Kind: obj.GetObjectKind().GroupVersionKind().Kind, | 
|  | } | 
|  | resp := admission.Response{ | 
|  | AdmissionResponse: v1.AdmissionResponse{ | 
|  | Allowed: false, | 
|  | Result: &metav1.Status{ | 
|  | Status:  "Failure", | 
|  | Message: dubboErr.Error(), | 
|  | Reason:  "Invalid", | 
|  | Code:    int32(422), | 
|  | Details: details, | 
|  | }, | 
|  | }, | 
|  | } | 
|  | for _, violation := range dubboErr.Violations { | 
|  | cause := metav1.StatusCause{ | 
|  | Type:    "FieldValueInvalid", | 
|  | Message: violation.Message, | 
|  | Field:   violation.Field, | 
|  | } | 
|  | details.Causes = append(details.Causes, cause) | 
|  | } | 
|  | return resp | 
|  | } |