Repository: flant/shell-operator
Pull request #439 (2022-11-16 12:09:13)

feat: Implement Mutating Webhooks
<!--
Thank you for sending a pull request! Here some tips for contributors:

1. Fill the description template below.
2. Include appropriate tests (if necessary). Make sure that all CI checks passed.
3. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.
-->

#### Overview

The most basic types of automation Kubernetes admins are interested in are validating and mutating webhooks. Currently shell-operator only supports validating webhooks. Because validating and mutating webhooks are nearly identical except for the patch field, the existing validating webhook manager can be expanded to also support mutating webhooks.

#### What this PR does / why we need it

<!--
- Please state in detail why we need this PR and what it solves.
- If your PR closes some of the existing issues, please add links to them here.
  Mentioned issues will be automatically closed.
  Usage: "Closes #<issue number>", or "Closes (paste link of issue)"
-->

Closes https://github.com/flant/shell-operator/issues/325

#### Special notes for your reviewer

Status:
- support for mutating webhooks has been added to the webhook manager.
- The validating package and types have been renamed to 'admission' to reflect their more general usage.
- Remainder of the codebase has been updated with the new package names and types.

TODO: 
- hook controller fixup
- 'activate' mutating webhook configs and add test coverage for mutating webhooks. 
- end-to-end testing in my own k8s environment
- documentation

#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note

```

chzar: still a lot of work to be done to properly route events. Just tracing the code paths and making changes accordingly.
chzar: Ok it should be working at this point - I can trigger the webhook by calling the http api manually - then shell-operator crashes when trying do a GET request to the k8s api after running the test script.

I think this is expected behaviour at this point because I dont think this functionality can be tested outside of a cluster. Would like to hear from other contributors about this.
diafour: Hello! Great work!

I've copied example/204-validating-webhook and created a hook with `kubernetesMutating` configuration in #443. And it works!

The hook is simple, it mutates a field in the CronTab custom resource:
```
  PATCH=$( echo '[{"op": "add", "path": "/spec/replicas", "value": 333 }]' | base64 )
    cat <<EOF > $VALIDATING_RESPONSE_PATH
{"allowed":true, "patch": "$PATCH"}
EOF
```

There is an important addition to make it actualy work: "copy Patch from the hook response to the AdmissionResponse": https://github.com/flant/shell-operator/commit/b5e7272020f91454fdb58038eb9ea64e353975a2. You can cherry-pick it to your branch.

There are some minor issues:

1. An `event` label in logs is still "kubernetesValidating":

```
{"level":"info","msg":"Got AdmissionReview request for confId='hooks' webhookId='change-crontab-resources'","time":"2022-12-08T06:34:01Z"}
{"binding":"kubernetesValidating","event.id":"63149ed4-c66c-439e-bd75-e9e613248603","level":"debug","msg":"Handle 'kubernetesValidating' event 'hooks' 'change-crontab-resources'","time":"2022-12-08T06:34:01Z"}
{"binding":"change.crontab.resources","event":"kubernetesValidating","hook":"mutating.sh","level":"info","msg":"Execute hook","queue":"","task":"HookRun","time":"2022-12-08T06:34:01Z"}
{"binding":"change.crontab.resources","event":"kubernetesValidating","hook":"mutating.sh","level":"debug","msg":"snapshot info: no kubernetes bindings for hook","queue":"","task":"HookRun","time":"2022-12-08T06:34:01Z"}
{"binding":"change.crontab.resources","event":"kubernetesValidating","hook":"mutating.sh","level":"debug","msg":"Executing command '/hooks/mutating.sh' in '/hooks' dir","queue":"","task":"HookRun","time":"2022-12-08T06:34:01Z"}
```

2. No `__on_mutating` handler in the shell framework https://github.com/flant/shell-operator/blob/main/frameworks/shell/hook.sh#L54-L59 

3. There is an extra field in MutatingWebhookConfiguration: `reinvocationPolicy`. https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#mutatingwebhook-v1-admissionregistration-k8s-io (a link to all fields, there is no direct link)
It ok to add it in a separate PR.

chzar: @diafour
 
Thanks Ivan!

I have been a bit short on time lately and was struggling to get minikube and hyperv networking to play nice so I could do some testing. I will try and address your comments push some code as soon as I can!
chzar: I have set this PR as ready to review. There is still some additional needs for tests/docs, but I can take that in a follow up.

Patch:
diff --git a/cmd/shell-operator/main.go b/cmd/shell-operator/main.go
index 319705f..c48d48c 100644
--- a/cmd/shell-operator/main.go
+++ b/cmd/shell-operator/main.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/flant/kube-client/klogtologrus"
+	"github.com/flant/shell-operator/pkg/jq"
 	"gopkg.in/alecthomas/kingpin.v2"
 
 	"github.com/flant/shell-operator/pkg/app"
@@ -30,6 +31,7 @@ func main() {
 	// print version
 	kpApp.Command("version", "Show version.").Action(func(c *kingpin.ParseContext) error {
 		fmt.Printf("%s %s\n", app.AppName, app.Version)
+		fmt.Println(jq.JqFilterInfo())
 		return nil
 	})
 
diff --git a/pkg/app/webhook.go b/pkg/app/webhook.go
index 4ca81a3..250d08e 100644
--- a/pkg/app/webhook.go
+++ b/pkg/app/webhook.go
@@ -1,13 +1,13 @@
 package app
 
 import (
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
 	"github.com/flant/shell-operator/pkg/webhook/server"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 	"gopkg.in/alecthomas/kingpin.v2"
 )
 
-var ValidatingWebhookSettings = &validating.WebhookSettings{
+var ValidatingWebhookSettings = &admission.WebhookSettings{
 	Settings: server.Settings{
 		ServerCertPath: "/validating-certs/tls.crt",
 		ServerKeyPath:  "/validating-certs/tls.key",
diff --git a/pkg/hook/config/config.go b/pkg/hook/config/config.go
index befb14e..f42bac2 100644
--- a/pkg/hook/config/config.go
+++ b/pkg/hook/config/config.go
@@ -8,6 +8,8 @@ import (
 	. "github.com/flant/shell-operator/pkg/hook/types"
 )
 
+var validBindingTypes = []BindingType{OnStartup, Schedule, OnKubernetesEvent, KubernetesValidating, KubernetesMutating, KubernetesConversion}
+
 // HookConfig is a structure with versioned hook configuration
 type HookConfig struct {
 	// effective version of config
@@ -22,6 +24,7 @@ type HookConfig struct {
 	Schedules            []ScheduleConfig
 	OnKubernetesEvents   []OnKubernetesEventConfig
 	KubernetesValidating []ValidatingConfig
+	KubernetesMutating   []MutatingConfig
 	KubernetesConversion []ConversionConfig
 	Settings             *Settings
 }
@@ -92,7 +95,7 @@ func (c *HookConfig) ConvertAndCheck(data []byte) error {
 func (c *HookConfig) Bindings() []BindingType {
 	res := []BindingType{}
 
-	for _, binding := range []BindingType{OnStartup, Schedule, OnKubernetesEvent, KubernetesValidating, KubernetesConversion} {
+	for _, binding := range validBindingTypes {
 		if c.HasBinding(binding) {
 			res = append(res, binding)
 		}
@@ -112,6 +115,8 @@ func (c *HookConfig) HasBinding(binding BindingType) bool {
 		return len(c.OnKubernetesEvents) > 0
 	case KubernetesValidating:
 		return len(c.KubernetesValidating) > 0
+	case KubernetesMutating:
+		return len(c.KubernetesMutating) > 0
 	case KubernetesConversion:
 		return len(c.KubernetesConversion) > 0
 	}
diff --git a/pkg/hook/config/config_v1.go b/pkg/hook/config/config_v1.go
index 2e1a031..7ab1f60 100644
--- a/pkg/hook/config/config_v1.go
+++ b/pkg/hook/config/config_v1.go
@@ -16,8 +16,8 @@ import (
 	. "github.com/flant/shell-operator/pkg/schedule_manager/types"
 
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 	"github.com/flant/shell-operator/pkg/webhook/validating/validation"
 )
 
@@ -26,7 +26,8 @@ type HookConfigV1 struct {
 	OnStartup            interface{}                    `json:"onStartup"`
 	Schedule             []ScheduleConfigV1             `json:"schedule"`
 	OnKubernetesEvent    []OnKubernetesEventConfigV1    `json:"kubernetes"`
-	KubernetesValidating []KubernetesValidatingConfigV1 `json:"kubernetesValidating"`
+	KubernetesValidating []KubernetesAdmissionConfigV1  `json:"kubernetesValidating"`
+	KubernetesMutating   []KubernetesAdmissionConfigV1  `json:"kubernetesMutating"`
 	KubernetesConversion []KubernetesConversionConfigV1 `json:"kubernetesCustomResourceConversion"`
 	Settings             *SettingsV1                    `json:"settings"`
 }
@@ -70,8 +71,8 @@ type KubeFieldSelectorV1 FieldSelector
 
 type KubeNamespaceSelectorV1 NamespaceSelector
 
-// version 1 of kubernetes event configuration
-type KubernetesValidatingConfigV1 struct {
+// version 1 of kubernetes vali configuration
+type KubernetesAdmissionConfigV1 struct {
 	Name                 string                   `json:"name,omitempty"`
 	IncludeSnapshotsFrom []string                 `json:"includeSnapshotsFrom,omitempty"`
 	Group                string                   `json:"group,omitempty"`
@@ -207,28 +208,42 @@ func (cv1 *HookConfigV1) ConvertAndCheck(c *HookConfig) (err error) {
 	// Validating webhooks
 	c.KubernetesValidating = []ValidatingConfig{}
 	for i, rawValidating := range c.V1.KubernetesValidating {
-		err := cv1.CheckValidating(c.OnKubernetesEvents, rawValidating)
+		err := cv1.CheckAdmission(c.OnKubernetesEvents, rawValidating)
 		if err != nil {
 			return fmt.Errorf("invalid kubernetesValidating config [%d]: %v", i, err)
 		}
-		validating, err := cv1.ConvertValidating(rawValidating)
+		validating, err := convertValidating(rawValidating)
 		if err != nil {
 			return err
 		}
 		c.KubernetesValidating = append(c.KubernetesValidating, validating)
 	}
-	// Validate webhooks
-	webhooks := []v1.ValidatingWebhook{}
+	// Validate validatingWebhooks
+	validatingWebhooks := []v1.ValidatingWebhook{}
 	for _, cfg := range c.KubernetesValidating {
-		webhooks = append(webhooks, *cfg.Webhook.ValidatingWebhook)
+		validatingWebhooks = append(validatingWebhooks, *cfg.Webhook.ValidatingWebhook)
 	}
 	err = validation.ValidateValidatingWebhooks(&v1.ValidatingWebhookConfiguration{
-		Webhooks: webhooks,
+		Webhooks: validatingWebhooks,
 	})
 	if err != nil {
 		return err
 	}
 
+	c.KubernetesMutating = []MutatingConfig{}
+	for i, rawMutating := range c.V1.KubernetesMutating {
+		err := cv1.CheckAdmission(c.OnKubernetesEvents, rawMutating)
+		if err != nil {
+			return fmt.Errorf("invalid kubernetesMutating config [%d]: %v", i, err)
+		}
+		mutating, err := convertMutating(rawMutating)
+		if err != nil {
+			return err
+		}
+		c.KubernetesMutating = append(c.KubernetesMutating, mutating)
+	}
+	// TODO: Validate mutatingWebhooks
+
 	// Conversion webhooks.
 	c.KubernetesConversion = []ConversionConfig{}
 	for i, rawConversion := range c.V1.KubernetesConversion {
@@ -372,7 +387,7 @@ func (сv1 *HookConfigV1) CheckOnKubernetesEvent(kubeCfg OnKubernetesEventConfig
 	return allErr
 }
 
-func (cv1 *HookConfigV1) CheckValidating(kubeConfigs []OnKubernetesEventConfig, cfgV1 KubernetesValidatingConfigV1) (allErr error) {
+func (cv1 *HookConfigV1) CheckAdmission(kubeConfigs []OnKubernetesEventConfig, cfgV1 KubernetesAdmissionConfigV1) (allErr error) {
 	var err error
 
 	if len(cfgV1.IncludeSnapshotsFrom) > 0 {
@@ -399,7 +414,7 @@ func (cv1 *HookConfigV1) CheckValidating(kubeConfigs []OnKubernetesEventConfig,
 	return allErr
 }
 
-func (cv1 *HookConfigV1) ConvertValidating(cfgV1 KubernetesValidatingConfigV1) (ValidatingConfig, error) {
+func convertValidating(cfgV1 KubernetesAdmissionConfigV1) (ValidatingConfig, error) {
 	cfg := ValidatingConfig{}
 
 	cfg.Group = cfgV1.Group
@@ -436,7 +451,7 @@ func (cv1 *HookConfigV1) ConvertValidating(cfgV1 KubernetesValidatingConfigV1) (
 		webhook.TimeoutSeconds = &DefaultTimeoutSeconds
 	}
 
-	cfg.Webhook = &validating.ValidatingWebhookConfig{
+	cfg.Webhook = &admission.ValidatingWebhookConfig{
 		ValidatingWebhook: webhook,
 	}
 	cfg.Webhook.Metadata.LogLabels = map[string]string{}
@@ -445,6 +460,52 @@ func (cv1 *HookConfigV1) ConvertValidating(cfgV1 KubernetesValidatingConfigV1) (
 	return cfg, nil
 }
 
+func convertMutating(cfgV1 KubernetesAdmissionConfigV1) (MutatingConfig, error) {
+	cfg := MutatingConfig{}
+
+	cfg.Group = cfgV1.Group
+	cfg.IncludeSnapshotsFrom = cfgV1.IncludeSnapshotsFrom
+	cfg.BindingName = cfgV1.Name
+
+	DefaultFailurePolicy := v1.Fail
+	DefaultSideEffects := v1.SideEffectClassNone
+	DefaultTimeoutSeconds := int32(10)
+
+	webhook := &v1.MutatingWebhook{
+		Name:  cfgV1.Name,
+		Rules: cfgV1.Rules,
+	}
+	if cfgV1.Namespace != nil {
+		webhook.NamespaceSelector = cfgV1.Namespace.LabelSelector
+	}
+	if cfgV1.LabelSelector != nil {
+		webhook.ObjectSelector = cfgV1.LabelSelector
+	}
+	if cfgV1.FailurePolicy != nil {
+		webhook.FailurePolicy = cfgV1.FailurePolicy
+	} else {
+		webhook.FailurePolicy = &DefaultFailurePolicy
+	}
+	if cfgV1.SideEffects != nil {
+		webhook.SideEffects = cfgV1.SideEffects
+	} else {
+		webhook.SideEffects = &DefaultSideEffects
+	}
+	if cfgV1.TimeoutSeconds != nil {
+		webhook.TimeoutSeconds = cfgV1.TimeoutSeconds
+	} else {
+		webhook.TimeoutSeconds = &DefaultTimeoutSeconds
+	}
+
+	cfg.Webhook = &admission.MutatingWebhookConfig{
+		MutatingWebhook: webhook,
+	}
+	cfg.Webhook.Metadata.LogLabels = map[string]string{}
+	cfg.Webhook.Metadata.MetricLabels = map[string]string{}
+
+	return cfg, nil
+}
+
 func (cv1 *HookConfigV1) CheckConversion(kubeConfigs []OnKubernetesEventConfig, cfgV1 KubernetesConversionConfigV1) (allErr error) {
 	var err error
 
diff --git a/pkg/hook/config/schemas.go b/pkg/hook/config/schemas.go
index 98243a8..3dae841 100644
--- a/pkg/hook/config/schemas.go
+++ b/pkg/hook/config/schemas.go
@@ -195,6 +195,93 @@ properties:
               "$ref": "#/definitions/nameSelector"
             labelSelector:
               "$ref": "#/definitions/labelSelector"
+  kubernetesMutating:
+    title: kubernetesMutatingConfiguration handlers
+    type: array
+    additionalItems: false
+    minItems: 1
+    items:
+      type: object
+      additionalProperties: false
+      required:
+      - name
+      properties:
+        name:
+          type: string
+        group:
+          type: string
+        includeSnapshotsFrom:
+          type: array
+          additionalItems: false
+          minItems: 1
+          items:
+            type: string
+        failurePolicy:
+          type: string
+          enum:
+          - Ignore
+          - Fail
+        sideEffects:
+          type: string
+          enum:
+          - None
+          - NoneOnDryRun
+        timeoutSeconds:
+          type: integer
+          example: 10
+        labelSelector:
+          "$ref": "#/definitions/labelSelector"
+        namespace:
+          type: object
+          additionalProperties: false
+          required:
+          - labelSelector
+          properties:
+            labelSelector:
+              "$ref": "#/definitions/labelSelector"
+        rules:
+          type: array
+          additionalItems: false
+          minItems: 1
+          items:
+            type: object
+            additionalProperties: false
+            required:
+              - apiVersions
+              - apiGroups
+              - resources
+              - operations
+            properties:
+              apiVersions:
+                type: array
+                minItems: 1
+                items:
+                  type: string
+              apiGroups:
+                type: array
+                minItems: 1
+                items:
+                  type: string
+              resources:
+                type: array
+                minItems: 1
+                items:
+                  type: string
+              operations:
+                type: array
+                minItems: 1
+                items:
+                  type: string
+                  enum:
+                  - "CREATE"
+                  - "UPDATE"
+                  - "*"
+              scope:
+                type: string
+                enum:
+                - "Cluster"
+                - "Namespaced"
+                - "*"
   kubernetesValidating:
     title: ValidatingWebhookConfiguration handlers
     type: array
diff --git a/pkg/hook/controller/admission_bindings_controller.go b/pkg/hook/controller/admission_bindings_controller.go
new file mode 100644
index 0000000..a72ad78
--- /dev/null
+++ b/pkg/hook/controller/admission_bindings_controller.go
@@ -0,0 +1,183 @@
+package controller
+
+import (
+	log "github.com/sirupsen/logrus"
+
+	. "github.com/flant/shell-operator/pkg/hook/binding_context"
+	. "github.com/flant/shell-operator/pkg/hook/types"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
+
+	"github.com/flant/shell-operator/pkg/webhook/admission"
+)
+
+// A link between a hook and a kube monitor
+type AdmissionBindingToWebhookLink struct {
+	BindingName     string
+	ConfigurationId string
+	WebhookId       string
+	// Useful fields to create a BindingContext
+	IncludeSnapshots []string
+	Group            string
+}
+
+// ScheduleBindingsController handles schedule bindings for one hook.
+type AdmissionBindingsController interface {
+	WithValidatingBindings([]ValidatingConfig)
+	WithMutatingBindings([]MutatingConfig)
+	WithWebhookManager(*admission.WebhookManager)
+	EnableValidatingBindings()
+	EnableMutatingBindings()
+	DisableValidatingBindings()
+	DisableMutatingBindings()
+	CanHandleEvent(event AdmissionEvent) bool
+	HandleEvent(event AdmissionEvent) BindingExecutionInfo
+}
+
+type admissionBindingsController struct {
+	// Controller holds validating/mutating bindings from one hook. Hook always belongs to one configurationId.
+	ConfigurationId string
+	// WebhookId -> link
+	AdmissionLinks map[string]*AdmissionBindingToWebhookLink
+
+	ValidatingBindings []ValidatingConfig
+	MutatingBindings   []MutatingConfig
+
+	webhookManager *admission.WebhookManager
+}
+
+var _ AdmissionBindingsController = &admissionBindingsController{}
+
+// NewKubernetesHooksController returns an implementation of KubernetesHooksController
+var NewValidatingBindingsController = func() *admissionBindingsController {
+	return &admissionBindingsController{
+		AdmissionLinks: make(map[string]*AdmissionBindingToWebhookLink),
+	}
+}
+
+func (c *admissionBindingsController) WithValidatingBindings(bindings []ValidatingConfig) {
+	c.ValidatingBindings = bindings
+}
+
+func (c *admissionBindingsController) WithMutatingBindings(bindings []MutatingConfig) {
+	c.MutatingBindings = bindings
+}
+
+func (c *admissionBindingsController) WithWebhookManager(mgr *admission.WebhookManager) {
+	c.webhookManager = mgr
+}
+
+func (c *admissionBindingsController) EnableValidatingBindings() {
+	confId := ""
+
+	if len(c.ValidatingBindings) <= 0 {
+		return
+	}
+
+	for _, config := range c.ValidatingBindings {
+		if config.Webhook.Metadata.ConfigurationId == "" && confId == "" {
+			continue
+		}
+		if config.Webhook.Metadata.ConfigurationId != "" && confId == "" {
+			confId = config.Webhook.Metadata.ConfigurationId
+			continue
+		}
+		if config.Webhook.Metadata.ConfigurationId != confId {
+			log.Errorf("Possible bug!!! kubernetesValidating has non-unique configurationIds: '%s' '%s'", config.Webhook.Metadata.ConfigurationId, confId)
+		}
+	}
+	c.ConfigurationId = confId
+
+	for _, config := range c.ValidatingBindings {
+		c.AdmissionLinks[config.Webhook.Metadata.WebhookId] = &AdmissionBindingToWebhookLink{
+			BindingName:      config.BindingName,
+			ConfigurationId:  c.ConfigurationId,
+			WebhookId:        config.Webhook.Metadata.WebhookId,
+			IncludeSnapshots: config.IncludeSnapshotsFrom,
+			Group:            config.Group,
+		}
+		c.webhookManager.AddValidatingWebhook(config.Webhook)
+	}
+}
+
+func (c *admissionBindingsController) EnableMutatingBindings() {
+	confId := ""
+
+	if len(c.MutatingBindings) <= 0 {
+		return
+	}
+
+	for _, config := range c.MutatingBindings {
+		if config.Webhook.Metadata.ConfigurationId == "" && confId == "" {
+			continue
+		}
+		if config.Webhook.Metadata.ConfigurationId != "" && confId == "" {
+			confId = config.Webhook.Metadata.ConfigurationId
+			continue
+		}
+		if config.Webhook.Metadata.ConfigurationId != confId {
+			log.Errorf("Possible bug!!! kubernetesMutating has non-unique configurationIds: '%s' '%s'", config.Webhook.Metadata.ConfigurationId, confId)
+		}
+	}
+	c.ConfigurationId = confId
+
+	for _, config := range c.MutatingBindings {
+		c.AdmissionLinks[config.Webhook.Metadata.WebhookId] = &AdmissionBindingToWebhookLink{
+			BindingName:      config.BindingName,
+			ConfigurationId:  c.ConfigurationId,
+			WebhookId:        config.Webhook.Metadata.WebhookId,
+			IncludeSnapshots: config.IncludeSnapshotsFrom,
+			Group:            config.Group,
+		}
+		c.webhookManager.AddMutatingWebhook(config.Webhook)
+	}
+}
+
+func (c *admissionBindingsController) DisableValidatingBindings() {
+	// TODO dynamic enable/disable validating webhooks.
+}
+
+func (c *admissionBindingsController) DisableMutatingBindings() {
+	// TODO dynamic enable/disable mutating webhooks.
+}
+
+func (c *admissionBindingsController) CanHandleEvent(event AdmissionEvent) bool {
+	if c.ConfigurationId != event.ConfigurationId {
+		return false
+	}
+	_, has := c.AdmissionLinks[event.WebhookId]
+	return has
+}
+
+func (c *admissionBindingsController) HandleEvent(event AdmissionEvent) BindingExecutionInfo {
+	if c.ConfigurationId != event.ConfigurationId {
+		log.Errorf("Possible bug!!! Unknown validating event: no binding for configurationId '%s' (webhookId '%s')", event.ConfigurationId, event.WebhookId)
+		return BindingExecutionInfo{
+			BindingContext: []BindingContext{},
+			AllowFailure:   false,
+		}
+	}
+
+	link, hasKey := c.AdmissionLinks[event.WebhookId]
+	if !hasKey {
+		log.Errorf("Possible bug!!! Unknown validating event: no binding for configurationId '%s', webhookId '%s'", event.ConfigurationId, event.WebhookId)
+		return BindingExecutionInfo{
+			BindingContext: []BindingContext{},
+			AllowFailure:   false,
+		}
+	}
+
+	bc := BindingContext{
+		Binding:         link.BindingName,
+		AdmissionReview: event.Review,
+	}
+	bc.Metadata.BindingType = KubernetesValidating
+	bc.Metadata.IncludeSnapshots = link.IncludeSnapshots
+	bc.Metadata.Group = link.Group
+
+	return BindingExecutionInfo{
+		BindingContext:   []BindingContext{bc},
+		Binding:          link.BindingName,
+		IncludeSnapshots: link.IncludeSnapshots,
+		Group:            link.Group,
+	}
+}
diff --git a/pkg/hook/controller/hook_controller.go b/pkg/hook/controller/hook_controller.go
index 799e07d..f70b4b5 100644
--- a/pkg/hook/controller/hook_controller.go
+++ b/pkg/hook/controller/hook_controller.go
@@ -4,12 +4,12 @@ import (
 	. "github.com/flant/shell-operator/pkg/hook/binding_context"
 	. "github.com/flant/shell-operator/pkg/hook/types"
 	. "github.com/flant/shell-operator/pkg/kube_events_manager/types"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
 	"github.com/flant/shell-operator/pkg/schedule_manager"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 )
 
 type BindingExecutionInfo struct {
@@ -37,12 +37,12 @@ type BindingExecutionInfo struct {
 type HookController interface {
 	InitKubernetesBindings([]OnKubernetesEventConfig, kube_events_manager.KubeEventsManager)
 	InitScheduleBindings([]ScheduleConfig, schedule_manager.ScheduleManager)
-	InitValidatingBindings([]ValidatingConfig, *validating.WebhookManager)
+	InitAdmissionBindings([]ValidatingConfig, []MutatingConfig, *admission.WebhookManager)
 	InitConversionBindings([]ConversionConfig, *conversion.WebhookManager)
 
 	CanHandleKubeEvent(kubeEvent KubeEvent) bool
 	CanHandleScheduleEvent(crontab string) bool
-	CanHandleValidatingEvent(event ValidatingEvent) bool
+	CanHandleAdmissionEvent(event AdmissionEvent) bool
 	CanHandleConversionEvent(event conversion.Event, rule conversion.Rule) bool
 
 	// These method should call an underlying *Binding*Controller to get binding context
@@ -50,7 +50,7 @@ type HookController interface {
 	HandleEnableKubernetesBindings(createTasksFn func(BindingExecutionInfo)) error
 	HandleKubeEvent(event KubeEvent, createTasksFn func(BindingExecutionInfo))
 	HandleScheduleEvent(crontab string, createTasksFn func(BindingExecutionInfo))
-	HandleValidatingEvent(event ValidatingEvent, createTasksFn func(BindingExecutionInfo))
+	HandleAdmissionEvent(event AdmissionEvent, createTasksFn func(BindingExecutionInfo))
 	HandleConversionEvent(event conversion.Event, rule conversion.Rule, createTasksFn func(BindingExecutionInfo))
 
 	UnlockKubernetesEvents()
@@ -61,7 +61,7 @@ type HookController interface {
 	EnableScheduleBindings()
 	DisableScheduleBindings()
 
-	EnableValidatingBindings()
+	EnableAdmissionBindings()
 
 	EnableConversionBindings()
 
@@ -80,11 +80,12 @@ func NewHookController() HookController {
 type hookController struct {
 	KubernetesController KubernetesBindingsController
 	ScheduleController   ScheduleBindingsController
-	ValidatingController ValidatingBindingsController
+	AdmissionController  AdmissionBindingsController
 	ConversionController ConversionBindingsController
 	kubernetesBindings   []OnKubernetesEventConfig
 	scheduleBindings     []ScheduleConfig
 	validatingBindings   []ValidatingConfig
+	mutatingBindings     []MutatingConfig
 	conversionBindings   []ConversionConfig
 }
 
@@ -112,18 +113,35 @@ func (hc *hookController) InitScheduleBindings(bindings []ScheduleConfig, schedu
 	hc.scheduleBindings = bindings
 }
 
-func (hc *hookController) InitValidatingBindings(bindings []ValidatingConfig, webhookMgr *validating.WebhookManager) {
+func (hc *hookController) InitAdmissionBindings(vbindings []ValidatingConfig, mbindings []MutatingConfig, webhookMgr *admission.WebhookManager) {
+
+	bindingCtrl := NewValidatingBindingsController()
+	bindingCtrl.WithWebhookManager(webhookMgr)
+	hc.AdmissionController = bindingCtrl
+
+	hc.initValidatingBindings(vbindings)
+	hc.initMutatingBindings(mbindings)
+
+}
+
+func (hc *hookController) initValidatingBindings(bindings []ValidatingConfig) {
 	if len(bindings) == 0 {
 		return
 	}
 
-	bindingCtrl := NewValidatingBindingsController()
-	bindingCtrl.WithWebhookManager(webhookMgr)
-	bindingCtrl.WithValidatingBindings(bindings)
-	hc.ValidatingController = bindingCtrl
+	hc.AdmissionController.WithValidatingBindings(bindings)
 	hc.validatingBindings = bindings
 }
 
+func (hc *hookController) initMutatingBindings(bindings []MutatingConfig) {
+	if len(bindings) == 0 {
+		return
+	}
+
+	hc.AdmissionController.WithMutatingBindings(bindings)
+	hc.mutatingBindings = bindings
+}
+
 func (hc *hookController) InitConversionBindings(bindings []ConversionConfig, webhookMgr *conversion.WebhookManager) {
 	if len(bindings) == 0 {
 		return
@@ -150,9 +168,9 @@ func (hc *hookController) CanHandleScheduleEvent(crontab string) bool {
 	return false
 }
 
-func (hc *hookController) CanHandleValidatingEvent(event ValidatingEvent) bool {
-	if hc.ValidatingController != nil {
-		return hc.ValidatingController.CanHandleEvent(event)
+func (hc *hookController) CanHandleAdmissionEvent(event AdmissionEvent) bool {
+	if hc.AdmissionController != nil {
+		return hc.AdmissionController.CanHandleEvent(event)
 	}
 	return false
 }
@@ -190,11 +208,11 @@ func (hc *hookController) HandleKubeEvent(event KubeEvent, createTasksFn func(Bi
 	}
 }
 
-func (hc *hookController) HandleValidatingEvent(event ValidatingEvent, createTasksFn func(BindingExecutionInfo)) {
-	if hc.ValidatingController == nil {
+func (hc *hookController) HandleAdmissionEvent(event AdmissionEvent, createTasksFn func(BindingExecutionInfo)) {
+	if hc.AdmissionController == nil {
 		return
 	}
-	execInfo := hc.ValidatingController.HandleEvent(event)
+	execInfo := hc.AdmissionController.HandleEvent(event)
 	if createTasksFn != nil {
 		createTasksFn(execInfo)
 	}
@@ -260,9 +278,10 @@ func (hc *hookController) DisableScheduleBindings() {
 	}
 }
 
-func (hc *hookController) EnableValidatingBindings() {
-	if hc.ValidatingController != nil {
-		hc.ValidatingController.EnableValidatingBindings()
+func (hc *hookController) EnableAdmissionBindings() {
+	if hc.AdmissionController != nil {
+		hc.AdmissionController.EnableValidatingBindings()
+		hc.AdmissionController.EnableMutatingBindings()
 	}
 }
 
diff --git a/pkg/hook/controller/validating_bindings_controller.go b/pkg/hook/controller/validating_bindings_controller.go
deleted file mode 100644
index 23dff99..0000000
--- a/pkg/hook/controller/validating_bindings_controller.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package controller
-
-import (
-	log "github.com/sirupsen/logrus"
-
-	. "github.com/flant/shell-operator/pkg/hook/binding_context"
-	. "github.com/flant/shell-operator/pkg/hook/types"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
-
-	"github.com/flant/shell-operator/pkg/webhook/validating"
-)
-
-// A link between a hook and a kube monitor
-type ValidatingBindingToWebhookLink struct {
-	BindingName     string
-	ConfigurationId string
-	WebhookId       string
-	// Useful fields to create a BindingContext
-	IncludeSnapshots []string
-	Group            string
-}
-
-// ScheduleBindingsController handles schedule bindings for one hook.
-type ValidatingBindingsController interface {
-	WithValidatingBindings([]ValidatingConfig)
-	WithWebhookManager(*validating.WebhookManager)
-	EnableValidatingBindings()
-	DisableValidatingBindings()
-	CanHandleEvent(event ValidatingEvent) bool
-	HandleEvent(event ValidatingEvent) BindingExecutionInfo
-}
-
-type validatingBindingsController struct {
-	// Controller holds validating bindings from one hook. Hook always belongs to one configurationId.
-	ConfigurationId string
-	// WebhookId -> link
-	ValidatingLinks map[string]*ValidatingBindingToWebhookLink
-
-	ValidatingBindings []ValidatingConfig
-
-	webhookManager *validating.WebhookManager
-}
-
-var _ ValidatingBindingsController = &validatingBindingsController{}
-
-// NewKubernetesHooksController returns an implementation of KubernetesHooksController
-var NewValidatingBindingsController = func() *validatingBindingsController {
-	return &validatingBindingsController{
-		ValidatingLinks: make(map[string]*ValidatingBindingToWebhookLink),
-	}
-}
-
-func (c *validatingBindingsController) WithValidatingBindings(bindings []ValidatingConfig) {
-	c.ValidatingBindings = bindings
-}
-
-func (c *validatingBindingsController) WithWebhookManager(mgr *validating.WebhookManager) {
-	c.webhookManager = mgr
-}
-
-func (c *validatingBindingsController) EnableValidatingBindings() {
-	confId := ""
-	for _, config := range c.ValidatingBindings {
-		if config.Webhook.Metadata.ConfigurationId == "" && confId == "" {
-			continue
-		}
-		if config.Webhook.Metadata.ConfigurationId != "" && confId == "" {
-			confId = config.Webhook.Metadata.ConfigurationId
-			continue
-		}
-		if config.Webhook.Metadata.ConfigurationId != confId {
-			log.Errorf("Possible bug!!! kubernetesValidating has non-unique configurationIds: '%s' '%s'", config.Webhook.Metadata.ConfigurationId, confId)
-		}
-	}
-	c.ConfigurationId = confId
-
-	for _, config := range c.ValidatingBindings {
-		c.ValidatingLinks[config.Webhook.Metadata.WebhookId] = &ValidatingBindingToWebhookLink{
-			BindingName:      config.BindingName,
-			ConfigurationId:  c.ConfigurationId,
-			WebhookId:        config.Webhook.Metadata.WebhookId,
-			IncludeSnapshots: config.IncludeSnapshotsFrom,
-			Group:            config.Group,
-		}
-		c.webhookManager.AddWebhook(config.Webhook)
-	}
-}
-
-func (c *validatingBindingsController) DisableValidatingBindings() {
-	// TODO dynamic enable/disable validating webhooks.
-}
-
-func (c *validatingBindingsController) CanHandleEvent(event ValidatingEvent) bool {
-	if c.ConfigurationId != event.ConfigurationId {
-		return false
-	}
-	_, has := c.ValidatingLinks[event.WebhookId]
-	return has
-}
-
-func (c *validatingBindingsController) HandleEvent(event ValidatingEvent) BindingExecutionInfo {
-	if c.ConfigurationId != event.ConfigurationId {
-		log.Errorf("Possible bug!!! Unknown validating event: no binding for configurationId '%s' (webhookId '%s')", event.ConfigurationId, event.WebhookId)
-		return BindingExecutionInfo{
-			BindingContext: []BindingContext{},
-			AllowFailure:   false,
-		}
-	}
-
-	link, hasKey := c.ValidatingLinks[event.WebhookId]
-	if !hasKey {
-		log.Errorf("Possible bug!!! Unknown validating event: no binding for configurationId '%s', webhookId '%s'", event.ConfigurationId, event.WebhookId)
-		return BindingExecutionInfo{
-			BindingContext: []BindingContext{},
-			AllowFailure:   false,
-		}
-	}
-
-	bc := BindingContext{
-		Binding:         link.BindingName,
-		AdmissionReview: event.Review,
-	}
-	bc.Metadata.BindingType = KubernetesValidating
-	bc.Metadata.IncludeSnapshots = link.IncludeSnapshots
-	bc.Metadata.Group = link.Group
-
-	return BindingExecutionInfo{
-		BindingContext:   []BindingContext{bc},
-		Binding:          link.BindingName,
-		IncludeSnapshots: link.IncludeSnapshots,
-		Group:            link.Group,
-	}
-}
diff --git a/pkg/hook/hook.go b/pkg/hook/hook.go
index 1dfb1b8..9d41470 100644
--- a/pkg/hook/hook.go
+++ b/pkg/hook/hook.go
@@ -14,7 +14,7 @@ import (
 
 	. "github.com/flant/shell-operator/pkg/hook/binding_context"
 	. "github.com/flant/shell-operator/pkg/hook/types"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 
 	"github.com/flant/shell-operator/pkg/app"
 	"github.com/flant/shell-operator/pkg/executor"
@@ -32,7 +32,7 @@ type HookResult struct {
 	Usage                *executor.CmdUsage
 	Metrics              []operation.MetricOperation
 	ConversionResponse   *conversion.Response
-	ValidatingResponse   *ValidatingResponse
+	ValidatingResponse   *AdmissionResponse
 	KubernetesPatchBytes []byte
 }
 
@@ -148,7 +148,7 @@ func (h *Hook) Run(_ BindingType, context []BindingContext, logLabels map[string
 		return result, fmt.Errorf("got bad metrics: %s", err)
 	}
 
-	result.ValidatingResponse, err = ValidatingResponseFromFile(validatingPath)
+	result.ValidatingResponse, err = AdmissionResponseFromFile(validatingPath)
 	if err != nil {
 		return result, fmt.Errorf("got bad validating response: %s", err)
 	}
diff --git a/pkg/hook/hook_manager.go b/pkg/hook/hook_manager.go
index c254f8d..6fa3743 100644
--- a/pkg/hook/hook_manager.go
+++ b/pkg/hook/hook_manager.go
@@ -12,15 +12,15 @@ import (
 
 	. "github.com/flant/shell-operator/pkg/hook/types"
 	. "github.com/flant/shell-operator/pkg/kube_events_manager/types"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 
 	"github.com/flant/shell-operator/pkg/executor"
 	"github.com/flant/shell-operator/pkg/hook/controller"
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
 	"github.com/flant/shell-operator/pkg/schedule_manager"
 	utils_file "github.com/flant/shell-operator/pkg/utils/file"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 )
 
 type HookManager interface {
@@ -30,7 +30,7 @@ type HookManager interface {
 	WithKubeEventManager(kube_events_manager.KubeEventsManager)
 	WithScheduleManager(schedule_manager.ScheduleManager)
 	WithConversionWebhookManager(*conversion.WebhookManager)
-	WithValidatingWebhookManager(*validating.WebhookManager)
+	WithAdmissionWebhookManager(*admission.WebhookManager)
 	WorkingDir() string
 	TempDir() string
 	GetHook(name string) *Hook
@@ -38,7 +38,7 @@ type HookManager interface {
 	GetHooksInOrder(bindingType BindingType) ([]string, error)
 	HandleKubeEvent(kubeEvent KubeEvent, createTaskFn func(*Hook, controller.BindingExecutionInfo))
 	HandleScheduleEvent(crontab string, createTaskFn func(*Hook, controller.BindingExecutionInfo))
-	HandleValidatingEvent(event ValidatingEvent, createTaskFn func(*Hook, controller.BindingExecutionInfo))
+	HandleAdmissionEvent(event AdmissionEvent, createTaskFn func(*Hook, controller.BindingExecutionInfo))
 	HandleConversionEvent(event conversion.Event, rule conversion.Rule, createTaskFn func(*Hook, controller.BindingExecutionInfo))
 	FindConversionChain(crdName string, rule conversion.Rule) []conversion.Rule
 }
@@ -50,7 +50,7 @@ type hookManager struct {
 	kubeEventsManager        kube_events_manager.KubeEventsManager
 	scheduleManager          schedule_manager.ScheduleManager
 	conversionWebhookManager *conversion.WebhookManager
-	validatingWebhookManager *validating.WebhookManager
+	admissionWebhookManager  *admission.WebhookManager
 
 	// sorted hook names
 	hookNamesInOrder []string
@@ -89,8 +89,8 @@ func (hm *hookManager) WithScheduleManager(mgr schedule_manager.ScheduleManager)
 	hm.scheduleManager = mgr
 }
 
-func (hm *hookManager) WithValidatingWebhookManager(mgr *validating.WebhookManager) {
-	hm.validatingWebhookManager = mgr
+func (hm *hookManager) WithAdmissionWebhookManager(mgr *admission.WebhookManager) {
+	hm.admissionWebhookManager = mgr
 }
 
 func (hm *hookManager) WithConversionWebhookManager(mgr *conversion.WebhookManager) {
@@ -188,6 +188,7 @@ func (hm *hookManager) loadHook(hookPath string) (hook *Hook, err error) {
 			"binding": conversionCfg.BindingName,
 		}
 	}
+
 	for _, validatingCfg := range hook.GetConfig().KubernetesValidating {
 		validatingCfg.Webhook.Metadata.LogLabels["hook"] = hook.Name
 		validatingCfg.Webhook.Metadata.MetricLabels = map[string]string{
@@ -196,12 +197,22 @@ func (hm *hookManager) loadHook(hookPath string) (hook *Hook, err error) {
 		}
 		validatingCfg.Webhook.UpdateIds("", validatingCfg.BindingName)
 	}
+	for _, mutatingCfg := range hook.GetConfig().KubernetesMutating {
+		mutatingCfg.Webhook.Metadata.LogLabels["hook"] = hook.Name
+		mutatingCfg.Webhook.Metadata.MetricLabels = map[string]string{
+			"hook":    hook.Name,
+			"binding": mutatingCfg.BindingName,
+		}
+		mutatingCfg.Webhook.UpdateIds("", mutatingCfg.BindingName)
+	}
 
 	hookCtrl := controller.NewHookController()
 	hookCtrl.InitKubernetesBindings(hook.GetConfig().OnKubernetesEvents, hm.kubeEventsManager)
 	hookCtrl.InitScheduleBindings(hook.GetConfig().Schedules, hm.scheduleManager)
 	hookCtrl.InitConversionBindings(hook.GetConfig().KubernetesConversion, hm.conversionWebhookManager)
-	hookCtrl.InitValidatingBindings(hook.GetConfig().KubernetesValidating, hm.validatingWebhookManager)
+	hookCtrl.InitAdmissionBindings(hook.GetConfig().KubernetesValidating, hook.GetConfig().KubernetesMutating, hm.admissionWebhookManager)
+	// TODO
+	// hookCtrl.InitMutatingBindings(hook.GetConfig().KubernetesMutating, hm.admissionWebhookManager)
 
 	hook.WithHookController(hookCtrl)
 	hook.WithTmpDir(hm.TempDir())
@@ -309,12 +320,26 @@ func (hm *hookManager) HandleScheduleEvent(crontab string, createTaskFn func(*Ho
 	}
 }
 
-func (hm *hookManager) HandleValidatingEvent(event ValidatingEvent, createTaskFn func(*Hook, controller.BindingExecutionInfo)) {
+func (hm *hookManager) HandleAdmissionEvent(event AdmissionEvent, createTaskFn func(*Hook, controller.BindingExecutionInfo)) {
 	vHooks, _ := hm.GetHooksInOrder(KubernetesValidating)
 	for _, hookName := range vHooks {
 		h := hm.GetHook(hookName)
-		if h.HookController.CanHandleValidatingEvent(event) {
-			h.HookController.HandleValidatingEvent(event, func(info controller.BindingExecutionInfo) {
+		event.Binding = string(KubernetesValidating)
+		if h.HookController.CanHandleAdmissionEvent(event) {
+			h.HookController.HandleAdmissionEvent(event, func(info controller.BindingExecutionInfo) {
+				if createTaskFn != nil {
+					createTaskFn(h, info)
+				}
+			})
+		}
+	}
+
+	mHooks, _ := hm.GetHooksInOrder(KubernetesMutating)
+	for _, hookName := range mHooks {
+		h := hm.GetHook(hookName)
+		event.Binding = string(KubernetesMutating)
+		if h.HookController.CanHandleAdmissionEvent(event) {
+			h.HookController.HandleAdmissionEvent(event, func(info controller.BindingExecutionInfo) {
 				if createTaskFn != nil {
 					createTaskFn(h, info)
 				}
diff --git a/pkg/hook/types/bindings.go b/pkg/hook/types/bindings.go
index c8fc535..c32b43a 100644
--- a/pkg/hook/types/bindings.go
+++ b/pkg/hook/types/bindings.go
@@ -5,8 +5,8 @@ import (
 
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
 	. "github.com/flant/shell-operator/pkg/schedule_manager/types"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 )
 
 type BindingType string
@@ -17,6 +17,7 @@ const (
 	OnKubernetesEvent    BindingType = "kubernetes"
 	KubernetesConversion BindingType = "kubernetesCustomResourceConversion"
 	KubernetesValidating BindingType = "kubernetesValidating"
+	KubernetesMutating   BindingType = "kubernetesMutating"
 )
 
 // Types for effective binding configs
@@ -60,7 +61,14 @@ type ValidatingConfig struct {
 	CommonBindingConfig
 	IncludeSnapshotsFrom []string
 	Group                string
-	Webhook              *validating.ValidatingWebhookConfig
+	Webhook              *admission.ValidatingWebhookConfig
+}
+
+type MutatingConfig struct {
+	CommonBindingConfig
+	IncludeSnapshotsFrom []string
+	Group                string
+	Webhook              *admission.MutatingWebhookConfig
 }
 
 type Settings struct {
diff --git a/pkg/jq/apply_jq_exec.go b/pkg/jq/apply_jq_exec.go
index 0b06c42..1b052bc 100644
--- a/pkg/jq/apply_jq_exec.go
+++ b/pkg/jq/apply_jq_exec.go
@@ -1,3 +1,4 @@
+//go:build !cgo || (cgo && !use_libjq)
 // +build !cgo cgo,!use_libjq
 
 package jq
@@ -12,5 +13,5 @@ func ApplyJqFilter(jqFilter string, jsonData []byte, libPath string) (string, er
 }
 
 func JqFilterInfo() string {
-	return "Use jq binary for jqFilter"
+	return "jqFilter implementation: use jq binary from $PATH"
 }
diff --git a/pkg/jq/apply_libjq-go.go b/pkg/jq/apply_libjq-go.go
index a00a5d8..59c084c 100644
--- a/pkg/jq/apply_libjq-go.go
+++ b/pkg/jq/apply_libjq-go.go
@@ -1,3 +1,4 @@
+//go:build cgo && use_libjq
 // +build cgo,use_libjq
 
 package jq
@@ -29,7 +30,7 @@ func ApplyJqFilter(jqFilter string, jsonData []byte, libPath string) (string, er
 
 func JqFilterInfo() string {
 	if os.Getenv("JQ_EXEC") == "yes" {
-		return "JQ_EXEC is set, use jq binary for jqFilter"
+		return "jqFilter implementation: use jq binary from $PATH (JQ_EXEC=yes is set)"
 	}
-	return "Use libjq-go for jqFilter"
+	return "jqFilter implementation: use embedded libjq-go"
 }
diff --git a/pkg/jq/jq_exec.go b/pkg/jq/jq_exec.go
index 6c59a99..d5c2123 100644
--- a/pkg/jq/jq_exec.go
+++ b/pkg/jq/jq_exec.go
@@ -13,9 +13,9 @@ import (
 func jqExec(jqFilter string, jsonData []byte, libPath string) (result string, err error) {
 	var cmd *exec.Cmd
 	if libPath == "" {
-		cmd = exec.Command("/usr/bin/jq", jqFilter)
+		cmd = exec.Command("jq", jqFilter)
 	} else {
-		cmd = exec.Command("/usr/bin/jq", "-L", libPath, jqFilter)
+		cmd = exec.Command("jq", "-L", libPath, jqFilter)
 	}
 
 	var stdinBuf bytes.Buffer
diff --git a/pkg/shell-operator/bootstrap.go b/pkg/shell-operator/bootstrap.go
index f86d81f..56ae3b2 100644
--- a/pkg/shell-operator/bootstrap.go
+++ b/pkg/shell-operator/bootstrap.go
@@ -18,8 +18,8 @@ import (
 	"github.com/flant/shell-operator/pkg/schedule_manager"
 	"github.com/flant/shell-operator/pkg/task/queue"
 	utils_file "github.com/flant/shell-operator/pkg/utils/file"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
 )
 
 // Init initialize logging, ensures directories and creates
@@ -218,11 +218,11 @@ func SetupEventManagers(op *ShellOperator) {
 
 // SetupHookManagers instantiates different hook managers.
 func SetupHookManagers(op *ShellOperator, hooksDir string, tempDir string) {
-	// Initialize validating webhooks manager
-	op.ValidatingWebhookManager = validating.NewWebhookManager()
-	op.ValidatingWebhookManager.WithKubeClient(op.KubeClient)
-	op.ValidatingWebhookManager.Settings = app.ValidatingWebhookSettings
-	op.ValidatingWebhookManager.Namespace = app.Namespace
+	// Initialize admission webhooks manager
+	op.AdmissionWebhookManager = admission.NewWebhookManager()
+	op.AdmissionWebhookManager.WithKubeClient(op.KubeClient)
+	op.AdmissionWebhookManager.Settings = app.ValidatingWebhookSettings
+	op.AdmissionWebhookManager.Namespace = app.Namespace
 
 	// Initialize validating webhooks manager
 	op.ConversionWebhookManager = conversion.NewWebhookManager()
@@ -235,6 +235,6 @@ func SetupHookManagers(op *ShellOperator, hooksDir string, tempDir string) {
 	op.HookManager.WithDirectories(hooksDir, tempDir)
 	op.HookManager.WithKubeEventManager(op.KubeEventsManager)
 	op.HookManager.WithScheduleManager(op.ScheduleManager)
-	op.HookManager.WithValidatingWebhookManager(op.ValidatingWebhookManager)
+	op.HookManager.WithAdmissionWebhookManager(op.AdmissionWebhookManager)
 	op.HookManager.WithConversionWebhookManager(op.ConversionWebhookManager)
 }
diff --git a/pkg/shell-operator/operator.go b/pkg/shell-operator/operator.go
index b2abc95..f434872 100644
--- a/pkg/shell-operator/operator.go
+++ b/pkg/shell-operator/operator.go
@@ -23,9 +23,9 @@ import (
 	"github.com/flant/shell-operator/pkg/task/queue"
 	utils "github.com/flant/shell-operator/pkg/utils/labels"
 	"github.com/flant/shell-operator/pkg/utils/measure"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
 )
 
 var WaitQueuesTimeout = time.Second * 10
@@ -49,7 +49,7 @@ type ShellOperator struct {
 
 	HookManager hook.HookManager
 
-	ValidatingWebhookManager *validating.WebhookManager
+	AdmissionWebhookManager  *admission.WebhookManager
 	ConversionWebhookManager *conversion.WebhookManager
 }
 
@@ -154,16 +154,19 @@ func (op *ShellOperator) InitHookManager() (err error) {
 // InitValidatingWebhookManager adds kubernetesValidating hooks
 // to a WebhookManager and set a validating event handler.
 func (op *ShellOperator) InitValidatingWebhookManager() (err error) {
-	if op.HookManager == nil || op.ValidatingWebhookManager == nil {
+	if op.HookManager == nil || op.AdmissionWebhookManager == nil {
 		return
 	}
 	// Do not init ValidatingWebhook if there are no KubernetesValidating hooks.
-	hookNames, _ := op.HookManager.GetHooksInOrder(KubernetesValidating)
+	hookNamesV, _ := op.HookManager.GetHooksInOrder(KubernetesValidating)
+	hookNamesM, _ := op.HookManager.GetHooksInOrder(KubernetesMutating)
+
+	hookNames := append(hookNamesV, hookNamesM...)
 	if len(hookNames) == 0 {
 		return
 	}
 
-	err = op.ValidatingWebhookManager.Init()
+	err = op.AdmissionWebhookManager.Init()
 	if err != nil {
 		log.Errorf("ValidatingWebhookManager init: %v", err)
 		return err
@@ -171,24 +174,24 @@ func (op *ShellOperator) InitValidatingWebhookManager() (err error) {
 
 	for _, hookName := range hookNames {
 		h := op.HookManager.GetHook(hookName)
-		h.HookController.EnableValidatingBindings()
+		h.HookController.EnableAdmissionBindings()
 	}
 
-	// Define handler for ValidatingEvent
-	op.ValidatingWebhookManager.WithValidatingEventHandler(func(event ValidatingEvent) (*ValidatingResponse, error) {
+	// Define handler for AdmissionEvent
+	op.AdmissionWebhookManager.WithAdmissionEventHandler(func(event AdmissionEvent) (*AdmissionResponse, error) {
 		logLabels := map[string]string{
 			"event.id": uuid.NewV4().String(),
-			"binding":  string(KubernetesValidating),
+			"binding":  string(BindingType(event.Binding)),
 		}
 		logEntry := log.WithFields(utils.LabelsToLogFields(logLabels))
-		logEntry.Debugf("Handle '%s' event '%s' '%s'", string(KubernetesValidating), event.ConfigurationId, event.WebhookId)
+		logEntry.Debugf("Handle '%s' event '%s' '%s'", BindingType(event.Binding), event.ConfigurationId, event.WebhookId)
 
 		var tasks []task.Task
-		op.HookManager.HandleValidatingEvent(event, func(hook *hook.Hook, info controller.BindingExecutionInfo) {
+		op.HookManager.HandleAdmissionEvent(event, func(hook *hook.Hook, info controller.BindingExecutionInfo) {
 			newTask := task.NewTask(HookRun).
 				WithMetadata(HookMetadata{
 					HookName:       hook.Name,
-					BindingType:    KubernetesValidating,
+					BindingType:    BindingType(event.Binding),
 					BindingContext: info.BindingContext,
 					AllowFailure:   info.AllowFailure,
 					Binding:        info.Binding,
@@ -211,14 +214,14 @@ func (op *ShellOperator) InitValidatingWebhookManager() (err error) {
 		res := op.TaskHandler(tasks[0])
 
 		if res.Status == "Fail" {
-			return &ValidatingResponse{
+			return &AdmissionResponse{
 				Allowed: false,
 				Message: "Hook failed",
 			}, nil
 		}
 
 		validatingProp := tasks[0].GetProp("validatingResponse")
-		validatingResponse, ok := validatingProp.(*ValidatingResponse)
+		validatingResponse, ok := validatingProp.(*AdmissionResponse)
 		if !ok {
 			logEntry.Errorf("'validatingResponse' task prop is not of type *ValidatingResponse: %T", validatingProp)
 			return nil, fmt.Errorf("hook task prop error")
@@ -226,7 +229,7 @@ func (op *ShellOperator) InitValidatingWebhookManager() (err error) {
 		return validatingResponse, nil
 	})
 
-	err = op.ValidatingWebhookManager.Start()
+	err = op.AdmissionWebhookManager.Start()
 	if err != nil {
 		log.Errorf("ValidatingWebhookManager start: %v", err)
 	}
diff --git a/pkg/webhook/admission/config.go b/pkg/webhook/admission/config.go
new file mode 100644
index 0000000..855fcac
--- /dev/null
+++ b/pkg/webhook/admission/config.go
@@ -0,0 +1,72 @@
+package admission
+
+import (
+	"github.com/flant/shell-operator/pkg/utils/string_helper"
+	v1 "k8s.io/api/admissionregistration/v1"
+)
+
+// ConfigurationId is a first element in Path field for each Webhook.
+// It should be url safe.
+//
+// WebhookId is a second element for Path field.
+
+type Metadata struct {
+	Name            string
+	WebhookId       string
+	ConfigurationId string // A suffix to create different ValidatingWebhookConfiguration/MutatingWebhookConfiguration resources.
+	DebugName       string
+	LogLabels       map[string]string
+	MetricLabels    map[string]string
+}
+
+type IWebhookConfig interface {
+	GetMeta() Metadata
+	SetMeta(Metadata)
+	SetClientConfig(v1.WebhookClientConfig)
+	UpdateIds(string, string)
+}
+
+// ValidatingWebhookConfig
+type ValidatingWebhookConfig struct {
+	*v1.ValidatingWebhook
+	Metadata
+}
+
+func (wc *ValidatingWebhookConfig) GetMeta() Metadata  { return wc.Metadata }
+func (wc *ValidatingWebhookConfig) SetMeta(m Metadata) { wc.Metadata = m }
+func (wc *ValidatingWebhookConfig) SetClientConfig(cc v1.WebhookClientConfig) {
+	equivalent := v1.Equivalent
+	wc.MatchPolicy = &equivalent
+	wc.ClientConfig = cc
+}
+
+// MutatingWebhookConfig
+type MutatingWebhookConfig struct {
+	*v1.MutatingWebhook
+	Metadata
+}
+
+func (wc *MutatingWebhookConfig) GetMeta() Metadata  { return wc.Metadata }
+func (wc *MutatingWebhookConfig) SetMeta(m Metadata) { wc.Metadata = m }
+func (wc *MutatingWebhookConfig) SetClientConfig(cc v1.WebhookClientConfig) {
+	equivalent := v1.Equivalent
+	wc.MatchPolicy = &equivalent
+	wc.ClientConfig = cc
+}
+
+// UpdateIds use confId and webhookId to set a ConfigurationId prefix and a WebhookId.
+func (c *ValidatingWebhookConfig) UpdateIds(confID, webhookID string) {
+	c.Metadata.ConfigurationId = confID
+	if confID == "" {
+		c.Metadata.ConfigurationId = DefaultConfigurationId
+	}
+	c.Metadata.WebhookId = string_helper.SafeURLString(webhookID)
+}
+
+func (c *MutatingWebhookConfig) UpdateIds(confID, webhookID string) {
+	c.Metadata.ConfigurationId = confID
+	if confID == "" {
+		c.Metadata.ConfigurationId = DefaultConfigurationId
+	}
+	c.Metadata.WebhookId = string_helper.SafeURLString(webhookID)
+}
diff --git a/pkg/webhook/validating/handler.go b/pkg/webhook/admission/handler.go
similarity index 76%
rename from pkg/webhook/validating/handler.go
rename to pkg/webhook/admission/handler.go
index a138049..8099d90 100644
--- a/pkg/webhook/validating/handler.go
+++ b/pkg/webhook/admission/handler.go
@@ -1,9 +1,9 @@
-package validating
+package admission
 
 import (
 	"encoding/json"
 	"fmt"
-	"io"
+	"io/ioutil"
 	"net/http"
 	"strings"
 
@@ -14,13 +14,16 @@ import (
 	v1 "k8s.io/api/admission/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/flant/shell-operator/pkg/utils/structured-logger"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
+	structured_logger "github.com/flant/shell-operator/pkg/utils/structured-logger"
+
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 )
 
+type AdmissionEventHandlerFn func(event AdmissionEvent) (*AdmissionResponse, error)
+
 type WebhookHandler struct {
-	Manager *WebhookManager
 	Router  chi.Router
+	Handler AdmissionEventHandlerFn
 }
 
 func NewWebhookHandler() *WebhookHandler {
@@ -39,7 +42,7 @@ func NewWebhookHandler() *WebhookHandler {
 func (h *WebhookHandler) ServeReviewRequest(w http.ResponseWriter, r *http.Request) {
 	defer r.Body.Close()
 
-	bodyBytes, err := io.ReadAll(r.Body)
+	bodyBytes, err := ioutil.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
 		_, _ = w.Write([]byte("Error reading request body"))
@@ -85,7 +88,7 @@ func (h *WebhookHandler) HandleReviewRequest(path string, body []byte) (*v1.Admi
 		},
 	}
 
-	if h.Manager.ValidatingEventHandlerFn == nil {
+	if h.Handler == nil {
 		response.Response.Allowed = false
 		response.Response.Result = &metav1.Status{
 			Code:    500,
@@ -94,13 +97,13 @@ func (h *WebhookHandler) HandleReviewRequest(path string, body []byte) (*v1.Admi
 		return response, nil
 	}
 
-	event := ValidatingEvent{
+	event := AdmissionEvent{
 		WebhookId:       webhookID,
 		ConfigurationId: configurationID,
 		Review:          &review,
 	}
 
-	validatingResponse, err := h.Manager.ValidatingEventHandlerFn(event)
+	validatingResponse, err := h.Handler(event)
 	if err != nil {
 		response.Response.Allowed = false
 		response.Response.Result = &metav1.Status{
@@ -124,13 +127,25 @@ func (h *WebhookHandler) HandleReviewRequest(path string, body []byte) (*v1.Admi
 	}
 
 	response.Response.Allowed = true
+
+	// When allowing a request, a mutating admission webhook may optionally modify the
+	// incoming object as well. This is done using the patch and patchType fields in the response.
+	// The only currently supported patchType is JSONPatch. See JSON patch documentation for
+	// more details. For patchType: JSONPatch, the patch field contains a base64-encoded
+	// array of JSON patch operations.
+	if len(validatingResponse.Patch) > 0 {
+		response.Response.Patch = validatingResponse.Patch
+		patchType := v1.PatchTypeJSONPatch
+		response.Response.PatchType = &patchType
+	}
+
 	return response, nil
 }
 
 // DetectConfigurationAndWebhook extracts configurationID and a webhookID from the url path.
 func DetectConfigurationAndWebhook(path string) (configurationID string, webhookID string) {
 	parts := strings.Split(path, "/")
-	webhookParts := make([]string, 0)
+	webhookParts := []string{}
 	for _, p := range parts {
 		if p == "" {
 			continue
diff --git a/pkg/webhook/admission/manager.go b/pkg/webhook/admission/manager.go
new file mode 100644
index 0000000..e7457d3
--- /dev/null
+++ b/pkg/webhook/admission/manager.go
@@ -0,0 +1,145 @@
+package admission
+
+import (
+	"io/ioutil"
+
+	log "github.com/sirupsen/logrus"
+
+	klient "github.com/flant/kube-client/client"
+	"github.com/flant/shell-operator/pkg/webhook/server"
+)
+
+// DefaultConfigurationId is a ConfigurationId for ValidatingWebhookConfiguration/MutatingWebhookConfiguration
+// without suffix.
+const DefaultConfigurationId = "hooks"
+
+// WebhookManager is a public interface to be used from operator.go.
+//
+// No dynamic configuration for now. The steps are:
+//   - Init manager
+//   - Call AddWEbhook for every binding in hooks
+//   - Start() to run server and create ValidatingWebhookConfiguration/MutatingWebhookConfiguration
+type WebhookManager struct {
+	KubeClient klient.Client
+
+	Settings  *WebhookSettings
+	Namespace string
+
+	DefaultConfigurationId string
+
+	Server              *server.WebhookServer
+	ValidatingResources map[string]*ValidatingWebhookResource
+	MutatingResources   map[string]*MutatingWebhookResource
+	Handler             *WebhookHandler
+}
+
+func NewWebhookManager() *WebhookManager {
+	return &WebhookManager{
+		ValidatingResources: make(map[string]*ValidatingWebhookResource),
+		MutatingResources:   make(map[string]*MutatingWebhookResource),
+	}
+}
+
+func (m *WebhookManager) WithKubeClient(kubeClient klient.Client) {
+	m.KubeClient = kubeClient
+}
+
+func (m *WebhookManager) WithAdmissionEventHandler(handler AdmissionEventHandlerFn) {
+	if m.Handler == nil {
+		m.Handler = &WebhookHandler{
+			Handler: handler,
+		}
+	} else {
+		m.Handler.Handler = handler
+	}
+}
+
+// Init creates dependencies
+func (m *WebhookManager) Init() error {
+	log.Info("Initialize validating webhooks manager. Load certificates.")
+
+	if m.DefaultConfigurationId == "" {
+		m.DefaultConfigurationId = DefaultConfigurationId
+	}
+	// settings
+	caBundleBytes, err := ioutil.ReadFile(m.Settings.CAPath)
+	if err != nil {
+		return err
+	}
+	m.Settings.CABundle = caBundleBytes
+
+	m.Handler = NewWebhookHandler()
+
+	m.Server = &server.WebhookServer{
+		Settings:  &m.Settings.Settings,
+		Namespace: m.Namespace,
+		Router:    m.Handler.Router,
+	}
+
+	return nil
+}
+
+func (m *WebhookManager) AddValidatingWebhook(config *ValidatingWebhookConfig) {
+	confId := config.Metadata.ConfigurationId
+	if confId == "" {
+		confId = m.DefaultConfigurationId
+	}
+	r, ok := m.ValidatingResources[confId]
+	if !ok {
+		r = NewValidatingWebhookResource(
+			WebhookResourceOptions{
+				m.KubeClient,
+				m.Namespace,
+				m.Settings.ConfigurationName + "-" + confId,
+				m.Settings.ServiceName,
+				m.Settings.CABundle,
+			},
+		)
+		m.ValidatingResources[confId] = r
+	}
+	r.Set(config)
+}
+
+func (m *WebhookManager) AddMutatingWebhook(config *MutatingWebhookConfig) {
+	confId := config.Metadata.ConfigurationId
+	if confId == "" {
+		confId = m.DefaultConfigurationId
+	}
+	r, ok := m.MutatingResources[confId]
+	if !ok {
+		r = NewMutatingWebhookResource(
+			WebhookResourceOptions{
+				m.KubeClient,
+				m.Namespace,
+				m.Settings.ConfigurationName + "-" + confId,
+				m.Settings.ServiceName,
+				m.Settings.CABundle,
+			},
+		)
+		m.MutatingResources[confId] = r
+	}
+	r.Set(config)
+}
+
+func (m *WebhookManager) Start() error {
+	err := m.Server.Start()
+	if err != nil {
+		return err
+	}
+
+	for _, r := range m.ValidatingResources {
+		err = r.Register()
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, r := range m.MutatingResources {
+		err = r.Register()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/webhook/admission/resource.go b/pkg/webhook/admission/resource.go
new file mode 100644
index 0000000..93a711c
--- /dev/null
+++ b/pkg/webhook/admission/resource.go
@@ -0,0 +1,196 @@
+package admission
+
+import (
+	"context"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+
+	v1 "k8s.io/api/admissionregistration/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	klient "github.com/flant/kube-client/client"
+)
+
+type WebhookResourceOptions struct {
+	KubeClient        klient.Client
+	Namespace         string
+	ConfigurationName string
+	ServiceName       string
+	CABundle          []byte
+}
+
+type ValidatingWebhookResource struct {
+	hooks map[string]*ValidatingWebhookConfig
+	opts  WebhookResourceOptions
+}
+
+func NewValidatingWebhookResource(opts WebhookResourceOptions) *ValidatingWebhookResource {
+	return &ValidatingWebhookResource{
+		hooks: make(map[string]*ValidatingWebhookConfig),
+		opts:  opts,
+	}
+}
+
+func (w *ValidatingWebhookResource) Set(whc *ValidatingWebhookConfig) {
+	w.hooks[whc.Metadata.WebhookId] = whc
+}
+
+func (w *ValidatingWebhookResource) Get(id string) *ValidatingWebhookConfig {
+	return w.hooks[id]
+}
+
+func (w *ValidatingWebhookResource) Register() error {
+
+	configuration := &v1.ValidatingWebhookConfiguration{
+		Webhooks: []v1.ValidatingWebhook{},
+	}
+	configuration.Name = w.opts.ConfigurationName
+
+	for _, webhook := range w.hooks {
+		equivalent := v1.Equivalent
+		webhook.MatchPolicy = &equivalent
+		webhook.AdmissionReviewVersions = []string{"v1", "v1beta1"}
+		webhook.ClientConfig = v1.WebhookClientConfig{
+			Service: &v1.ServiceReference{
+				Namespace: w.opts.Namespace,
+				Name:      w.opts.ServiceName,
+				Path:      createWebhookPath(IWebhookConfig(webhook)),
+			},
+			CABundle: w.opts.CABundle,
+		}
+
+		log.Infof("Add '%s' path to '%s'", *webhook.ClientConfig.Service.Path, w.opts.ConfigurationName)
+
+		configuration.Webhooks = append(configuration.Webhooks, *webhook.ValidatingWebhook)
+	}
+
+	return w.submit(configuration)
+}
+
+func (w *ValidatingWebhookResource) Unregister() error {
+	return w.opts.KubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().
+		Delete(context.TODO(), w.opts.ConfigurationName, metav1.DeleteOptions{})
+}
+
+func createWebhookPath(webhook IWebhookConfig) *string {
+	s := new(strings.Builder)
+
+	s.WriteString("/")
+	if webhook.GetMeta().ConfigurationId == "" {
+		s.WriteString(DefaultConfigurationId)
+	} else {
+		s.WriteString(webhook.GetMeta().ConfigurationId)
+	}
+
+	s.WriteString("/")
+	s.WriteString(webhook.GetMeta().WebhookId)
+
+	res := s.String()
+	return &res
+}
+
+func (w *ValidatingWebhookResource) submit(conf *v1.ValidatingWebhookConfiguration) error {
+	client := w.opts.KubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations()
+
+	listOpts := metav1.ListOptions{
+		FieldSelector: "metadata.name=" + conf.Name,
+	}
+	list, err := client.List(context.TODO(), listOpts)
+	if err != nil {
+		return err
+	}
+	if len(list.Items) == 0 {
+		_, err = client.Create(context.TODO(), conf, metav1.CreateOptions{})
+		if err != nil {
+			log.Errorf("Create ValidatingWebhookConfiguration/%s: %v", conf.Name, err)
+		}
+	} else {
+		newConf := list.Items[0]
+		newConf.Webhooks = conf.Webhooks
+		_, err = client.Update(context.TODO(), &newConf, metav1.UpdateOptions{})
+		if err != nil {
+			log.Errorf("Replace ValidatingWebhookConfiguration/%s: %v", conf.Name, err)
+		}
+	}
+	return nil
+}
+
+type MutatingWebhookResource struct {
+	hooks map[string]*MutatingWebhookConfig
+	opts  WebhookResourceOptions
+}
+
+func NewMutatingWebhookResource(opts WebhookResourceOptions) *MutatingWebhookResource {
+	return &MutatingWebhookResource{
+		hooks: make(map[string]*MutatingWebhookConfig),
+		opts:  opts,
+	}
+}
+
+func (w *MutatingWebhookResource) Set(whc *MutatingWebhookConfig) {
+	w.hooks[whc.Metadata.WebhookId] = whc
+}
+
+func (w *MutatingWebhookResource) Get(id string) *MutatingWebhookConfig {
+	return w.hooks[id]
+}
+
+func (w *MutatingWebhookResource) Register() error {
+
+	configuration := &v1.MutatingWebhookConfiguration{
+		Webhooks: []v1.MutatingWebhook{},
+	}
+	configuration.Name = w.opts.ConfigurationName
+
+	for _, webhook := range w.hooks {
+		equivalent := v1.Equivalent
+		webhook.MatchPolicy = &equivalent
+		webhook.AdmissionReviewVersions = []string{"v1", "v1beta1"}
+		webhook.ClientConfig = v1.WebhookClientConfig{
+			Service: &v1.ServiceReference{
+				Namespace: w.opts.Namespace,
+				Name:      w.opts.ServiceName,
+				Path:      createWebhookPath(IWebhookConfig(webhook)),
+			},
+			CABundle: w.opts.CABundle,
+		}
+
+		log.Infof("Add '%s' path to '%s'", *webhook.ClientConfig.Service.Path, w.opts.ConfigurationName)
+
+		configuration.Webhooks = append(configuration.Webhooks, *webhook.MutatingWebhook)
+	}
+
+	return w.submit(configuration)
+}
+
+func (w *MutatingWebhookResource) Unregister() error {
+	return w.opts.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().
+		Delete(context.TODO(), w.opts.ConfigurationName, metav1.DeleteOptions{})
+}
+
+func (w *MutatingWebhookResource) submit(conf *v1.MutatingWebhookConfiguration) error {
+	client := w.opts.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations()
+
+	listOpts := metav1.ListOptions{
+		FieldSelector: "metadata.name=" + conf.Name,
+	}
+	list, err := client.List(context.TODO(), listOpts)
+	if err != nil {
+		return err
+	}
+	if len(list.Items) == 0 {
+		_, err = client.Create(context.TODO(), conf, metav1.CreateOptions{})
+		if err != nil {
+			log.Errorf("Create MutatingWebhookConfiguration/%s: %v", conf.Name, err)
+		}
+	} else {
+		newConf := list.Items[0]
+		newConf.Webhooks = conf.Webhooks
+		_, err = client.Update(context.TODO(), &newConf, metav1.UpdateOptions{})
+		if err != nil {
+			log.Errorf("Replace MutatingWebhookConfiguration/%s: %v", conf.Name, err)
+		}
+	}
+	return nil
+}
diff --git a/pkg/webhook/validating/settings.go b/pkg/webhook/admission/settings.go
similarity index 90%
rename from pkg/webhook/validating/settings.go
rename to pkg/webhook/admission/settings.go
index f0eb17d..d9164db 100644
--- a/pkg/webhook/validating/settings.go
+++ b/pkg/webhook/admission/settings.go
@@ -1,4 +1,4 @@
-package validating
+package admission
 
 import "github.com/flant/shell-operator/pkg/webhook/server"
 
diff --git a/pkg/webhook/validating/testdata/demo-certs/ca.pem b/pkg/webhook/admission/testdata/demo-certs/ca.pem
similarity index 100%
rename from pkg/webhook/validating/testdata/demo-certs/ca.pem
rename to pkg/webhook/admission/testdata/demo-certs/ca.pem
diff --git a/pkg/webhook/validating/testdata/demo-certs/client-ca.pem b/pkg/webhook/admission/testdata/demo-certs/client-ca.pem
similarity index 100%
rename from pkg/webhook/validating/testdata/demo-certs/client-ca.pem
rename to pkg/webhook/admission/testdata/demo-certs/client-ca.pem
diff --git a/pkg/webhook/validating/testdata/demo-certs/server-key.pem b/pkg/webhook/admission/testdata/demo-certs/server-key.pem
similarity index 100%
rename from pkg/webhook/validating/testdata/demo-certs/server-key.pem
rename to pkg/webhook/admission/testdata/demo-certs/server-key.pem
diff --git a/pkg/webhook/validating/testdata/demo-certs/server.crt b/pkg/webhook/admission/testdata/demo-certs/server.crt
similarity index 100%
rename from pkg/webhook/validating/testdata/demo-certs/server.crt
rename to pkg/webhook/admission/testdata/demo-certs/server.crt
diff --git a/pkg/webhook/validating/types/event.go b/pkg/webhook/admission/types/event.go
similarity index 73%
rename from pkg/webhook/validating/types/event.go
rename to pkg/webhook/admission/types/event.go
index 45b9c7f..a389098 100644
--- a/pkg/webhook/validating/types/event.go
+++ b/pkg/webhook/admission/types/event.go
@@ -4,8 +4,9 @@ import (
 	v1 "k8s.io/api/admission/v1"
 )
 
-type ValidatingEvent struct {
+type AdmissionEvent struct {
 	WebhookId       string
 	ConfigurationId string
 	Review          *v1.AdmissionReview
+	Binding         string
 }
diff --git a/pkg/webhook/validating/types/response.go b/pkg/webhook/admission/types/response.go
similarity index 58%
rename from pkg/webhook/validating/types/response.go
rename to pkg/webhook/admission/types/response.go
index f4f68d6..6915127 100644
--- a/pkg/webhook/validating/types/response.go
+++ b/pkg/webhook/admission/types/response.go
@@ -5,19 +5,20 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"os"
+	"io/ioutil"
 	"strconv"
 	"strings"
 )
 
-type ValidatingResponse struct {
+type AdmissionResponse struct {
 	Allowed  bool     `json:"allowed"`
 	Message  string   `json:"message,omitempty"`
 	Warnings []string `json:"warnings,omitempty"`
+	Patch    []byte   `json:"patch,omitempty"`
 }
 
-func ValidatingResponseFromFile(filePath string) (*ValidatingResponse, error) {
-	data, err := os.ReadFile(filePath)
+func AdmissionResponseFromFile(filePath string) (*AdmissionResponse, error) {
+	data, err := ioutil.ReadFile(filePath)
 	if err != nil {
 		return nil, fmt.Errorf("cannot read %s: %s", filePath, err)
 	}
@@ -25,28 +26,26 @@ func ValidatingResponseFromFile(filePath string) (*ValidatingResponse, error) {
 	if len(data) == 0 {
 		return nil, nil
 	}
-	return ValidatingResponseFromBytes(data)
+
+	return AdmissionResponseFromBytes(data)
 }
 
-func ValidatingResponseFromBytes(data []byte) (*ValidatingResponse, error) {
-	return ValidatingResponseFromReader(bytes.NewReader(data))
+func AdmissionResponseFromBytes(data []byte) (*AdmissionResponse, error) {
+	return FromReader(bytes.NewReader(data))
 }
 
-func ValidatingResponseFromReader(r io.Reader) (*ValidatingResponse, error) {
-	response := new(ValidatingResponse)
+func FromReader(r io.Reader) (*AdmissionResponse, error) {
+	response := new(AdmissionResponse)
 
 	dec := json.NewDecoder(r)
-
-	err := dec.Decode(response)
-
-	if err != nil {
+	if err := dec.Decode(response); err != nil {
 		return nil, err
 	}
 
 	return response, nil
 }
 
-func (r *ValidatingResponse) Dump() string {
+func (r *AdmissionResponse) Dump() string {
 	b := new(strings.Builder)
 	b.WriteString("ValidatingResponse(allowed=")
 	b.WriteString(strconv.FormatBool(r.Allowed))
diff --git a/pkg/webhook/validating/types/response_test.go b/pkg/webhook/admission/types/response_test.go.txt
similarity index 100%
rename from pkg/webhook/validating/types/response_test.go
rename to pkg/webhook/admission/types/response_test.go.txt
diff --git a/pkg/webhook/validating/types/testdata/response/good_allow.json b/pkg/webhook/admission/types/testdata/good_allow.json
similarity index 100%
rename from pkg/webhook/validating/types/testdata/response/good_allow.json
rename to pkg/webhook/admission/types/testdata/good_allow.json
diff --git a/pkg/webhook/validating/types/testdata/response/good_allow_warnings.json b/pkg/webhook/admission/types/testdata/good_allow_warnings.json
similarity index 100%
rename from pkg/webhook/validating/types/testdata/response/good_allow_warnings.json
rename to pkg/webhook/admission/types/testdata/good_allow_warnings.json
diff --git a/pkg/webhook/validating/types/testdata/response/good_deny.json b/pkg/webhook/admission/types/testdata/good_deny.json
similarity index 100%
rename from pkg/webhook/validating/types/testdata/response/good_deny.json
rename to pkg/webhook/admission/types/testdata/good_deny.json
diff --git a/pkg/webhook/validating/types/testdata/response/good_deny_quiet.json b/pkg/webhook/admission/types/testdata/good_deny_quiet.json
similarity index 100%
rename from pkg/webhook/validating/types/testdata/response/good_deny_quiet.json
rename to pkg/webhook/admission/types/testdata/good_deny_quiet.json
diff --git a/pkg/webhook/validating/config.go b/pkg/webhook/validating/config.go
deleted file mode 100644
index 2913f61..0000000
--- a/pkg/webhook/validating/config.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package validating
-
-import (
-	"github.com/flant/shell-operator/pkg/utils/string_helper"
-	v1 "k8s.io/api/admissionregistration/v1"
-)
-
-// ConfigurationId is a first element in Path field for each Webhook.
-// It should be url safe.
-//
-// WebhookId is a second element for Path field.
-
-// ValidatingWebhookConfig
-type ValidatingWebhookConfig struct {
-	*v1.ValidatingWebhook
-	Metadata struct {
-		Name            string
-		WebhookId       string
-		ConfigurationId string // A suffix to create different ValidatingWebhookConfiguration resources.
-		DebugName       string
-		LogLabels       map[string]string
-		MetricLabels    map[string]string
-	}
-}
-
-// UpdateIds use confId and webhookId to set a ConfigurationId prefix and a WebhookId.
-func (c *ValidatingWebhookConfig) UpdateIds(confID, webhookID string) {
-	c.Metadata.ConfigurationId = confID
-	if confID == "" {
-		c.Metadata.ConfigurationId = DefaultConfigurationId
-	}
-	c.Metadata.WebhookId = string_helper.SafeURLString(webhookID)
-}
diff --git a/pkg/webhook/validating/manager.go b/pkg/webhook/validating/manager.go
deleted file mode 100644
index b930043..0000000
--- a/pkg/webhook/validating/manager.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package validating
-
-import (
-	"os"
-
-	log "github.com/sirupsen/logrus"
-
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
-
-	klient "github.com/flant/kube-client/client"
-	"github.com/flant/shell-operator/pkg/webhook/server"
-)
-
-type ValidatingEventHandlerFn func(event ValidatingEvent) (*ValidatingResponse, error)
-
-// DefaultConfigurationId is a ConfigurationId for ValidatingWebhookConfiguration
-// without suffix.
-const DefaultConfigurationId = "hooks"
-
-// WebhookManager is a public interface to be used from operator.go.
-//
-// No dynamic configuration for now. The steps are:
-//   - Init manager
-//   - Call AddWEbhook for every binding in hooks
-//   - Start() to run server and create ValidatingWebhookConfiguration
-type WebhookManager struct {
-	KubeClient klient.Client
-
-	ValidatingEventHandlerFn ValidatingEventHandlerFn
-
-	Settings  *WebhookSettings
-	Namespace string
-
-	DefaultConfigurationId string
-
-	Server    *server.WebhookServer
-	Resources map[string]*WebhookResource
-	Handler   *WebhookHandler
-}
-
-func NewWebhookManager() *WebhookManager {
-	return &WebhookManager{
-		Resources: make(map[string]*WebhookResource),
-	}
-}
-
-func (m *WebhookManager) WithKubeClient(kubeClient klient.Client) {
-	m.KubeClient = kubeClient
-}
-
-func (m *WebhookManager) WithValidatingEventHandler(handler ValidatingEventHandlerFn) {
-	m.ValidatingEventHandlerFn = handler
-}
-
-// Init creates dependencies
-func (m *WebhookManager) Init() error {
-	log.Info("Initialize validating webhooks manager. Load certificates.")
-
-	if m.DefaultConfigurationId == "" {
-		m.DefaultConfigurationId = DefaultConfigurationId
-	}
-	// settings
-	caBundleBytes, err := os.ReadFile(m.Settings.CAPath)
-	if err != nil {
-		return err
-	}
-	m.Settings.CABundle = caBundleBytes
-
-	m.Handler = NewWebhookHandler()
-	m.Handler.Manager = m
-
-	m.Server = &server.WebhookServer{
-		Settings:  &m.Settings.Settings,
-		Namespace: m.Namespace,
-		Router:    m.Handler.Router,
-	}
-
-	m.Resources = make(map[string]*WebhookResource)
-	r := NewWebhookResource()
-	r.KubeClient = m.KubeClient
-	r.Namespace = m.Namespace
-	r.ConfigurationName = m.Settings.ConfigurationName
-	r.ServiceName = m.Settings.ServiceName
-	r.CABundle = m.Settings.CABundle
-	m.Resources[m.DefaultConfigurationId] = r
-
-	return nil
-}
-
-func (m *WebhookManager) AddWebhook(config *ValidatingWebhookConfig) {
-	confId := config.Metadata.ConfigurationId
-	if confId == "" {
-		confId = m.DefaultConfigurationId
-	}
-	r, ok := m.Resources[confId]
-	if !ok {
-		r = NewWebhookResource()
-		r.KubeClient = m.KubeClient
-		r.Namespace = m.Namespace
-		r.ConfigurationName = m.Settings.ConfigurationName + "-" + confId
-		r.ServiceName = m.Settings.ServiceName
-		r.CABundle = m.Settings.CABundle
-		m.Resources[confId] = r
-	}
-	r.AddWebhook(config)
-}
-
-func (m *WebhookManager) Start() error {
-	err := m.Server.Start()
-	if err != nil {
-		return err
-	}
-
-	for _, r := range m.Resources {
-		err = r.CreateConfiguration()
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
diff --git a/pkg/webhook/validating/resource.go b/pkg/webhook/validating/resource.go
deleted file mode 100644
index 6393441..0000000
--- a/pkg/webhook/validating/resource.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package validating
-
-import (
-	"context"
-	"strings"
-
-	log "github.com/sirupsen/logrus"
-
-	v1 "k8s.io/api/admissionregistration/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	klient "github.com/flant/kube-client/client"
-)
-
-type WebhookResource struct {
-	KubeClient        klient.Client
-	Webhooks          map[string]*ValidatingWebhookConfig
-	Namespace         string
-	ConfigurationName string
-	ServiceName       string
-	CABundle          []byte
-}
-
-func NewWebhookResource() *WebhookResource {
-	return &WebhookResource{
-		Webhooks: make(map[string]*ValidatingWebhookConfig),
-	}
-}
-
-func (w *WebhookResource) AddWebhook(config *ValidatingWebhookConfig) {
-	w.Webhooks[config.Metadata.WebhookId] = config
-}
-
-func (w *WebhookResource) CreateConfiguration() error {
-	equivalent := v1.Equivalent
-
-	configuration := &v1.ValidatingWebhookConfiguration{
-		Webhooks: []v1.ValidatingWebhook{},
-	}
-	configuration.Name = w.ConfigurationName
-
-	for _, webhook := range w.Webhooks {
-		webhook.MatchPolicy = &equivalent
-		webhook.AdmissionReviewVersions = []string{"v1", "v1beta1"}
-		webhook.ClientConfig = v1.WebhookClientConfig{
-			Service: &v1.ServiceReference{
-				Namespace: w.Namespace,
-				Name:      w.ServiceName,
-				Path:      w.CreateWebhookPath(webhook),
-			},
-			CABundle: w.CABundle,
-		}
-
-		log.Infof("Add '%s' path to '%s'", *webhook.ClientConfig.Service.Path, w.ConfigurationName)
-
-		configuration.Webhooks = append(configuration.Webhooks, *webhook.ValidatingWebhook)
-	}
-
-	return w.CreateOrUpdateConfiguration(configuration)
-}
-
-func (w *WebhookResource) UpdateConfiguration() error {
-	return nil
-}
-
-func (w *WebhookResource) DeleteConfiguration() error {
-	return w.KubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().
-		Delete(context.TODO(), w.ConfigurationName, metav1.DeleteOptions{})
-}
-
-func (w *WebhookResource) CreateWebhookPath(webhook *ValidatingWebhookConfig) *string {
-	s := new(strings.Builder)
-
-	s.WriteString("/")
-	if webhook.Metadata.ConfigurationId == "" {
-		s.WriteString(DefaultConfigurationId)
-	} else {
-		s.WriteString(webhook.Metadata.ConfigurationId)
-	}
-
-	s.WriteString("/")
-	s.WriteString(webhook.Metadata.WebhookId)
-
-	res := s.String()
-	return &res
-}
-
-func (w *WebhookResource) CreateOrUpdateConfiguration(conf *v1.ValidatingWebhookConfiguration) error {
-	client := w.KubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations()
-
-	listOpts := metav1.ListOptions{
-		FieldSelector: "metadata.name=" + conf.Name,
-	}
-	list, err := client.List(context.TODO(), listOpts)
-	if err != nil {
-		return err
-	}
-	if len(list.Items) == 0 {
-		_, err = client.Create(context.TODO(), conf, metav1.CreateOptions{})
-		if err != nil {
-			log.Errorf("Create ValidatingWebhookConfiguration/%s: %v", conf.Name, err)
-		}
-	} else {
-		newConf := list.Items[0]
-		newConf.Webhooks = conf.Webhooks
-		_, err = client.Update(context.TODO(), &newConf, metav1.UpdateOptions{})
-		if err != nil {
-			log.Errorf("Replace ValidatingWebhookConfiguration/%s: %v", conf.Name, err)
-		}
-	}
-	return nil
-}


Test patch:
diff --git a/pkg/hook/hook_manager_test.go b/pkg/hook/hook_manager_test.go
index e9dd6af..87a6668 100644
--- a/pkg/hook/hook_manager_test.go
+++ b/pkg/hook/hook_manager_test.go
@@ -10,9 +10,9 @@ import (
 	"github.com/flant/shell-operator/pkg/app"
 	"github.com/flant/shell-operator/pkg/hook/controller"
 	"github.com/flant/shell-operator/pkg/hook/types"
+	"github.com/flant/shell-operator/pkg/webhook/admission"
+	. "github.com/flant/shell-operator/pkg/webhook/admission/types"
 	"github.com/flant/shell-operator/pkg/webhook/conversion"
-	"github.com/flant/shell-operator/pkg/webhook/validating"
-	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
 )
 
 func newHookManager(t *testing.T, testdataDir string) *hookManager {
@@ -25,9 +25,9 @@ func newHookManager(t *testing.T, testdataDir string) *hookManager {
 	conversionManager.Settings = app.ConversionWebhookSettings
 	hm.WithConversionWebhookManager(conversionManager)
 
-	validatingManager := validating.NewWebhookManager()
+	validatingManager := admission.NewWebhookManager()
 	validatingManager.Settings = app.ValidatingWebhookSettings
-	hm.WithValidatingWebhookManager(validatingManager)
+	hm.WithAdmissionWebhookManager(validatingManager)
 
 	return hm
 }
@@ -87,21 +87,21 @@ func TestHookController_HandleValidatingEvent(t *testing.T) {
 		t.Fatalf("Hook manager Init should not fail: %v", err)
 	}
 
-	ev := ValidatingEvent{
+	ev := AdmissionEvent{
 		WebhookId:       "test-policy-example-com",
 		ConfigurationId: "hooks",
 		Review:          nil,
 	}
 
 	h := hm.GetHook("hook.sh")
-	h.HookController.EnableValidatingBindings()
+	h.HookController.EnableAdmissionBindings()
 
-	canHandle := h.HookController.CanHandleValidatingEvent(ev)
+	canHandle := h.HookController.CanHandleAdmissionEvent(ev)
 
 	g.Expect(canHandle).To(BeTrue())
 
 	var infoList []controller.BindingExecutionInfo
-	h.HookController.HandleValidatingEvent(ev, func(info controller.BindingExecutionInfo) {
+	h.HookController.HandleAdmissionEvent(ev, func(info controller.BindingExecutionInfo) {
 		infoList = append(infoList, info)
 	})
 
diff --git a/pkg/webhook/validating/handler_test.go b/pkg/webhook/admission/handler_test.go
similarity index 97%
rename from pkg/webhook/validating/handler_test.go
rename to pkg/webhook/admission/handler_test.go
index b02ab7e..ca2893b 100644
--- a/pkg/webhook/validating/handler_test.go
+++ b/pkg/webhook/admission/handler_test.go
@@ -1,4 +1,4 @@
-package validating
+package admission
 
 import "testing"
 
diff --git a/pkg/webhook/validating/manager_test.go b/pkg/webhook/admission/manager_test.go
similarity index 84%
rename from pkg/webhook/validating/manager_test.go
rename to pkg/webhook/admission/manager_test.go
index 1671f74..d55c5ea 100644
--- a/pkg/webhook/validating/manager_test.go
+++ b/pkg/webhook/admission/manager_test.go
@@ -1,4 +1,4 @@
-package validating
+package admission
 
 import (
 	"testing"
@@ -45,15 +45,15 @@ func Test_Manager_AddWebhook(t *testing.T) {
 			TimeoutSeconds: &timeoutSeconds,
 		},
 	}
-	m.AddWebhook(cfg)
+	m.AddValidatingWebhook(cfg)
 
-	if len(m.Resources) != 1 {
-		t.Fatalf("WebhookManager should have resources: got length %d", len(m.Resources))
+	if len(m.ValidatingResources) != 1 {
+		t.Fatalf("WebhookManager should have resources: got length %d", len(m.ValidatingResources))
 	}
 
-	for k, v := range m.Resources {
-		if len(v.Webhooks) != 1 {
-			t.Fatalf("Resource '%s' should have Webhooks: got length %d", k, len(m.Resources))
+	for k, v := range m.ValidatingResources {
+		if len(v.hooks) != 1 {
+			t.Fatalf("Resource '%s' should have Webhooks: got length %d", k, len(m.ValidatingResources))
 		}
 	}
 }