From 655fd26cbfcc3fc6a5ac28fc907c1b307ccf1e24 Mon Sep 17 00:00:00 2001 From: Duan Jiong Date: Sun, 28 Jun 2020 09:44:05 +0800 Subject: [PATCH] fix nsnp webhook to validate all fileds in it such as ipblock, service. And the crd openAPIV3Schema could not validate all scenarios Signed-off-by: Duan Jiong --- cmd/controller-manager/app/server.go | 2 +- config/webhook/nsnp.yaml | 8 +- .../network/nsnetworkpolicy/webhook.go | 166 ++++++++++++++++-- 3 files changed, 156 insertions(+), 20 deletions(-) diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index 40a0ffba..8c2c7bdf 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -173,7 +173,7 @@ func Run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) klog.Info("registering webhooks to the webhook server") hookServer.Register("/validate-email-iam-kubesphere-io-v1alpha2-user", &webhook.Admission{Handler: &user.EmailValidator{Client: mgr.GetClient()}}) - hookServer.Register("/validate-service-nsnp-kubesphere-io-v1alpha1-network", &webhook.Admission{Handler: &nsnetworkpolicy.ServiceValidator{}}) + hookServer.Register("/validate-nsnp-kubesphere-io-v1alpha1-network", &webhook.Admission{Handler: &nsnetworkpolicy.NSNPValidator{Client: mgr.GetClient()}}) klog.V(0).Info("Starting the controllers.") if err = mgr.Start(stopCh); err != nil { diff --git a/config/webhook/nsnp.yaml b/config/webhook/nsnp.yaml index d02cd5a7..48aaec0b 100644 --- a/config/webhook/nsnp.yaml +++ b/config/webhook/nsnp.yaml @@ -2,23 +2,23 @@ apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: creationTimestamp: null - name: kubesphere-nsnp-validate-service + name: kubesphere-nsnp-validate webhooks: - clientConfig: caBundle: service: name: kubesphere-controller-manager-service namespace: kubesphere-system - path: /validate-service-nsnp-kubesphere-io-v1alpha1-network + path: /validate-nsnp-kubesphere-io-v1alpha1-network failurePolicy: Fail name: validate.nsnp.kubesphere.io rules: - apiGroups: - - "" + - network.kubesphere.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - - services + - namespacenetworkpolicies diff --git a/pkg/controller/network/nsnetworkpolicy/webhook.go b/pkg/controller/network/nsnetworkpolicy/webhook.go index 1a153456..bfbff705 100644 --- a/pkg/controller/network/nsnetworkpolicy/webhook.go +++ b/pkg/controller/network/nsnetworkpolicy/webhook.go @@ -2,37 +2,173 @@ package nsnetworkpolicy import ( "context" - "fmt" - "net/http" - corev1 "k8s.io/api/core/v1" + k8snet "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" + "net" + "net/http" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -// +kubebuilder:webhook:path=/validate-service-nsnp-kubesphere-io-v1alpha1-network,name=validate-v1-service,mutating=false,failurePolicy=fail,groups="",resources=services,verbs=create;update,versions=v1 - -// serviceValidator validates service -type ServiceValidator struct { +type NSNPValidator struct { + Client client.Client decoder *admission.Decoder } -// Service must hash label, becasue nsnp will use it -func (v *ServiceValidator) Handle(ctx context.Context, req admission.Request) admission.Response { - service := &corev1.Service{} +func (v *NSNPValidator) Handle(ctx context.Context, req admission.Request) admission.Response { + nsnp := &networkv1alpha1.NamespaceNetworkPolicy{} - err := v.decoder.Decode(req, service) + err := v.decoder.Decode(req, nsnp) if err != nil { return admission.Errored(http.StatusBadRequest, err) } - if service.Spec.Selector == nil { - return admission.Denied(fmt.Sprintf("missing label")) + allErrs := field.ErrorList{} + allErrs = append(allErrs, v.ValidateNSNPSpec(&nsnp.Spec, field.NewPath("spec"))...) + + if len(allErrs) != 0 { + return admission.Denied(allErrs.ToAggregate().Error()) } return admission.Allowed("") } -func (a *ServiceValidator) InjectDecoder(d *admission.Decoder) error { - a.decoder = d +func (v *NSNPValidator) InjectDecoder(d *admission.Decoder) error { + v.decoder = d return nil } + +// ValidateNetworkPolicyPort validates a NetworkPolicyPort +func (v *NSNPValidator) ValidateNetworkPolicyPort(port *k8snet.NetworkPolicyPort, portPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if port.Protocol != nil && *port.Protocol != corev1.ProtocolTCP && *port.Protocol != corev1.ProtocolUDP && *port.Protocol != corev1.ProtocolSCTP { + allErrs = append(allErrs, field.NotSupported(portPath.Child("protocol"), *port.Protocol, []string{string(corev1.ProtocolTCP), string(corev1.ProtocolUDP), string(corev1.ProtocolSCTP)})) + } + if port.Port != nil { + if port.Port.Type == intstr.Int { + for _, msg := range validation.IsValidPortNum(int(port.Port.IntVal)) { + allErrs = append(allErrs, field.Invalid(portPath.Child("port"), port.Port.IntVal, msg)) + } + } else { + for _, msg := range validation.IsValidPortName(port.Port.StrVal) { + allErrs = append(allErrs, field.Invalid(portPath.Child("port"), port.Port.StrVal, msg)) + } + } + } + + return allErrs +} + +func (v *NSNPValidator) ValidateServiceSelector(serviceSelector *networkv1alpha1.ServiceSelector, fldPath *field.Path) field.ErrorList { + service := &corev1.Service{} + allErrs := field.ErrorList{} + + err := v.Client.Get(context.TODO(), client.ObjectKey{Namespace: serviceSelector.Namespace, Name: serviceSelector.Name}, service) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath, serviceSelector, "cannot get service")) + return allErrs + } + + if len(service.Spec.Selector) <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath, serviceSelector, "service should have selector")) + } + + return allErrs +} + +// ValidateCIDR validates whether a CIDR matches the conventions expected by net.ParseCIDR +func ValidateCIDR(cidr string) (*net.IPNet, error) { + _, net, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + return net, nil +} + +// ValidateIPBlock validates a cidr and the except fields of an IpBlock NetworkPolicyPeer +func (v *NSNPValidator) ValidateIPBlock(ipb *k8snet.IPBlock, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(ipb.CIDR) == 0 || ipb.CIDR == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("cidr"), "")) + return allErrs + } + cidrIPNet, err := ValidateCIDR(ipb.CIDR) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), ipb.CIDR, "not a valid CIDR")) + return allErrs + } + exceptCIDR := ipb.Except + for i, exceptIP := range exceptCIDR { + exceptPath := fldPath.Child("except").Index(i) + exceptCIDR, err := ValidateCIDR(exceptIP) + if err != nil { + allErrs = append(allErrs, field.Invalid(exceptPath, exceptIP, "not a valid CIDR")) + return allErrs + } + if !cidrIPNet.Contains(exceptCIDR.IP) { + allErrs = append(allErrs, field.Invalid(exceptPath, exceptCIDR.IP, "not within CIDR range")) + } + } + return allErrs +} + +// ValidateNSNPPeer validates a NetworkPolicyPeer +func (v *NSNPValidator) ValidateNSNPPeer(peer *networkv1alpha1.NetworkPolicyPeer, peerPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + numPeers := 0 + + if peer.ServiceSelector != nil { + numPeers++ + allErrs = append(allErrs, v.ValidateServiceSelector(peer.ServiceSelector, peerPath.Child("service"))...) + } + if peer.NamespaceSelector != nil { + numPeers++ + } + if peer.IPBlock != nil { + numPeers++ + allErrs = append(allErrs, v.ValidateIPBlock(peer.IPBlock, peerPath.Child("ipBlock"))...) + } + + if numPeers == 0 { + allErrs = append(allErrs, field.Required(peerPath, "must specify a peer")) + } else if numPeers > 1 && peer.IPBlock != nil { + allErrs = append(allErrs, field.Forbidden(peerPath, "may not specify both ipBlock and another peer")) + } + + return allErrs +} + +func (v *NSNPValidator) ValidateNSNPSpec(spec *networkv1alpha1.NamespaceNetworkPolicySpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // Validate ingress rules. + for i, ingress := range spec.Ingress { + ingressPath := fldPath.Child("ingress").Index(i) + for i, port := range ingress.Ports { + portPath := ingressPath.Child("ports").Index(i) + allErrs = append(allErrs, v.ValidateNetworkPolicyPort(&port, portPath)...) + } + for i, from := range ingress.From { + fromPath := ingressPath.Child("from").Index(i) + allErrs = append(allErrs, v.ValidateNSNPPeer(&from, fromPath)...) + } + } + // Validate egress rules + for i, egress := range spec.Egress { + egressPath := fldPath.Child("egress").Index(i) + for i, port := range egress.Ports { + portPath := egressPath.Child("ports").Index(i) + allErrs = append(allErrs, v.ValidateNetworkPolicyPort(&port, portPath)...) + } + for i, to := range egress.To { + toPath := egressPath.Child("to").Index(i) + allErrs = append(allErrs, v.ValidateNSNPPeer(&to, toPath)...) + } + } + + return allErrs +} -- GitLab