diff --git a/pkg/webhooks/admission/hypernodes/validate/admit_hypernode.go b/pkg/webhooks/admission/hypernodes/validate/admit_hypernode.go new file mode 100644 index 0000000000..38e4cf6f50 --- /dev/null +++ b/pkg/webhooks/admission/hypernodes/validate/admit_hypernode.go @@ -0,0 +1,102 @@ +/* +Copyright 2024 The Volcano 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 validate + +import ( + "fmt" + + admissionv1 "k8s.io/api/admission/v1" + whv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/klog/v2" + + hypernodev1alpha1 "volcano.sh/apis/pkg/apis/topology/v1alpha1" + "volcano.sh/volcano/pkg/webhooks/router" + "volcano.sh/volcano/pkg/webhooks/schema" + "volcano.sh/volcano/pkg/webhooks/util" +) + +func init() { + router.RegisterAdmission(service) +} + +var config = &router.AdmissionServiceConfig{} + +var service = &router.AdmissionService{ + Path: "/hypernodes/validate", + Func: AdmitHyperNode, + + Config: config, + + ValidatingConfig: &whv1.ValidatingWebhookConfiguration{ + Webhooks: []whv1.ValidatingWebhook{{ + Name: "validatehypernode.volcano.sh", + Rules: []whv1.RuleWithOperations{ + { + Operations: []whv1.OperationType{whv1.Create, whv1.Update}, + Rule: whv1.Rule{ + APIGroups: []string{hypernodev1alpha1.SchemeGroupVersion.Group}, + APIVersions: []string{hypernodev1alpha1.SchemeGroupVersion.Version}, + Resources: []string{"hypernodes"}, + }, + }, + }, + }}, + }, +} + +// AdmitHyperNode is to admit hypernode and return response. +func AdmitHyperNode(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { + klog.V(3).Infof("admitting hypernode -- %s", ar.Request.Operation) + + hypernode, err := schema.DecodeHyperNode(ar.Request.Object, ar.Request.Resource) + if err != nil { + return util.ToAdmissionResponse(err) + } + + switch ar.Request.Operation { + case admissionv1.Create, admissionv1.Update: + err = validateHyperNode(hypernode) + if err != nil { + return util.ToAdmissionResponse(err) + } + } + + return &admissionv1.AdmissionResponse{ + Allowed: true, + } +} + +// validateHyperNode is to validate hypernode. +func validateHyperNode(hypernode *hypernodev1alpha1.HyperNode) error { + if len(hypernode.Spec.Members) == 0 { + return nil + } + + for _, member := range hypernode.Spec.Members { + if member.Selector.Type == "" { + continue + } + + if member.Selector.Type == hypernodev1alpha1.ExactMatchMemberSelectorType && member.Selector.ExactMatch == nil { + return fmt.Errorf("exactMatch is required when type is Exact") + } + if member.Selector.Type == hypernodev1alpha1.RegexMatchMemberSelectorType && member.Selector.RegexMatch == nil { + return fmt.Errorf("regexMatch is required when type is Regex") + } + } + return nil +} diff --git a/pkg/webhooks/admission/hypernodes/validate/admit_hypernode_test.go b/pkg/webhooks/admission/hypernodes/validate/admit_hypernode_test.go new file mode 100644 index 0000000000..10ca56fea3 --- /dev/null +++ b/pkg/webhooks/admission/hypernodes/validate/admit_hypernode_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 The Volcano 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 validate + +import ( + "testing" + + hypernodev1alpha1 "volcano.sh/apis/pkg/apis/topology/v1alpha1" +) + +func TestValidateHyperNode(t *testing.T) { + testCases := []struct { + Name string + HyperNode hypernodev1alpha1.HyperNode + ExpectErr bool + }{ + { + Name: "validate valid hypernode", + HyperNode: hypernodev1alpha1.HyperNode{ + Spec: hypernodev1alpha1.HyperNodeSpec{ + Members: []hypernodev1alpha1.MemberSpec{ + { + Selector: hypernodev1alpha1.MemberSelector{ + Type: hypernodev1alpha1.ExactMatchMemberSelectorType, + ExactMatch: &hypernodev1alpha1.ExactMatch{Name: "node-1"}, + }, + }, + }, + }, + }, + ExpectErr: false, + }, + { + Name: "validate invalid hypernode with empty exactMatch", + HyperNode: hypernodev1alpha1.HyperNode{ + Spec: hypernodev1alpha1.HyperNodeSpec{ + Members: []hypernodev1alpha1.MemberSpec{ + { + Selector: hypernodev1alpha1.MemberSelector{ + Type: hypernodev1alpha1.ExactMatchMemberSelectorType, + ExactMatch: nil, + }, + }, + }, + }, + }, + ExpectErr: true, + }, + { + Name: "validate invalid hypernode with empty regexMatch", + HyperNode: hypernodev1alpha1.HyperNode{ + Spec: hypernodev1alpha1.HyperNodeSpec{ + Members: []hypernodev1alpha1.MemberSpec{ + { + Selector: hypernodev1alpha1.MemberSelector{ + Type: hypernodev1alpha1.RegexMatchMemberSelectorType, + RegexMatch: nil, + }, + }, + }, + }, + }, + ExpectErr: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + err := validateHyperNode(&testCase.HyperNode) + if err != nil && !testCase.ExpectErr { + t.Errorf("validateHyperNode failed: %v", err) + } + }) + } +} diff --git a/pkg/webhooks/schema/schema.go b/pkg/webhooks/schema/schema.go index 8c1427d74c..47e8d05cdc 100644 --- a/pkg/webhooks/schema/schema.go +++ b/pkg/webhooks/schema/schema.go @@ -30,6 +30,7 @@ import ( batchv1alpha1 "volcano.sh/apis/pkg/apis/batch/v1alpha1" schedulingv1beta1 "volcano.sh/apis/pkg/apis/scheduling/v1beta1" + hypernodev1alpha1 "volcano.sh/apis/pkg/apis/topology/v1alpha1" ) func init() { @@ -127,3 +128,24 @@ func DecodePodGroup(object runtime.RawExtension, resource metav1.GroupVersionRes return &podgroup, nil } + +// DecodeHyperNode decodes the hypernode using deserializer from the raw object. +func DecodeHyperNode(object runtime.RawExtension, resource metav1.GroupVersionResource) (*hypernodev1alpha1.HyperNode, error) { + hypernodeResource := metav1.GroupVersionResource{ + Group: hypernodev1alpha1.SchemeGroupVersion.Group, + Version: hypernodev1alpha1.SchemeGroupVersion.Version, + Resource: "hypernodes", + } + + if resource != hypernodeResource { + klog.Errorf("expect resource to be %s", hypernodeResource) + return nil, fmt.Errorf("expect resource to be %s", hypernodeResource) + } + + hypernode := hypernodev1alpha1.HyperNode{} + if _, _, err := Codecs.UniversalDeserializer().Decode(object.Raw, nil, &hypernode); err != nil { + return nil, err + } + + return &hypernode, nil +}