blob: 6e15bd33f56751d3ddc1cb3cc108c01afeacde31 [file] [log] [blame]
/*
* 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
}