Skip to content

Commit

Permalink
[Misc] Webhook workload name check and label check on CAPTenantOutput (
Browse files Browse the repository at this point in the history
  • Loading branch information
anirudhprasad-sap authored Sep 6, 2024
1 parent c84180b commit f52a332
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 0 deletions.
55 changes: 55 additions & 0 deletions cmd/web-hooks/internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"net/http"
"os"
"regexp"
"strconv"

"github.com/google/go-cmp/cmp"
Expand All @@ -21,13 +22,15 @@ 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"
)

const (
LabelTenantType = "sme.sap.com/tenant-type"
LabelTenantId = "sme.sap.com/btp-tenant-id"
ProviderTenantType = "provider"
SideCarEnv = "WEBHOOK_SIDE_CAR"
AdmissionError = "admission error:"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions cmd/web-hooks/internal/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ func TestCavInvalidity(t *testing.T) {
multipleContentJobsWithNoOrder bool
missingContentJobinContentJobs bool
invalidJobinContentJobs bool
invalidWorkloadName bool
backlogItems []string
}{
{
Expand Down Expand Up @@ -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) + "; "}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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":"[email protected]"},{"name":"user_t2", "email":"[email protected]"}]}`,
},
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")
}
}
})
}
}

0 comments on commit f52a332

Please sign in to comment.