From f52a3326f91aa7653208a7f89820c271e2f46019 Mon Sep 17 00:00:00 2001 From: anirudhprasad-sap <126493692+anirudhprasad-sap@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:24:01 +0200 Subject: [PATCH] [Misc] Webhook workload name check and label check on CAPTenantOutput (#123) --- cmd/web-hooks/internal/handler/handler.go | 55 +++++++++++ .../internal/handler/handler_test.go | 92 +++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/cmd/web-hooks/internal/handler/handler.go b/cmd/web-hooks/internal/handler/handler.go index 5bf033a..58984e0 100644 --- a/cmd/web-hooks/internal/handler/handler.go +++ b/cmd/web-hooks/internal/handler/handler.go @@ -12,6 +12,7 @@ import ( "io" "net/http" "os" + "regexp" "strconv" "github.com/google/go-cmp/cmp" @@ -21,6 +22,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/klog/v2" @@ -28,6 +30,7 @@ import ( const ( LabelTenantType = "sme.sap.com/tenant-type" + LabelTenantId = "sme.sap.com/btp-tenant-id" ProviderTenantType = "provider" SideCarEnv = "WEBHOOK_SIDE_CAR" AdmissionError = "admission error:" @@ -68,6 +71,12 @@ type ResponseCat struct { Kind string `json:"kind"` } +type ResponseCtout struct { + Metadata `json:"metadata"` + Spec *v1alpha1.CAPTenantOutputSpec `json:"spec"` + Kind string `json:"kind"` +} + type ResponseCav struct { Metadata `json:"metadata"` Spec *v1alpha1.CAPApplicationVersionSpec `json:"spec"` @@ -257,11 +266,21 @@ func checkWorkloadContentJob(cavObjNew *ResponseCav) validateResource { } func validateWorkloads(cavObjNew *ResponseCav) validateResource { + // regex pattern for workload name - based on RFC 1123 label + regex, _ := regexp.Compile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) + // Check: Workload name should be unique // Only one workload deployment of type CAP, router and content is allowed uniqueWorkloadNameCountMap := make(map[string]int) for _, workload := range cavObjNew.Spec.Workloads { + if !regex.MatchString(workload.Name) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s Invalid workload name: %s", InvalidationMessage, cavObjNew.Kind, workload.Name), + } + } + if workloadTypeValidate := checkWorkloadType(&workload); !workloadTypeValidate.allowed { return workloadTypeValidate } @@ -522,6 +541,38 @@ func (wh *WebhookHandler) validateCAPTenant(w http.ResponseWriter, admissionRevi return validAdmissionReviewObj() } +func (wh *WebhookHandler) validateCAPTenantOutput(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview) validateResource { + ctoutObjNew := ResponseCtout{} + + if admissionReview.Request.Operation == admissionv1.Delete { + return validAdmissionReviewObj() + } + + if validatedResource := unmarshalRawObj(w, admissionReview.Request.Object.Raw, &ctoutObjNew, v1alpha1.CAPTenantOutputKind); !validatedResource.allowed { + return validatedResource + } + + if _, exists := ctoutObjNew.Labels[LabelTenantId]; !exists { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s label %s missing on CAP tenant output %s", InvalidationMessage, v1alpha1.CAPTenantOutputKind, LabelTenantId, ctoutObjNew.Name), + } + } else { + labelSelector, _ := labels.ValidatedSelectorFromSet(map[string]string{ + LabelTenantId: ctoutObjNew.Labels[LabelTenantId], + }) + ctList, err := wh.CrdClient.SmeV1alpha1().CAPTenants(ctoutObjNew.Namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) + if err != nil || len(ctList.Items) == 0 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s label %s on CAP tenant output %s does not contain a valid tenant ID", InvalidationMessage, v1alpha1.CAPTenantOutputKind, LabelTenantId, ctoutObjNew.Name), + } + } + } + + return validAdmissionReviewObj() +} + func (wh *WebhookHandler) validateCAPApplication(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview) validateResource { caObjOld := ResponseCa{} caObjNew := ResponseCa{} @@ -592,6 +643,10 @@ func (wh *WebhookHandler) Validate(w http.ResponseWriter, r *http.Request) { if validation = wh.validateCAPApplication(w, admissionReview); validation.errorOccured { return } + case v1alpha1.CAPTenantOutputKind: + if validation = wh.validateCAPTenantOutput(w, admissionReview); validation.errorOccured { + return + } } // prepare response diff --git a/cmd/web-hooks/internal/handler/handler_test.go b/cmd/web-hooks/internal/handler/handler_test.go index 96e5ee2..7041ff7 100644 --- a/cmd/web-hooks/internal/handler/handler_test.go +++ b/cmd/web-hooks/internal/handler/handler_test.go @@ -782,6 +782,7 @@ func TestCavInvalidity(t *testing.T) { multipleContentJobsWithNoOrder bool missingContentJobinContentJobs bool invalidJobinContentJobs bool + invalidWorkloadName bool backlogItems []string }{ { @@ -874,6 +875,11 @@ func TestCavInvalidity(t *testing.T) { invalidJobinContentJobs: true, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-4351"}, }, + { + operation: admissionv1.Create, + invalidWorkloadName: true, + backlogItems: []string{}, + }, } for _, test := range tests { nameParts := []string{"Testing CAPApplicationversion invalidity for operation " + string(test.operation) + "; "} @@ -1145,6 +1151,8 @@ func TestCavInvalidity(t *testing.T) { }, }) crd.Spec.ContentJobs = append(crd.Spec.ContentJobs, "content", "content-2", "dummy") + } else if test.invalidWorkloadName == true { + crd.Spec.Workloads[0].Name = "WrongWorkloadName" } rawBytes, _ := json.Marshal(crd) @@ -1202,6 +1210,8 @@ func TestCavInvalidity(t *testing.T) { errorMessage = fmt.Sprintf("%s %s content job content-2 is not specified as part of ContentJobs", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) } else if test.invalidJobinContentJobs == true { errorMessage = fmt.Sprintf("%s %s job dummy specified as part of ContentJobs is not a valid content job", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.invalidWorkloadName == true { + errorMessage = fmt.Sprintf("%s %s Invalid workload name: %s", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, "WrongWorkloadName") } if admissionReviewRes.Response.Allowed || admissionReviewRes.Response.Result.Message != errorMessage { @@ -1210,3 +1220,85 @@ func TestCavInvalidity(t *testing.T) { }) } } + +func TestCtoutInvalidity(t *testing.T) { + tests := []struct { + operation admissionv1.Operation + labelPresent bool + }{ + { + operation: admissionv1.Create, + labelPresent: true, + }, + { + operation: admissionv1.Create, + labelPresent: false, + }, + { + operation: admissionv1.Update, + labelPresent: true, + }, + { + operation: admissionv1.Update, + labelPresent: false, + }, + } + for _, test := range tests { + t.Run("Testing CAPTenantOutput invalidity for operation "+string(test.operation), func(t *testing.T) { + var crdObjects []runtime.Object + + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(crdObjects...), + } + + ctout := &ResponseCtout{ + Metadata: Metadata{ + Name: "some-ctout", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{}, + }, + Spec: &v1alpha1.CAPTenantOutputSpec{ + SubscriptionCallbackData: `{"supportUsers":[{"name":"user_t1", "email":"usert1@foo.com"},{"name":"user_t2", "email":"usert2@foo.com"}]}`, + }, + Kind: v1alpha1.CAPApplicationVersionKind, + } + + if test.labelPresent { + ctout.Labels[LabelTenantId] = "some-tenant-id" + } + + admissionReview, err := createAdmissionRequest(test.operation, v1alpha1.CAPTenantOutputKind, ctout.Name, noUpdate) + if err != nil { + t.Fatal("admission review error") + } + + rawBytes, _ := json.Marshal(ctout) + admissionReview.Request.Object.Raw = rawBytes + bytesRequest, err := json.Marshal(admissionReview) + if err != nil { + t.Fatal("marshal error") + } + request := httptest.NewRequest(http.MethodGet, "/validate", bytes.NewBuffer(bytesRequest)) + recorder := httptest.NewRecorder() + + wh.Validate(recorder, request) + + admissionReviewRes := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + universalDeserializer.Decode(bytes, nil, &admissionReviewRes) + + if test.labelPresent { + if admissionReviewRes.Response.Allowed || admissionReviewRes.Response.Result.Message != fmt.Sprintf("%s %s label %s on CAP tenant output %s does not contain a valid tenant ID", InvalidationMessage, v1alpha1.CAPTenantOutputKind, LabelTenantId, "some-ctout") { + t.Fatal("validation response error") + } + } else { + if admissionReviewRes.Response.Allowed || admissionReviewRes.Response.Result.Message != fmt.Sprintf("%s %s label %s missing on CAP tenant output %s", InvalidationMessage, v1alpha1.CAPTenantOutputKind, LabelTenantId, "some-ctout") { + t.Fatal("validation response error") + } + } + }) + } +}