diff --git a/images/webhooks/src/main.go b/images/webhooks/src/main.go index 943d9e1f..1d9ecc39 100644 --- a/images/webhooks/src/main.go +++ b/images/webhooks/src/main.go @@ -108,6 +108,7 @@ func main() { mux := http.NewServeMux() mux.Handle("/pod-scheduler-mutation", pscmWhHandler) mux.Handle("/storage-class-update", scuWhHandler) + http.HandleFunc("/sc-validate", validators.ValidateSCOperation) mux.HandleFunc("/healthz", httpHandlerHealthz) logger.Infof("Listening on %s", port) diff --git a/images/webhooks/src/validators/scOperationsValidator.go b/images/webhooks/src/validators/scOperationsValidator.go new file mode 100644 index 00000000..a79d6d1b --- /dev/null +++ b/images/webhooks/src/validators/scOperationsValidator.go @@ -0,0 +1,63 @@ +package validators + +import ( + "encoding/json" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "net/http" +) + +const ( + localCSIProvisioner = "local.csi.storage.deckhouse.io" +) + +func ValidateSCOperation(w http.ResponseWriter, r *http.Request) { + arReview := v1beta1.AdmissionReview{} + if err := json.NewDecoder(r.Body).Decode(&arReview); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } else if arReview.Request == nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + raw := arReview.Request.Object.Raw + + arReview.Response = &v1beta1.AdmissionResponse{ + UID: arReview.Request.UID, + Allowed: true, + } + + var jsonData map[string]interface{} + err := json.Unmarshal(raw, &jsonData) + if err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + if jsonData["provisioner"] == localCSIProvisioner { + if arReview.Request.UserInfo.Username == "system:serviceaccount:d8-sds-local-volume:sds-local-volume-controller" { + arReview.Response.Allowed = true + klog.Infof("Incoming request approved (%s)", string(raw)) + } else if arReview.Request.Operation == "DELETE" { + arReview.Response.Allowed = true + klog.Infof("Incoming request approved (%s)", string(raw)) + } else { + arReview.Response.Allowed = false + arReview.Response.Result = &metav1.Status{ + Message: "Manual operations with this StorageClass is prohibited. Please use LocalStorageClass instead.", + } + klog.Infof("Incoming request denied: Manual operations with this StorageClass is prohibited. Please use LocalStorageClass instead (%s)", string(raw)) + } + } else { + arReview.Response.Allowed = true + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(&arReview) + if err != nil { + http.Error(w, "invalid response body", http.StatusInternalServerError) + return + } +} diff --git a/templates/webhooks/webhook.yaml b/templates/webhooks/webhook.yaml index 17e7e1d9..279bd601 100644 --- a/templates/webhooks/webhook.yaml +++ b/templates/webhooks/webhook.yaml @@ -55,3 +55,26 @@ webhooks: admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None timeoutSeconds: 5 +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: "d8-{{ .Chart.Name }}-sc-validation" +webhooks: + - name: "d8-{{ .Chart.Name }}-sc-validation.storage.deckhouse.io" + rules: + - apiGroups: ["storage.k8s.io"] + apiVersions: ["v1"] + operations: ["*"] + resources: ["storageclasses"] + scope: "Cluster" + clientConfig: + service: + namespace: "d8-{{ .Chart.Name }}" + name: "webhooks" + path: "/sc-validate" + caBundle: | + {{ .Values.sdsLocalVolume.internal.customWebhookCert.ca }} + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + timeoutSeconds: 5 \ No newline at end of file