Skip to content

Commit

Permalink
Validating webhook implementation (#274)
Browse files Browse the repository at this point in the history
validating webhook implementation
Signed-off-by: ichbinblau <[email protected]>
  • Loading branch information
ichbinblau authored Aug 16, 2024
1 parent 488a1ca commit df5f6f3
Show file tree
Hide file tree
Showing 18 changed files with 1,283 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pr-gmc-helm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,5 @@ jobs:
kubectl delete namespace "$SYSTEM_NAMESPACE"
fi
kubectl delete crd gmconnectors.gmc.opea.io
kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io ${RELEASE_NAME}-controller --ignore-not-found
fi
1 change: 1 addition & 0 deletions .github/workflows/scripts/e2e/gmc_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function cleanup_gmc() {
echo "Deleting namespace: $SYSTEM_NAMESPACE"
kubectl delete namespace "$SYSTEM_NAMESPACE"
kubectl delete crd gmconnectors.gmc.opea.io || true
kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io validating-webhook-configuration --ignore-not-found
else
echo "Namespace $SYSTEM_NAMESPACE does not exist"
fi
Expand Down
53 changes: 52 additions & 1 deletion .github/workflows/scripts/e2e/gmc_xeon_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ CHATQNA_SWITCH_NAMESPACE="${APP_NAMESPACE}-chatqna-switch"
CODEGEN_NAMESPACE="${APP_NAMESPACE}-codegen"
CODETRANS_NAMESPACE="${APP_NAMESPACE}-codetrans"
DOCSUM_NAMESPACE="${APP_NAMESPACE}-docsum"
WEBHOOK_NAMESPACE="${APP_NAMESPACE}-webhook"

function validate_gmc() {
echo "validate audio-qna"
Expand All @@ -39,13 +40,63 @@ function validate_gmc() {
# echo "validate docsum"
# validate_docsum

echo "validate webhook"
validate_webhook

get_gmc_controller_logs
}

function validate_webhook() {
kubectl create ns $WEBHOOK_NAMESPACE || echo "namespace $WEBHOOK_NAMESPACE is created."
# validate root node existence
yq ".spec.nodes.node123 = .spec.nodes.root | del(.spec.nodes.root)" config/samples/chatQnA_xeon.yaml > /tmp/webhook-case1.yaml
sed -i "s|namespace: chatqa|namespace: $WEBHOOK_NAMESPACE|g" /tmp/webhook-case1.yaml
output=$(! kubectl apply -f /tmp/webhook-case1.yaml 2>&1)
if ! (echo $output | grep -q "a root node is required"); then
echo "Root node existence validation error message is not found!"
echo $output
exit 1
fi

# StepName validation
yq '(.spec.nodes.root.steps[] | select ( .name == "Llm")).name = "xyz"' config/samples/chatQnA_gaudi.yaml > /tmp/webhook-case2.yaml
sed -i "s|namespace: chatqa|namespace: $WEBHOOK_NAMESPACE|g" /tmp/webhook-case2.yaml
output=$(! kubectl apply -f /tmp/webhook-case2.yaml 2>&1)
if ! (echo $output | grep -q "invalid step name"); then
echo "Step name validation error message is not found!"
echo $output
exit 1
fi


# nodeName existence
yq '(.spec.nodes.root.steps[] | select ( .name == "Embedding")).nodeName = "node123"' config/samples/chatQnA_switch_xeon.yaml > /tmp/webhook-case3.yaml
sed -i "s|namespace: switch|namespace: $WEBHOOK_NAMESPACE|g" /tmp/webhook-case3.yaml
output=$(! kubectl apply -f /tmp/webhook-case3.yaml 2>&1)
if ! (echo $output | grep -q "node name: node123 in step Embedding does not exist"); then
echo "nodeName existence validation error message is not found!"
echo $output
exit 1
fi

# serviceName uniqueness
yq '(.spec.nodes.node1.steps[] | select ( .name == "Embedding")).internalService.serviceName = "tei-embedding-svc-bge15"' config/samples/chatQnA_switch_xeon.yaml > /tmp/webhook-case4.yaml
sed -i "s|namespace: switch|namespace: $WEBHOOK_NAMESPACE|g" /tmp/webhook-case4.yaml
output=$(! kubectl apply -f /tmp/webhook-case4.yaml 2>&1)
if ! (echo $output | grep -q "service name: tei-embedding-svc-bge15 in node node1 already exists"); then
echo "serviceName uniqueness validation error message is not found!"
echo $output
exit 1
fi

# clean up cases
rm -f /tmp/webhook-case*.yaml
}

function cleanup_apps() {
echo "clean up microservice-connector"
# namespaces=("$CHATQNA_NAMESPACE" "$CHATQNA_DATAPREP_NAMESPACE" "$CHATQNA_SWITCH_NAMESPACE" "$CODEGEN_NAMESPACE" "$CODETRANS_NAMESPACE" "$DOCSUM_NAMESPACE")
namespaces=("$AUDIOQA_NAMESPACE" "$CHATQNA_NAMESPACE" "$CHATQNA_DATAPREP_NAMESPACE" "$CHATQNA_SWITCH_NAMESPACE")
namespaces=("$AUDIOQA_NAMESPACE" "$CHATQNA_NAMESPACE" "$CHATQNA_DATAPREP_NAMESPACE" "$CHATQNA_SWITCH_NAMESPACE" "$WEBHOOK_NAMESPACE")
for ns in "${namespaces[@]}"; do
if kubectl get namespace $ns > /dev/null 2>&1; then
echo "Deleting namespace: $ns"
Expand Down
178 changes: 178 additions & 0 deletions microservices-connector/api/v1alpha3/validating_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright (C) 2024 Intel Corporation
* SPDX-License-Identifier: Apache-2.0
*/

package v1alpha3

import (
"fmt"
"slices"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// +kubebuilder:docs-gen:collapse=Go imports

var (
//setup a logger for the webhooks.
vlog = logf.Log.WithName("validating-webhook")
stepNames = []string{
"TeiEmbedding",
"TeiEmbeddingGaudi",
"Embedding",
"VectorDB",
"Retriever",
"Reranking",
"TeiReranking",
"Tgi",
"TgiGaudi",
"Llm",
"DocSum",
"Router",
"WebRetriever",
"Asr",
"Tts",
"SpeechT5",
"SpeechT5Gaudi",
"Whisper",
"WhisperGaudi",
"DataPrep",
}
)

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *GMConnector) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// +kubebuilder:webhook:verbs=create;update,path=/validate-gmc-opea-io-v1alpha3-gmconnector,mutating=false,failurePolicy=fail,groups=gmc.opea.io,resources=gmconnectors,versions=v1alpha3,name=vgmcconnector.gmc.opea.io,sideEffects=None,admissionReviewVersions=v1

var _ webhook.Validator = &GMConnector{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *GMConnector) ValidateCreate() (admission.Warnings, error) {
vlog.Info("validate create", "name", r.Name)

return nil, r.validateGMConnector()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *GMConnector) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
vlog.Info("validate update", "name", r.Name)

return nil, r.validateGMConnector()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *GMConnector) ValidateDelete() (admission.Warnings, error) {
vlog.Info("validate delete", "name", r.Name)
return nil, nil
}

/*
validate the name and the spec of the GMConnector.
*/
func (r *GMConnector) validateGMConnector() error {
if err := r.checkfields(); err != nil {
return apierrors.NewInvalid(
schema.GroupKind{Group: GroupVersion.Group, Kind: "GMCConnector"},
r.Name, err)
}

return nil

}

func (r *GMConnector) checkfields() field.ErrorList {
// The field helpers from the kubernetes API machinery help us return nicely
// structured validation errors.
var allErrs field.ErrorList
if errs := validateNames(r.Spec.Nodes, field.NewPath("spec").Child("nodes")); len(errs) > 0 {
allErrs = errs
}
if err := validateRootExistance(r.Spec.Nodes, field.NewPath("spec").Child("nodes")); err != nil {
allErrs = append(allErrs, err)
}

if len(allErrs) == 0 {
return nil
}
return allErrs
}

func checkStepName(s Step, fldRoot *field.Path, nodeName string) *field.Error {
if len(s.StepName) == 0 {
return field.Invalid(fldRoot.Child(nodeName).Child("stepName"), s, fmt.Sprintf("the step name for node %v cannot be empty", nodeName))
}
if !slices.Contains(stepNames, s.StepName) {
return field.Invalid(fldRoot.Child(nodeName).Child("stepName"), s, fmt.Sprintf("invalid step name: %s for node %v", s.StepName, nodeName))
}
return nil
}

func nodeNameExists(name string, nodes []string) bool {
// node name is not set, skip check
if len(name) == 0 {
return true
}
return slices.Contains(nodes, name)
}

func getKeys(m map[string]Router) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}

// validate step name and node name
func validateNames(nodes map[string]Router, fldPath *field.Path) field.ErrorList {
nodeNames := getKeys(nodes)
serviceNames := []string{}
var errs field.ErrorList

for name, router := range nodes {
for _, step := range router.Steps {
// validate step name
if err := checkStepName(step, fldPath, name); err != nil {
errs = append(errs, err)
}

// check node name has been defined in the spec
if !nodeNameExists(step.NodeName, nodeNames) {
errs = append(errs, field.Invalid(fldPath.Child(name).Child("nodeName"), step, fmt.Sprintf("node name: %v in step %v does not exist", step.NodeName, step.StepName)))
}

// check service name uniqueness
if len(step.InternalService.ServiceName) != 0 && slices.Contains(serviceNames, step.InternalService.ServiceName) {
errs = append(errs, field.Invalid(fldPath.Child(name).Child("internalService").Child("serviceName"),
step,
fmt.Sprintf("service name: %v in node %v already exists", step.InternalService.ServiceName, name)))
} else {
serviceNames = append(serviceNames, step.InternalService.ServiceName)
}
}
}
return errs
}

// check root node exists
func validateRootExistance(nodes map[string]Router, fldPath *field.Path) *field.Error {
if _, ok := nodes["root"]; !ok {
return field.Invalid(fldPath, nodes, "a root node is required")
}
return nil
}

// +kubebuilder:docs-gen:collapse=Existing Validation
Loading

0 comments on commit df5f6f3

Please sign in to comment.