Repository: flant/shell-operator
Pull request #457 (2023-01-30 17:00:46)

chore(deps): bump actions/cache from 3.2.1 to 3.2.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.1 to 3.2.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/cache/releases">actions/cache's releases</a>.</em></p>
<blockquote>
<h2>v3.2.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Update json5 package version by <a href="https://github.com/vsvipul"><code>@​vsvipul</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1065">actions/cache#1065</a></li>
<li>Cache recipes for cache, restore and save actions by <a href="https://github.com/kotewar"><code>@​kotewar</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1055">actions/cache#1055</a></li>
<li>Add gnu tar and zstd as pre-requisites for windows self-hosted runners by <a href="https://github.com/pdotl"><code>@​pdotl</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1068">actions/cache#1068</a></li>
<li>Fix a whitespace typo by <a href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1074">actions/cache#1074</a></li>
<li>:memo: <a href="https://github-redirect.dependabot.com/actions/cache/issues/1045">#1045</a> update using the <code>set-output</code> command is deprecated by <a href="https://github.com/siguikesse"><code>@​siguikesse</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1046">actions/cache#1046</a></li>
<li>Fix referenced output key in save action readme by <a href="https://github.com/ruudk"><code>@​ruudk</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1061">actions/cache#1061</a></li>
<li>Update workflows to use reusable-workflows by <a href="https://github.com/jongwooo"><code>@​jongwooo</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1066">actions/cache#1066</a></li>
<li>Introduce add-to-project step &amp; rename workflow files by <a href="https://github.com/pallavx"><code>@​pallavx</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1077">actions/cache#1077</a></li>
<li>chore: Fix syntax error typo by <a href="https://github.com/vHeemstra"><code>@​vHeemstra</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1081">actions/cache#1081</a></li>
<li>Update caching-strategies.md by <a href="https://github.com/kpfleming"><code>@​kpfleming</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1084">actions/cache#1084</a></li>
<li>Added another usage hint to foresee <a href="https://github-redirect.dependabot.com/actions/cache/issues/1072">#1072</a> by <a href="https://github.com/maybeec"><code>@​maybeec</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1089">actions/cache#1089</a></li>
<li>Add <code>fail-on-cache-miss</code> option by <a href="https://github.com/cdce8p"><code>@​cdce8p</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1036">actions/cache#1036</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/kurtmckee"><code>@​kurtmckee</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1074">actions/cache#1074</a></li>
<li><a href="https://github.com/siguikesse"><code>@​siguikesse</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1046">actions/cache#1046</a></li>
<li><a href="https://github.com/ruudk"><code>@​ruudk</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1061">actions/cache#1061</a></li>
<li><a href="https://github.com/pallavx"><code>@​pallavx</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1077">actions/cache#1077</a></li>
<li><a href="https://github.com/vHeemstra"><code>@​vHeemstra</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1081">actions/cache#1081</a></li>
<li><a href="https://github.com/kpfleming"><code>@​kpfleming</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1084">actions/cache#1084</a></li>
<li><a href="https://github.com/maybeec"><code>@​maybeec</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1089">actions/cache#1089</a></li>
<li><a href="https://github.com/cdce8p"><code>@​cdce8p</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1036">actions/cache#1036</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v3...v3.2.4">https://github.com/actions/cache/compare/v3...v3.2.4</a></p>
<h2>v3.2.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Add Mint example by <a href="https://github.com/uhooi"><code>@​uhooi</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1051">actions/cache#1051</a></li>
<li>Fixed broken link by <a href="https://github.com/kotewar"><code>@​kotewar</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1057">actions/cache#1057</a></li>
<li>Add support to opt-in enable cross-os caching on windows by <a href="https://github.com/Phantsure"><code>@​Phantsure</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1056">actions/cache#1056</a></li>
<li>Release support for cross-os caching as opt-in feature by <a href="https://github.com/Phantsure"><code>@​Phantsure</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1060">actions/cache#1060</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/uhooi"><code>@​uhooi</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1051">actions/cache#1051</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v3...v3.2.3">https://github.com/actions/cache/compare/v3...v3.2.3</a></p>
<h2>v3.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix formatting error in restore/README.md by <a href="https://github.com/me-and"><code>@​me-and</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1044">actions/cache#1044</a></li>
<li>save/README.md: Fix typo in example by <a href="https://github.com/mmuetzel"><code>@​mmuetzel</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1040">actions/cache#1040</a></li>
<li>README.md: remove outdated Windows cache tip link by <a href="https://github.com/me-and"><code>@​me-and</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1042">actions/cache#1042</a></li>
<li>Revert compression changes related to windows but keep version logging by <a href="https://github.com/Phantsure"><code>@​Phantsure</code></a> in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1049">actions/cache#1049</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/me-and"><code>@​me-and</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1044">actions/cache#1044</a></li>
<li><a href="https://github.com/mmuetzel"><code>@​mmuetzel</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/actions/cache/pull/1040">actions/cache#1040</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/actions/cache/blob/main/RELEASES.md">actions/cache's changelog</a>.</em></p>
<blockquote>
<h3>3.2.1</h3>
<ul>
<li>Update <code>@actions/cache</code> on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. (<a href="https://github-redirect.dependabot.com/actions/cache/issues/984">issue</a>)</li>
<li>Added support for fallback to gzip to restore old caches on windows.</li>
<li>Added logs for cache version in case of a cache miss.</li>
</ul>
<h3>3.2.2</h3>
<ul>
<li>Reverted the changes made in 3.2.1 to use gnu tar and zstd by default on windows.</li>
</ul>
<h3>3.2.3</h3>
<ul>
<li>Support cross os caching on Windows as an opt-in feature.</li>
<li>Fix issue with symlink restoration on Windows for cross-os caches.</li>
</ul>
<h3>3.2.4</h3>
<ul>
<li>Added option to fail job on cache miss.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/cache/commit/627f0f41f6904a5b1efbaed9f96d9eb58e92e920"><code>627f0f4</code></a> Add <code>fail-on-cache-miss</code> option (<a href="https://github-redirect.dependabot.com/actions/cache/issues/1036">#1036</a>)</li>
<li><a href="https://github.com/actions/cache/commit/8e3048d0f72c72babe53747a8f10e16a74c19ba1"><code>8e3048d</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/actions/cache/issues/1089">#1089</a> from maybeec/patch-1</li>
<li><a href="https://github.com/actions/cache/commit/1b004e8a69df75001db3876453d75c84376e630e"><code>1b004e8</code></a> Update tips-and-workarounds.md</li>
<li><a href="https://github.com/actions/cache/commit/75b110bc854cb272b33517e4c2b7dfb7e3eac2d1"><code>75b110b</code></a> Added another hint to foresee <a href="https://github-redirect.dependabot.com/actions/cache/issues/1072">#1072</a></li>
<li><a href="https://github.com/actions/cache/commit/2b5a782c6414f96354f95902141714b127692d1e"><code>2b5a782</code></a> Update caching-strategies.md (<a href="https://github-redirect.dependabot.com/actions/cache/issues/1084">#1084</a>)</li>
<li><a href="https://github.com/actions/cache/commit/6c2de3ba980384b3ac07a50cbf1fc6e7bc55ff37"><code>6c2de3b</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/actions/cache/issues/1081">#1081</a> from vHeemstra/patch-1</li>
<li><a href="https://github.com/actions/cache/commit/b63536828e6e5754df3059152c88daad28950ed4"><code>b635368</code></a> there as well ;)</li>
<li><a href="https://github.com/actions/cache/commit/cd2aaa9df8838bb988aa3c08613070efa898129b"><code>cd2aaa9</code></a> chore: Fix syntax error typo</li>
<li><a href="https://github.com/actions/cache/commit/9b7ef12f3e9ce81a867de9cc54829421aa406d7b"><code>9b7ef12</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/actions/cache/issues/1077">#1077</a> from actions/pallavx-patch-1</li>
<li><a href="https://github.com/actions/cache/commit/3c08cab74fe64645cd02a69a0d4c14f1791c9051"><code>3c08cab</code></a> Introduce add-to-project step &amp; rename add-reviewer-pr workflow file</li>
<li>Additional commits viewable in <a href="https://github.com/actions/cache/compare/v3.2.1...v3.2.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=3.2.1&new-version=3.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 1e1ceca..2bd87bc 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -18,13 +18,11 @@ jobs:
 
       - name: Restore Go modules
         id: go-modules-cache
-        uses: actions/cache@v3.2.1
+        uses: actions/cache@v3.2.4
         with:
           path: |
             ~/go/pkg/mod
           key: ${{ runner.os }}-gomod-${{ hashFiles('go.mod', 'go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-gomod-
 
       - name: Download Go modules
         if: steps.go-modules-cache.outputs.cache-hit != 'true'
@@ -43,7 +41,7 @@ jobs:
           export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/libjq/lib"
           export GOOS=linux
 
-          go build ./cmd/shell-operator
+          go build -tags use_libjq ./cmd/shell-operator
 
 # MacOS build works fine because jq package already has static libraries.
 # Windows build requires jq compilation, this should be done in libjq-go.
diff --git a/.github/workflows/code-checks.yaml b/.github/workflows/code-checks.yaml
deleted file mode 100644
index f6b1bf7..0000000
--- a/.github/workflows/code-checks.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-# every push to a branch:
-# - check grammar
-# - go fmt
-name: Code checks
-on:
-  pull_request:
-    types: [opened, synchronize]
-
-jobs:
-  code_style:
-    name: Go code style
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Go 1.19
-        uses: actions/setup-go@v3
-        with:
-          go-version: 1.19
-
-      - name: Check out shell-operator code
-        uses: actions/checkout@v3
-
-      - name: Ensure code is gofmted
-        run: |
-          gofmt -d $(find . -type f -iname '*.go')
-
-  codespell:
-    name: codespell
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Go 1.19
-        uses: actions/setup-go@v3
-        with:
-          go-version: 1.19
-
-      - uses: actions/setup-python@v4
-        with:
-          python-version: 3.8
-
-      - name: Check out shell-operator code
-        uses: actions/checkout@v3
-
-      - name: codespell
-        run: |
-          pip install codespell==v1.17.1
-          codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png,*.md" -L witht,eventtypes,uint,uptodate,keypair
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 8879803..fa116d6 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -20,7 +20,7 @@ jobs:
 
       - name: Restore Go modules
         id: go-modules-cache
-        uses: actions/cache@v3.2.1
+        uses: actions/cache@v3.2.4
         with:
           path: |
             ~/go/pkg/mod
@@ -34,16 +34,24 @@ jobs:
           go mod download
           echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod
 
-      - name: Download prebuilt libjq static libraries
-        run: |
-          curl -sSfL https://github.com/flant/libjq-go/releases/download/jq-b6be13d5-0/libjq-glibc-amd64.tgz | tar zxf -
-
       - name: Run golangci-lint
         run: |
-          export CGO_ENABLED=1
-          export CGO_CFLAGS="-I$GITHUB_WORKSPACE/libjq/include"
-          export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/libjq/lib"
-          export GOOS=linux
-
           curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b . v1.50.1
-          ./golangci-lint run --sort-results --timeout 5m --build-tags integration,test
+          ./golangci-lint run --sort-results --build-tags integration,test
+
+
+  codespell:
+    name: Run codespell
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.8
+
+      - name: Check out addon-operator code
+        uses: actions/checkout@v3
+
+      - name: Run codespell
+        run: |
+          pip install codespell==v1.17.1
+          codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png,*.md" -L witht,eventtypes,uint,uptodate,afterall,keypair
diff --git a/.golangci.yaml b/.golangci.yaml
index 1806f69..6de57a1 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,11 +1,12 @@
 run:
   timeout: 15m
 linters:
-  include:
-    - bodyclose
+  enable:
     - asciicheck
+    - bodyclose
     - dogsled
     - durationcheck
+    - gofumpt
 issues:
   max-issues-per-linter: 0
   max-same-issues: 0
diff --git a/examples/206-mutating-webhook/.dockerignore b/examples/206-mutating-webhook/.dockerignore
new file mode 100644
index 0000000..d110fe5
--- /dev/null
+++ b/examples/206-mutating-webhook/.dockerignore
@@ -0,0 +1,6 @@
+templates
+.helmignore
+Chart.yaml
+crontab*.yaml
+README.md
+values.yaml
diff --git a/examples/206-mutating-webhook/.helmignore b/examples/206-mutating-webhook/.helmignore
new file mode 100644
index 0000000..33da9e6
--- /dev/null
+++ b/examples/206-mutating-webhook/.helmignore
@@ -0,0 +1,6 @@
+crontab-valid.yaml
+crontab-non-valid.yaml
+Dockerfile
+README.md
+gen-certs.sh
+shell-operator
diff --git a/examples/206-mutating-webhook/Chart.yaml b/examples/206-mutating-webhook/Chart.yaml
new file mode 100644
index 0000000..3e41f8f
--- /dev/null
+++ b/examples/206-mutating-webhook/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+name: 206-mutating-webhook
+version: 1.0.0
+description: Shell-operator mutating hook example
diff --git a/examples/206-mutating-webhook/Dockerfile b/examples/206-mutating-webhook/Dockerfile
new file mode 100644
index 0000000..efdb244
--- /dev/null
+++ b/examples/206-mutating-webhook/Dockerfile
@@ -0,0 +1,5 @@
+FROM flant/shell-operator:latest
+LABEL app="shell-operator" example="206-mutating-webhook"
+
+# Add hooks
+ADD hooks /hooks
diff --git a/examples/206-mutating-webhook/README.md b/examples/206-mutating-webhook/README.md
new file mode 100644
index 0000000..2771f85
--- /dev/null
+++ b/examples/206-mutating-webhook/README.md
@@ -0,0 +1,64 @@
+# Example with mutating hook
+
+This is a simple example of `kubernetesMutating` binding. Read more information in [BINDING_MUTATING.md](../../BINDING_MUTATING.md).
+
+The example contains one hook that is used to mutate custom resource [CronTab](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) on create or update (see also [105-crd-example](../105-crd-example/README.md). The hook just updates a `spec.replicas` field to value 333.
+
+## Run
+
+### Generate certificates
+
+An HTTP server behind the MutatingWebhookConfiguration requires a certificate issued by the CA. For simplicity, this process is automated with `gen-certs.sh` script. Just run it:
+
+```
+./gen-certs.sh
+```
+
+> Note: `gen-certs.sh` requires [cfssl utility](https://github.com/cloudflare/cfssl/releases/latest).
+
+### Build and install example
+
+Build Docker image and use helm3 to install it:
+
+```
+docker build -t localhost:5000/shell-operator:example-206 .
+docker push localhost:5000/shell-operator:example-206
+helm upgrade --install \
+    --namespace example-206 \
+    --create-namespace \
+    example-206 .
+```
+
+### See mutating hook in action
+
+Create a CronTab resource:
+
+```
+$ kubectl -n example-206 apply -f crontab-valid.yaml
+crontab.stable.example.com/crontab-valid created
+```
+
+Check CronTab manifest:
+
+```
+$ kubectl -n example-206 get crontab -o yaml
+...
+apiVersion: stable.example.com/v1
+kind: CronTab
+metadata:
+  name: crontab-valid
+  ...
+spec:
+  cronSpec: '* * * * */5'    <--- It is from the YAML file.
+  image: repo.example.com/my-awesome-cron-image:v1    <--- It is from the YAML file.
+  replicas: 333  <--- This one is set by the hook.
+...
+```
+
+### Cleanup
+
+```
+helm delete --namespace=example-206 example-206
+kubectl delete ns example-206
+kubectl delete mutatingwebhookconfiguration/example-206
+```
diff --git a/examples/206-mutating-webhook/crontab-valid.yaml b/examples/206-mutating-webhook/crontab-valid.yaml
new file mode 100644
index 0000000..cbd03ff
--- /dev/null
+++ b/examples/206-mutating-webhook/crontab-valid.yaml
@@ -0,0 +1,9 @@
+apiVersion: "stable.example.com/v1"
+kind: CronTab
+metadata:
+  name: crontab-valid
+  labels:
+    heritage: example-206
+spec:
+  cronSpec: "* * * * */5"
+  image: repo.example.com/my-awesome-cron-image:v1
diff --git a/examples/206-mutating-webhook/gen-certs.sh b/examples/206-mutating-webhook/gen-certs.sh
new file mode 100755
index 0000000..86ca9f6
--- /dev/null
+++ b/examples/206-mutating-webhook/gen-certs.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+NAMESPACE=example-206
+SERVICE_NAME=example-206-mutating-service
+
+COMMON_NAME=${SERVICE_NAME}.${NAMESPACE}
+
+set -eo pipefail
+
+echo =================================================================
+echo THIS SCRIPT IS NOT SECURE! USE IT ONLY FOR DEMONSTATION PURPOSES.
+echo =================================================================
+echo
+
+mkdir -p certs && cd certs
+
+if [[ -e ca.csr  ]] ; then
+  read -p "Regenerate certificates? (yes/no) [no]: "
+  if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]
+  then
+    exit 0
+  fi
+fi
+
+RM_FILES="ca* cert*"
+echo ">>> Remove ${RM_FILES}"
+rm -f $RM_FILES
+
+echo ">>> Generate CA key and certificate"
+cat <<EOF | cfssl gencert -initca - | cfssljson -bare ca
+{
+  "CN": "Shell-operator example 206-mutating-webhook Root CA",
+  "key": {
+    "algo": "rsa",
+    "size": 2048
+  }
+}
+EOF
+
+
+CFSSL_CONFIG=$(cat <<EOF
+{
+  "signing": {
+    "default": {
+      "expiry": "8760h"
+    },
+    "profiles": {
+      "server": {
+        "usages": [
+          "signing",
+          "digital signing",
+          "key encipherment",
+          "server auth"
+        ],
+        "expiry": "8760h"
+      }
+    }
+  }
+}
+EOF
+)
+
+echo ">>> Generate cert.key and cert.crt"
+cat <<EOF | cfssl gencert -ca ca.pem -ca-key ca-key.pem -config <(echo "$CFSSL_CONFIG") -profile=server - | cfssljson -bare tls
+{
+  "CN": "${COMMON_NAME}.svc",
+  "key": {
+    "algo": "rsa",
+    "size": 2048
+  },
+  "hosts": [
+    "${COMMON_NAME}",
+    "${COMMON_NAME}.svc",
+    "${COMMON_NAME}.svc.cluster.local"
+  ]
+}
+EOF
diff --git a/examples/206-mutating-webhook/hooks/mutating.sh b/examples/206-mutating-webhook/hooks/mutating.sh
new file mode 100755
index 0000000..018e762
--- /dev/null
+++ b/examples/206-mutating-webhook/hooks/mutating.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+source /shell_lib.sh
+
+function __config__(){
+    cat <<EOF
+configVersion: v1
+kubernetesMutating:
+- name: change.crontab.resources
+  namespace:
+    labelSelector:
+      matchLabels:
+        # helm adds a 'name' label to a namespace it creates
+        name: example-206
+  rules:
+  - apiGroups:   ["stable.example.com"]
+    apiVersions: ["v1"]
+    operations:  ["CREATE", "UPDATE"]
+    resources:   ["crontabs"]
+    scope:       "Namespaced"
+EOF
+}
+
+function __main__() {
+  context::jq -r '.review.request'
+#  image=$(context::jq -r '.review.request.object.spec.image')
+#  echo "Got image: $image"
+
+#  if [[ $image == repo.example.com* ]] ; then
+  PATCH=$( echo '[{"op": "add", "path": "/spec/replicas", "value": 333 }]' | base64 )
+    cat <<EOF > $VALIDATING_RESPONSE_PATH
+{"allowed":true, "patch": "$PATCH"}
+EOF
+#  else
+#    cat <<EOF > $VALIDATING_RESPONSE_PATH
+#{"allowed":false, "message":"Only images from repo.example.com are allowed"}
+#EOF
+#  fi
+}
+
+hook::run $@
diff --git a/examples/206-mutating-webhook/templates/certs-secret.yaml b/examples/206-mutating-webhook/templates/certs-secret.yaml
new file mode 100644
index 0000000..c886565
--- /dev/null
+++ b/examples/206-mutating-webhook/templates/certs-secret.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: example-206-certs
+type: kubernetes.io/tls
+data:
+  tls.crt: |
+    {{ .Files.Get "certs/tls.pem" | b64enc }}
+  tls.key: |
+    {{ .Files.Get "certs/tls-key.pem" | b64enc }}
+  ca.crt: |
+    {{ .Files.Get "certs/ca.pem" | b64enc }}
diff --git a/examples/206-mutating-webhook/templates/crd/crontab.yaml b/examples/206-mutating-webhook/templates/crd/crontab.yaml
new file mode 100644
index 0000000..c097da4
--- /dev/null
+++ b/examples/206-mutating-webhook/templates/crd/crontab.yaml
@@ -0,0 +1,46 @@
+---
+# See: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
+#
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  # name must match the spec fields below, and be in the form: <plural>.<group>
+  name: crontabs.stable.example.com
+  labels:
+    heritage: example-206
+spec:
+  # group name to use for REST API: /apis/<group>/<version>
+  group: stable.example.com
+  # list of versions supported by this CustomResourceDefinition
+  versions:
+    - name: v1
+      # Each version can be enabled/disabled by Served flag.
+      served: true
+      # One and only one version must be marked as the storage version.
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          properties:
+            spec:
+              type: object
+              properties:
+                cronSpec:
+                  type: string
+                image:
+                  type: string
+                replicas:
+                  type: integer
+  # either Namespaced or Cluster
+  scope: Namespaced
+  names:
+    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
+    plural: crontabs
+    # singular name to be used as an alias on the CLI and for display
+    singular: crontab
+    # kind is normally the CamelCased singular type. Your resource manifests use this.
+    kind: CronTab
+    # shortNames allow shorter string to match your resource on the CLI
+    shortNames:
+      - ct
diff --git a/examples/206-mutating-webhook/templates/deployment.yaml b/examples/206-mutating-webhook/templates/deployment.yaml
new file mode 100644
index 0000000..9f87971
--- /dev/null
+++ b/examples/206-mutating-webhook/templates/deployment.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: shell-operator
+  labels:
+    heritage: example-206
+    app: shell-operator-example-206
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: shell-operator-example-206
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        heritage: example-206
+        app: shell-operator-example-206
+      annotations:
+        checksum/hook: {{ .Files.Get "hooks/validating.sh" | sha256sum }}
+    spec:
+      containers:
+      - name: shell-operator
+        image: {{ .Values.shellOperator.image | quote }}
+        imagePullPolicy: Always
+        env:
+        - name: SHELL_OPERATOR_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: LOG_LEVEL
+          value: Debug
+        - name: VALIDATING_WEBHOOK_SERVICE_NAME
+          value: {{ .Values.shellOperator.webhookServiceName | quote }}
+        - name: VALIDATING_WEBHOOK_CONFIGURATION_NAME
+          value: {{ .Values.shellOperator.webhookConfigurationName | quote }}
+        volumeMounts:
+        - name: validating-certs
+          mountPath: /validating-certs/
+          readOnly: true
+      serviceAccountName: example-206-acc
+      volumes:
+      - name: validating-certs
+        secret:
+          secretName: example-206-certs
diff --git a/examples/206-mutating-webhook/templates/rbac.yaml b/examples/206-mutating-webhook/templates/rbac.yaml
new file mode 100644
index 0000000..d62204c
--- /dev/null
+++ b/examples/206-mutating-webhook/templates/rbac.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: example-206-acc
+  labels:
+    heritage: example-206
+---
+# Create and update ValidatingWebhookConfiguration
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: example-206
+  labels:
+    heritage: example-206
+rules:
+- apiGroups: ["admissionregistration.k8s.io"]
+  resources: ["mutatingwebhookconfigurations"]
+  verbs: ["create", "list", "update"]
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: example-206
+  labels:
+    heritage: example-206
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: example-206
+subjects:
+  - kind: ServiceAccount
+    name: example-206-acc
+    namespace: {{ .Release.Namespace }}
diff --git a/examples/206-mutating-webhook/templates/webhook-service.yaml b/examples/206-mutating-webhook/templates/webhook-service.yaml
new file mode 100644
index 0000000..759cdd0
--- /dev/null
+++ b/examples/206-mutating-webhook/templates/webhook-service.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Values.shellOperator.webhookServiceName }}
+  labels:
+    heritage: example-206
+spec:
+#  type: LoadBalancer
+#  externalTrafficPolicy: Local
+  ports:
+    - name: webhook
+      port: 443
+      targetPort: 9680
+      protocol: TCP
+  selector:
+    app: shell-operator-example-206
diff --git a/examples/206-mutating-webhook/values.yaml b/examples/206-mutating-webhook/values.yaml
new file mode 100644
index 0000000..da5723e
--- /dev/null
+++ b/examples/206-mutating-webhook/values.yaml
@@ -0,0 +1,5 @@
+shellOperator:
+  image: localhost:5000/shell-operator:example-206
+  # Important: this name should be the same as in gen-certs.sh and csr.json.
+  webhookServiceName: example-206-mutating-service
+  webhookConfigurationName: example-206
diff --git a/frameworks/shell/hook.sh b/frameworks/shell/hook.sh
index 35442d6..0662a31 100644
--- a/frameworks/shell/hook.sh
+++ b/frameworks/shell/hook.sh
@@ -11,14 +11,14 @@ function hook::run() {
     export BINDING_CONTEXT_CURRENT_INDEX="${i}"
     export BINDING_CONTEXT_CURRENT_BINDING=$(context::jq -r '.binding // "unknown"')
 
-    HANDLERS=$(hook::_determine_kubernetes_and_scheduler_handlers)
+    HANDLERS=$(hook::_get_possible_handler_names)
     HANDLERS="${HANDLERS} __main__"
 
     hook::_run_first_available_handler "${HANDLERS}"
   done
 }
 
-function hook::_determine_kubernetes_and_scheduler_handlers() {
+function hook::_get_possible_handler_names() {
   if [[ "$BINDING_CONTEXT_CURRENT_BINDING" == "onStartup" ]]; then
     echo __on_startup
   elif BINDING_CONTEXT_CURRENT_TYPE=$(context::jq -er '.type'); then
@@ -54,6 +54,9 @@ function hook::_determine_kubernetes_and_scheduler_handlers() {
     "Validating")
       echo __on_validating::${BINDING_CONTEXT_CURRENT_BINDING}
     ;;
+    "Mutating")
+      echo __on_mutating::${BINDING_CONTEXT_CURRENT_BINDING}
+    ;;
     "Conversion")
       echo __on_conversion::${BINDING_CONTEXT_CURRENT_BINDING}::$(context::jq -er '[.fromVersion,.toVersion]| map(sub("/";".")) | join("::")')
       echo __on_conversion::${BINDING_CONTEXT_CURRENT_BINDING}
diff --git a/go.mod b/go.mod
index 1633ded..d6d82e7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.19
 
 require (
 	github.com/flant/kube-client v0.25.0
-	github.com/flant/libjq-go v1.6.2-0.20200616114952-907039e8a02a // branch: master
+	github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee // branch: master
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/go-openapi/spec v0.19.8
 	github.com/go-openapi/strfmt v0.19.5
diff --git a/go.sum b/go.sum
index 0ebb386..d0e06a9 100644
--- a/go.sum
+++ b/go.sum
@@ -107,8 +107,8 @@ github.com/flant/go-openapi-validate v0.19.12-flant.0 h1:xk6kvc9fHKMgUdB6J7kbpbL
 github.com/flant/go-openapi-validate v0.19.12-flant.0/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
 github.com/flant/kube-client v0.25.0 h1:7uwwbvGNl3d7jNk0nHM4VxNbTnhESXLTiv/gQaUb9A0=
 github.com/flant/kube-client v0.25.0/go.mod h1:BPNhiy57FgJmnSBXy0l8AwBN6F9SHOfgFgHi3UG2Jrw=
-github.com/flant/libjq-go v1.6.2-0.20200616114952-907039e8a02a h1:PlStPekqPtTSWDDKFlwgETsT1OiXD1gZtRHcNxAs1lc=
-github.com/flant/libjq-go v1.6.2-0.20200616114952-907039e8a02a/go.mod h1:+SYqi5wsNjtQVlkPg0Ep5IOuN+ydg79Jo/gk4/PuS8c=
+github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee h1:evii83J+/6QGNvyf6tjQ/p27DPY9iftxIBb37ALJRTg=
+github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee/go.mod h1:f+REaGl/+pZR97rbTcwHEka/MAipoQQ2Mc0iQUj4ak0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -368,7 +368,6 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
-github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
diff --git a/pkg/app/app.go b/pkg/app/app.go
index e5a09d9..fb1f714 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -6,19 +6,25 @@ import (
 	"gopkg.in/alecthomas/kingpin.v2"
 )
 
-var AppName = "shell-operator"
-var AppDescription = "Run your custom cluster-wide scripts in reaction to Kubernetes events or on schedule."
-var AppStartMessage = "shell-operator"
+var (
+	AppName         = "shell-operator"
+	AppDescription  = "Run your custom cluster-wide scripts in reaction to Kubernetes events or on schedule."
+	AppStartMessage = "shell-operator"
+)
 
 var Version = "dev"
 
-var HooksDir = "hooks"
-var TempDir = "/tmp/shell-operator"
+var (
+	HooksDir = "hooks"
+	TempDir  = "/tmp/shell-operator"
+)
 
-var Namespace = ""
-var ListenAddress = "0.0.0.0"
-var ListenPort = "9115"
-var HookMetricsListenPort = ""
+var (
+	Namespace             = ""
+	ListenAddress         = "0.0.0.0"
+	ListenPort            = "9115"
+	HookMetricsListenPort = ""
+)
 
 var PrometheusMetricsPrefix = "shell_operator_"
 
diff --git a/pkg/app/debug.go b/pkg/app/debug.go
index 714fd48..7ad0b22 100644
--- a/pkg/app/debug.go
+++ b/pkg/app/debug.go
@@ -14,7 +14,6 @@ var DebugKubernetesAPI = false
 
 // DefineDebugFlags init global command line flags for debug.
 func DefineDebugFlags(kpApp *kingpin.Application, cmd *kingpin.CmdClause) {
-
 	DefineDebugUnixSocketFlag(cmd)
 
 	cmd.Flag("debug-keep-tmp-files", "set to yes to disable cleanup of temporary files").
diff --git a/pkg/app/kube-client.go b/pkg/app/kube-client.go
index ca03ebf..f0ef622 100644
--- a/pkg/app/kube-client.go
+++ b/pkg/app/kube-client.go
@@ -6,21 +6,27 @@ import (
 	"gopkg.in/alecthomas/kingpin.v2"
 )
 
-var KubeContext = ""
-var KubeConfig = ""
-var KubeServer = ""
+var (
+	KubeContext = ""
+	KubeConfig  = ""
+	KubeServer  = ""
+)
 
-var KubeClientQpsDefault = "5" // DefaultQPS from k8s.io/client-go/rest/config.go
-var KubeClientQps float32
-var KubeClientBurstDefault = "10" // DefaultBurst from k8s.io/client-go/rest/config.go
-var KubeClientBurst int
+var (
+	KubeClientQpsDefault   = "5" // DefaultQPS from k8s.io/client-go/rest/config.go
+	KubeClientQps          float32
+	KubeClientBurstDefault = "10" // DefaultBurst from k8s.io/client-go/rest/config.go
+	KubeClientBurst        int
+)
 
-var ObjectPatcherKubeClientQpsDefault = "5" // DefaultQPS from k8s.io/client-go/rest/config.go
-var ObjectPatcherKubeClientQps float32
-var ObjectPatcherKubeClientBurstDefault = "10" // DefaultBurst from k8s.io/client-go/rest/config.go
-var ObjectPatcherKubeClientBurst int
-var ObjectPatcherKubeClientTimeoutDefault = "10s"
-var ObjectPatcherKubeClientTimeout time.Duration
+var (
+	ObjectPatcherKubeClientQpsDefault     = "5" // DefaultQPS from k8s.io/client-go/rest/config.go
+	ObjectPatcherKubeClientQps            float32
+	ObjectPatcherKubeClientBurstDefault   = "10" // DefaultBurst from k8s.io/client-go/rest/config.go
+	ObjectPatcherKubeClientBurst          int
+	ObjectPatcherKubeClientTimeoutDefault = "10s"
+	ObjectPatcherKubeClientTimeout        time.Duration
+)
 
 func DefineKubeClientFlags(cmd *kingpin.CmdClause) {
 	// Settings for Kubernetes connection.
@@ -60,5 +66,4 @@ func DefineKubeClientFlags(cmd *kingpin.CmdClause) {
 		Envar("OBJECT_PATCHER_KUBE_CLIENT_TIMEOUT").
 		Default(ObjectPatcherKubeClientTimeoutDefault).
 		DurationVar(&ObjectPatcherKubeClientTimeout)
-
 }
diff --git a/pkg/app/log.go b/pkg/app/log.go
index 296dbbe..a0a5079 100644
--- a/pkg/app/log.go
+++ b/pkg/app/log.go
@@ -13,14 +13,18 @@ import (
 )
 
 // Use info level with timestamps and a text output by default
-var LogLevel = "info"
-var LogNoTime = false
-var LogType = "text"
-var LogProxyHookJSON = false
+var (
+	LogLevel         = "info"
+	LogNoTime        = false
+	LogType          = "text"
+	LogProxyHookJSON = false
+)
 
 // ForcedDurationForDebugLevel - force expiration for debug level.
-const ForcedDurationForDebugLevel = 30 * time.Minute
-const ProxyJsonLogKey = "proxyJsonLog"
+const (
+	ForcedDurationForDebugLevel = 30 * time.Minute
+	ProxyJsonLogKey             = "proxyJsonLog"
+)
 
 // DefineLoggingFlags defines flags for logger settings.
 func DefineLoggingFlags(cmd *kingpin.CmdClause) {
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/debug/server.go b/pkg/debug/server.go
index 16e4edb..f17131f 100644
--- a/pkg/debug/server.go
+++ b/pkg/debug/server.go
@@ -39,7 +39,7 @@ func (s *Server) WithPrefix(prefix string) {
 func (s *Server) Init() (err error) {
 	address := s.SocketPath
 
-	err = os.MkdirAll(path.Dir(address), 0700)
+	err = os.MkdirAll(path.Dir(address), 0o700)
 	if err != nil {
 		log.Errorf("Debug HTTP server fail to create socket '%s': %v", address, err)
 		return err
@@ -111,7 +111,6 @@ func (s *Server) RoutePOST(pattern string, handler func(request *http.Request) (
 
 func HandleFormattedOutput(writer http.ResponseWriter, request *http.Request, handler func(request *http.Request) (interface{}, error)) {
 	out, err := handler(request)
-
 	if err != nil {
 		writer.WriteHeader(http.StatusInternalServerError)
 		_, _ = fmt.Fprintf(writer, "Error: %s", err)
@@ -122,7 +121,6 @@ func HandleFormattedOutput(writer http.ResponseWriter, request *http.Request, ha
 	structured_logger.GetLogEntry(request).Debugf("use format '%s'", format)
 
 	outBytes, err := TransformUsingFormat(out, format)
-
 	if err != nil {
 		writer.WriteHeader(http.StatusInternalServerError)
 		_, _ = fmt.Fprintf(writer, "Error '%s' transform: %s", format, err)
diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go
index 1112098..59874e0 100644
--- a/pkg/executor/executor.go
+++ b/pkg/executor/executor.go
@@ -3,7 +3,6 @@ package executor
 import (
 	"bufio"
 	"encoding/json"
-	"github.com/flant/shell-operator/pkg/app"
 	"io"
 	"os/exec"
 	"strings"
@@ -13,6 +12,7 @@ import (
 
 	log "github.com/sirupsen/logrus"
 
+	"github.com/flant/shell-operator/pkg/app"
 	utils "github.com/flant/shell-operator/pkg/utils/labels"
 )
 
diff --git a/pkg/hook/binding_context/binding_context.go b/pkg/hook/binding_context/binding_context.go
index c5af99f..0e91d53 100644
--- a/pkg/hook/binding_context/binding_context.go
+++ b/pkg/hook/binding_context/binding_context.go
@@ -71,13 +71,19 @@ func (bc BindingContext) MapV1() map[string]interface{} {
 		}
 	}
 
-	// Handle validating and conversion before grouping.
+	// Handle admission and conversion before grouping.
 	if bc.Metadata.BindingType == KubernetesValidating {
 		res["type"] = "Validating"
 		res["review"] = bc.AdmissionReview
 		return res
 	}
 
+	if bc.Metadata.BindingType == KubernetesMutating {
+		res["type"] = "Mutating"
+		res["review"] = bc.AdmissionReview
+		return res
+	}
+
 	if bc.Metadata.BindingType == KubernetesConversion {
 		res["type"] = "Conversion"
 		res["fromVersion"] = bc.FromVersion
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_v0.go b/pkg/hook/config/config_v0.go
index ffc3e71..2d39ed4 100644
--- a/pkg/hook/config/config_v0.go
+++ b/pkg/hook/config/config_v0.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"fmt"
+
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
 	"gopkg.in/robfig/cron.v2"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -43,7 +44,6 @@ type KubeNamespaceSelectorV0 struct {
 
 // ConvertAndCheckV0 fills non-versioned structures and run inter-field checks not covered by OpenAPI schemas.
 func (cv0 *HookConfigV0) ConvertAndCheck(c *HookConfig) (err error) {
-
 	c.OnStartup, err = c.ConvertOnStartup(cv0.OnStartup)
 	if err != nil {
 		return err
diff --git a/pkg/hook/config/config_v1.go b/pkg/hook/config/config_v1.go
index 2e1a031..92e666d 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 {
@@ -245,7 +260,7 @@ func (cv1 *HookConfigV1) ConvertAndCheck(c *HookConfig) (err error) {
 
 	// Update IncludeSnapshotsFrom for every binding with a group.
 	// Merge binding's IncludeSnapshotsFrom with snapshots list calculated for group.
-	var groupSnapshots = make(map[string][]string)
+	groupSnapshots := make(map[string][]string)
 	for _, kubeCfg := range c.OnKubernetesEvents {
 		if kubeCfg.Group == "" {
 			continue
@@ -337,7 +352,7 @@ func (cv1 *HookConfigV1) CheckSchedule(kubeConfigs []OnKubernetesEventConfig, sc
 	return allErr
 }
 
-func (сv1 *HookConfigV1) CheckOnKubernetesEvent(kubeCfg OnKubernetesEventConfigV1, rootPath string) (allErr error) {
+func (cv1 *HookConfigV1) CheckOnKubernetesEvent(kubeCfg OnKubernetesEventConfigV1, rootPath string) (allErr error) {
 	if kubeCfg.ApiVersion != "" {
 		_, err := schema.ParseGroupVersion(kubeCfg.ApiVersion)
 		if err != nil {
@@ -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..77e6ba2
--- /dev/null
+++ b/pkg/hook/controller/admission_bindings_controller.go
@@ -0,0 +1,186 @@
+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"
+)
+
+// AdmissionBindingToWebhookLink is a link between a hook and a webhook configuration.
+type AdmissionBindingToWebhookLink struct {
+	BindingType     BindingType
+	BindingName     string
+	ConfigurationId string
+	WebhookId       string
+	// Useful fields to create a BindingContext
+	IncludeSnapshots []string
+	Group            string
+}
+
+// AdmissionBindingsController handles admission 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{
+			BindingType:      KubernetesValidating,
+			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{
+			BindingType:      KubernetesMutating,
+			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 = link.BindingType
+	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..adb3f70 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,33 @@ 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 +166,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 +206,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 +276,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..e49721d 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
+	AdmissionResponse    *AdmissionResponse
 	KubernetesPatchBytes []byte
 }
 
@@ -98,7 +98,7 @@ func (h *Hook) Run(_ BindingType, context []BindingContext, logLabels map[string
 		return nil, err
 	}
 
-	validatingPath, err := h.prepareValidatingResponseFile()
+	admissionPath, err := h.prepareAdmissionResponseFile()
 	if err != nil {
 		return nil, err
 	}
@@ -119,7 +119,7 @@ func (h *Hook) Run(_ BindingType, context []BindingContext, logLabels map[string
 			_ = os.Remove(contextPath)
 			_ = os.Remove(metricsPath)
 			_ = os.Remove(conversionPath)
-			_ = os.Remove(validatingPath)
+			_ = os.Remove(admissionPath)
 			_ = os.Remove(kubernetesPatchPath)
 		}
 	}()
@@ -130,7 +130,8 @@ func (h *Hook) Run(_ BindingType, context []BindingContext, logLabels map[string
 		envs = append(envs, fmt.Sprintf("BINDING_CONTEXT_PATH=%s", contextPath))
 		envs = append(envs, fmt.Sprintf("METRICS_PATH=%s", metricsPath))
 		envs = append(envs, fmt.Sprintf("CONVERSION_RESPONSE_PATH=%s", conversionPath))
-		envs = append(envs, fmt.Sprintf("VALIDATING_RESPONSE_PATH=%s", validatingPath))
+		envs = append(envs, fmt.Sprintf("VALIDATING_RESPONSE_PATH=%s", admissionPath))
+		envs = append(envs, fmt.Sprintf("ADMISSION_RESPONSE_PATH=%s", admissionPath))
 		envs = append(envs, fmt.Sprintf("KUBERNETES_PATCH_PATH=%s", kubernetesPatchPath))
 	}
 
@@ -148,7 +149,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.AdmissionResponse, err = AdmissionResponseFromFile(admissionPath)
 	if err != nil {
 		return result, fmt.Errorf("got bad validating response: %s", err)
 	}
@@ -215,6 +216,24 @@ func (h *Hook) GetConfigDescription() string {
 		}
 		msgs = append(msgs, fmt.Sprintf("Validate k8s kinds: '%s'", strings.Join(kindList, "', '")))
 	}
+	if len(h.Config.KubernetesMutating) > 0 {
+		kinds := map[string]struct{}{}
+		for _, mutating := range h.Config.KubernetesMutating {
+			if mutating.Webhook == nil {
+				continue
+			}
+			for _, rule := range mutating.Webhook.Rules {
+				for _, resource := range rule.Resources {
+					kinds[strings.ToLower(resource)] = struct{}{}
+				}
+			}
+		}
+		kindList := make([]string, 0, len(kinds))
+		for kind := range kinds {
+			kindList = append(kindList, kind)
+		}
+		msgs = append(msgs, fmt.Sprintf("Mutate k8s kinds: '%s'", strings.Join(kindList, "', '")))
+	}
 	if len(h.Config.KubernetesConversion) > 0 {
 		crds := map[string]struct{}{}
 		for _, cfg := range h.Config.KubernetesConversion {
@@ -245,7 +264,7 @@ func (h *Hook) prepareBindingContextJsonFile(context BindingContextList) (string
 
 	bindingContextPath := filepath.Join(h.TmpDir, fmt.Sprintf("hook-%s-binding-context-%s.json", h.SafeName(), uuid.NewV4().String()))
 
-	err = os.WriteFile(bindingContextPath, data, 0644)
+	err = os.WriteFile(bindingContextPath, data, 0o644)
 	if err != nil {
 		return "", err
 	}
@@ -256,7 +275,7 @@ func (h *Hook) prepareBindingContextJsonFile(context BindingContextList) (string
 func (h *Hook) prepareMetricsFile() (string, error) {
 	metricsPath := filepath.Join(h.TmpDir, fmt.Sprintf("hook-%s-metrics-%s.json", h.SafeName(), uuid.NewV4().String()))
 
-	err := os.WriteFile(metricsPath, []byte{}, 0644)
+	err := os.WriteFile(metricsPath, []byte{}, 0o644)
 	if err != nil {
 		return "", err
 	}
@@ -264,21 +283,22 @@ func (h *Hook) prepareMetricsFile() (string, error) {
 	return metricsPath, nil
 }
 
-func (h *Hook) prepareValidatingResponseFile() (string, error) {
-	validatingPath := filepath.Join(h.TmpDir, fmt.Sprintf("hook-%s-validating-response-%s.json", h.SafeName(), uuid.NewV4().String()))
+// prepareAdmissionResponseFile create file to store response from validating and mutating hooks.
+func (h *Hook) prepareAdmissionResponseFile() (string, error) {
+	filePath := filepath.Join(h.TmpDir, fmt.Sprintf("hook-%s-admission-response-%s.json", h.SafeName(), uuid.NewV4().String()))
 
-	err := os.WriteFile(validatingPath, []byte{}, 0644)
+	err := os.WriteFile(filePath, []byte{}, 0o644)
 	if err != nil {
 		return "", err
 	}
 
-	return validatingPath, nil
+	return filePath, nil
 }
 
 func (h *Hook) prepareConversionResponseFile() (string, error) {
 	conversionPath := filepath.Join(h.TmpDir, fmt.Sprintf("hook-%s-conversion-response-%s.json", h.SafeName(), uuid.NewV4().String()))
 
-	err := os.WriteFile(conversionPath, []byte{}, 0644)
+	err := os.WriteFile(conversionPath, []byte{}, 0o644)
 	if err != nil {
 		return "", err
 	}
@@ -304,7 +324,7 @@ func CreateRateLimiter(cfg *config.HookConfig) *rate.Limiter {
 func (h *Hook) prepareObjectPatchFile() (string, error) {
 	objectPatchPath := filepath.Join(h.TmpDir, fmt.Sprintf("%s-object-patch-%s", h.SafeName(), uuid.NewV4().String()))
 
-	err := os.WriteFile(objectPatchPath, []byte{}, 0644)
+	err := os.WriteFile(objectPatchPath, []byte{}, 0o644)
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/hook/hook_manager.go b/pkg/hook/hook_manager.go
index c254f8d..cf7f11c 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,8 @@ 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))
+	DetectAdmissionEventType(event AdmissionEvent) BindingType
 	HandleConversionEvent(event conversion.Event, rule conversion.Rule, createTaskFn func(*Hook, controller.BindingExecutionInfo))
 	FindConversionChain(crdName string, rule conversion.Rule) []conversion.Rule
 }
@@ -50,7 +51,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 +90,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 +189,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 +198,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 +321,24 @@ 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) {
+		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)
+		if h.HookController.CanHandleAdmissionEvent(event) {
+			h.HookController.HandleAdmissionEvent(event, func(info controller.BindingExecutionInfo) {
 				if createTaskFn != nil {
 					createTaskFn(h, info)
 				}
@@ -323,6 +347,27 @@ func (hm *hookManager) HandleValidatingEvent(event ValidatingEvent, createTaskFn
 	}
 }
 
+func (hm *hookManager) DetectAdmissionEventType(event AdmissionEvent) BindingType {
+	vHooks, _ := hm.GetHooksInOrder(KubernetesValidating)
+	for _, hookName := range vHooks {
+		h := hm.GetHook(hookName)
+		if h.HookController.CanHandleAdmissionEvent(event) {
+			return KubernetesValidating
+		}
+	}
+
+	mHooks, _ := hm.GetHooksInOrder(KubernetesMutating)
+	for _, hookName := range mHooks {
+		h := hm.GetHook(hookName)
+		if h.HookController.CanHandleAdmissionEvent(event) {
+			return KubernetesMutating
+		}
+	}
+
+	log.Errorf("Possible bug!!! No linked hook for admission event %s %s kind=%s name=%s ns=%s", event.ConfigurationId, event.WebhookId, event.Review.Request.Kind, event.Review.Request.Name, event.Review.Request.Namespace)
+	return ""
+}
+
 // HandleConversionEvent receives a crdName and calculates a sequence of hooks to run.
 func (hm *hookManager) HandleConversionEvent(event conversion.Event, rule conversion.Rule, createTaskFn func(*Hook, controller.BindingExecutionInfo)) {
 	vHooks, _ := hm.GetHooksInOrder(KubernetesConversion)
diff --git a/pkg/hook/task_metadata/task_metadata.go b/pkg/hook/task_metadata/task_metadata.go
index 7af6b80..37622b9 100644
--- a/pkg/hook/task_metadata/task_metadata.go
+++ b/pkg/hook/task_metadata/task_metadata.go
@@ -41,10 +41,12 @@ type HookMetadata struct {
 	ExecuteOnSynchronization bool // A flag to skip hook execution in Synchronization tasks.
 }
 
-var _ HookNameAccessor = HookMetadata{}
-var _ BindingContextAccessor = HookMetadata{}
-var _ MonitorIDAccessor = HookMetadata{}
-var _ task.MetadataDescriptable = HookMetadata{}
+var (
+	_ HookNameAccessor          = HookMetadata{}
+	_ BindingContextAccessor    = HookMetadata{}
+	_ MonitorIDAccessor         = HookMetadata{}
+	_ task.MetadataDescriptable = HookMetadata{}
+)
 
 func HookMetadataAccessor(t task.Task) (hookMeta HookMetadata) {
 	meta := t.GetMetadata()
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/kube/object_patch/helpers.go b/pkg/kube/object_patch/helpers.go
index dbea7e3..bf33d22 100644
--- a/pkg/kube/object_patch/helpers.go
+++ b/pkg/kube/object_patch/helpers.go
@@ -77,7 +77,7 @@ func applyJQPatch(jqFilter string, obj *unstructured.Unstructured) (*unstructure
 			"error: %s", jqFilter, obj, err)
 	}
 
-	var retObj = &unstructured.Unstructured{}
+	retObj := &unstructured.Unstructured{}
 	_, _, err = unstructured.UnstructuredJSONScheme.Decode([]byte(filterResult), nil, retObj)
 	if err != nil {
 		return nil, fmt.Errorf("failed to convert filterResult:\n%s\nto Unstructured Object\nerror: %s", filterResult, err)
diff --git a/pkg/kube/object_patch/operation.go b/pkg/kube/object_patch/operation.go
index 339cd96..b7e1f62 100644
--- a/pkg/kube/object_patch/operation.go
+++ b/pkg/kube/object_patch/operation.go
@@ -51,8 +51,8 @@ func ParseOperations(specBytes []byte) ([]Operation, error) {
 		return nil, err
 	}
 
-	var validationErrors = &multierror.Error{}
-	var ops = make([]Operation, 0)
+	validationErrors := &multierror.Error{}
+	ops := make([]Operation, 0)
 	for _, spec := range specs {
 		err = ValidateOperationSpec(spec, GetSchema("v0"), "")
 		if err != nil {
diff --git a/pkg/kube/object_patch/options.go b/pkg/kube/object_patch/options.go
index 7729129..fed6d6d 100644
--- a/pkg/kube/object_patch/options.go
+++ b/pkg/kube/object_patch/options.go
@@ -28,15 +28,19 @@ type subresourceHolder struct {
 func WithSubresource(s string) *subresourceHolder {
 	return &subresourceHolder{subresource: s}
 }
+
 func (s *subresourceHolder) applyToCreate(operation *createOperation) {
 	operation.subresource = s.subresource
 }
+
 func (s *subresourceHolder) applyToDelete(operation *deleteOperation) {
 	operation.subresource = s.subresource
 }
+
 func (s *subresourceHolder) applyToPatch(operation *patchOperation) {
 	operation.subresource = s.subresource
 }
+
 func (s *subresourceHolder) applyToFilter(operation *filterOperation) {
 	operation.subresource = s.subresource
 }
diff --git a/pkg/kube/object_patch/patch.go b/pkg/kube/object_patch/patch.go
index db66771..f87a485 100644
--- a/pkg/kube/object_patch/patch.go
+++ b/pkg/kube/object_patch/patch.go
@@ -43,7 +43,7 @@ func (o *ObjectPatcher) ExecuteOperations(ops []Operation) error {
 	log.Debug("Starting execute operations process")
 	defer log.Debug("Finished execute operations process")
 
-	var applyErrors = &multierror.Error{}
+	applyErrors := &multierror.Error{}
 	for _, op := range ops {
 		log.Debugf("Applying operation: %s", op.Description())
 		if err := o.ExecuteOperation(op); err != nil {
diff --git a/pkg/kube/object_patch/validation.go b/pkg/kube/object_patch/validation.go
index bb775f0..1a5233d 100644
--- a/pkg/kube/object_patch/validation.go
+++ b/pkg/kube/object_patch/validation.go
@@ -193,7 +193,7 @@ func ValidateOperationSpec(obj interface{}, s *spec.Schema, rootName string) (mu
 		return nil
 	}
 
-	var allErrs = &multierror.Error{Errors: make([]error, 1)}
+	allErrs := &multierror.Error{Errors: make([]error, 1)}
 	for _, err := range result.Errors {
 		allErrs = multierror.Append(allErrs, err)
 	}
diff --git a/pkg/kube_events_manager/error_handler.go b/pkg/kube_events_manager/error_handler.go
index e165b85..785af71 100644
--- a/pkg/kube_events_manager/error_handler.go
+++ b/pkg/kube_events_manager/error_handler.go
@@ -1,8 +1,9 @@
 package kube_events_manager
 
 import (
-	log "github.com/sirupsen/logrus"
 	"io"
+
+	log "github.com/sirupsen/logrus"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/client-go/tools/cache"
 
diff --git a/pkg/kube_events_manager/monitor.go b/pkg/kube_events_manager/monitor.go
index 7ea98f4..3f50e18 100644
--- a/pkg/kube_events_manager/monitor.go
+++ b/pkg/kube_events_manager/monitor.go
@@ -316,7 +316,6 @@ func (m *monitor) PauseHandleEvents() {
 	if m.NamespaceInformer != nil {
 		m.NamespaceInformer.PauseHandleEvents()
 	}
-
 }
 
 func (m *monitor) SnapshotOperations() (total *CachedObjectsInfo, last *CachedObjectsInfo) {
diff --git a/pkg/kube_events_manager/namespace_informer.go b/pkg/kube_events_manager/namespace_informer.go
index 20440c5..f5a6d50 100644
--- a/pkg/kube_events_manager/namespace_informer.go
+++ b/pkg/kube_events_manager/namespace_informer.go
@@ -82,12 +82,11 @@ func (ni *namespaceInformer) CreateSharedInformer(addFn func(string), delFn func
 	ni.SharedInformer = corev1.NewFilteredNamespaceInformer(ni.KubeClient, resyncPeriod, indexers, tweakListOptions)
 	ni.addFn = addFn
 	ni.delFn = delFn
-	ni.SharedInformer.AddEventHandler(ni) //SharedNamespaceInformerEventHandler(ni, addFn, delFn))
+	ni.SharedInformer.AddEventHandler(ni) // SharedNamespaceInformerEventHandler(ni, addFn, delFn))
 
 	listOptions := metav1.ListOptions{}
 	tweakListOptions(&listOptions)
 	existedObjects, err := ni.KubeClient.CoreV1().Namespaces().List(context.TODO(), listOptions)
-
 	if err != nil {
 		log.Errorf("list existing namespaces: %v", err)
 		return err
@@ -114,9 +113,11 @@ func (ni *namespaceInformer) OnAdd(obj interface{}) {
 		ni.addFn(nsObj.Name)
 	}
 }
+
 func (ni *namespaceInformer) OnUpdate(_ interface{}, _ interface{}) {
 	// Modified event for namespace is ignored
 }
+
 func (ni *namespaceInformer) OnDelete(obj interface{}) {
 	if ni.stopped {
 		return
diff --git a/pkg/kube_events_manager/resource_informer.go b/pkg/kube_events_manager/resource_informer.go
index 449d41e..8eab31b 100644
--- a/pkg/kube_events_manager/resource_informer.go
+++ b/pkg/kube_events_manager/resource_informer.go
@@ -218,10 +218,10 @@ func (ei *resourceInformer) LoadExistedObjects() error {
 	}
 
 	// FIXME objList.Items has too much information for log
-	//log.Debugf("%s: Got %d existing '%s' resources: %+v", ei.Monitor.Metadata.DebugName, len(objList.Items), ei.Monitor.Kind, objList.Items)
+	// log.Debugf("%s: Got %d existing '%s' resources: %+v", ei.Monitor.Metadata.DebugName, len(objList.Items), ei.Monitor.Kind, objList.Items)
 	log.Debugf("%s: '%s' initial list: Got %d existing resources", ei.Monitor.Metadata.DebugName, ei.Monitor.Kind, len(objList.Items))
 
-	var filteredObjects = make(map[string]*ObjectAndFilterResult)
+	filteredObjects := make(map[string]*ObjectAndFilterResult)
 
 	for _, item := range objList.Items {
 		// copy loop var to avoid duplication of pointer in filteredObjects
@@ -295,7 +295,7 @@ func (ei *resourceInformer) HandleWatchEvent(object interface{}, eventType Watch
 	if staleObj, stale := object.(cache.DeletedFinalStateUnknown); stale {
 		object = staleObj.Obj
 	}
-	var obj = object.(*unstructured.Unstructured)
+	obj := object.(*unstructured.Unstructured)
 
 	resourceId := ResourceId(obj)
 
@@ -381,7 +381,7 @@ func (ei *resourceInformer) HandleWatchEvent(object interface{}, eventType Watch
 			resourceId,
 		)
 		// TODO: should be disabled by default and enabled by a debug feature switch
-		//log.Debugf("HandleKubeEvent: obj type is %T, value:\n%#v", obj, obj)
+		// log.Debugf("HandleKubeEvent: obj type is %T, value:\n%#v", obj, obj)
 
 		kubeEvent := KubeEvent{
 			Type:        TypeEvent,
diff --git a/pkg/kube_events_manager/util.go b/pkg/kube_events_manager/util.go
index e100a40..1fec774 100644
--- a/pkg/kube_events_manager/util.go
+++ b/pkg/kube_events_manager/util.go
@@ -49,10 +49,12 @@ func FormatFieldSelector(selector *FieldSelector) (string, error) {
 	return fields.AndSelectors(requirements...).String(), nil
 }
 
-const ResyncPeriodMedian = time.Duration(3) * time.Hour
-const ResyncPeriodSpread = time.Duration(2) * time.Hour
-const ResyncPeriodGranularity = time.Duration(5) * time.Minute
-const ResyncPeriodJitterGranularity = time.Duration(15) * time.Second
+const (
+	ResyncPeriodMedian            = time.Duration(3) * time.Hour
+	ResyncPeriodSpread            = time.Duration(2) * time.Hour
+	ResyncPeriodGranularity       = time.Duration(5) * time.Minute
+	ResyncPeriodJitterGranularity = time.Duration(15) * time.Second
+)
 
 // RandomizedResyncPeriod returns a time.Duration between 2 hours and 4 hours with jitter and granularity
 func RandomizedResyncPeriod() time.Duration {
diff --git a/pkg/metric_storage/metric_storage.go b/pkg/metric_storage/metric_storage.go
index 33ca5ee..7b05767 100644
--- a/pkg/metric_storage/metric_storage.go
+++ b/pkg/metric_storage/metric_storage.go
@@ -192,7 +192,6 @@ func (m *MetricStorage) Counter(metric string, labels map[string]string) *promet
 	}
 
 	return m.RegisterCounter(metric, labels)
-
 }
 
 // RegisterCounter registers a counter.
@@ -331,8 +330,8 @@ func (m *MetricStorage) SendBatch(ops []operation.MetricOperation, labels map[st
 	}
 
 	// Group operations by 'Group' value.
-	var groupedOps = make(map[string][]operation.MetricOperation)
-	var nonGroupedOps = make([]operation.MetricOperation, 0)
+	groupedOps := make(map[string][]operation.MetricOperation)
+	nonGroupedOps := make([]operation.MetricOperation, 0)
 
 	for _, op := range ops {
 		if op.Group == "" {
diff --git a/pkg/metric_storage/operation/operation.go b/pkg/metric_storage/operation/operation.go
index ae4a354..f12736a 100644
--- a/pkg/metric_storage/operation/operation.go
+++ b/pkg/metric_storage/operation/operation.go
@@ -57,7 +57,7 @@ func (m MetricOperation) String() string {
 }
 
 func MetricOperationsFromReader(r io.Reader) ([]MetricOperation, error) {
-	var operations = make([]MetricOperation, 0)
+	operations := make([]MetricOperation, 0)
 
 	dec := json.NewDecoder(r)
 	for {
diff --git a/pkg/shell-operator/bootstrap.go b/pkg/shell-operator/bootstrap.go
index f86d81f..5f66ca9 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
@@ -102,7 +102,7 @@ func EnsureTempDirectory(inDir string) (string, error) {
 		return "", fmt.Errorf("get absolute path: %v", err)
 	}
 	if exists, _ := utils_file.DirExists(dir); !exists {
-		err := os.Mkdir(dir, os.FileMode(0777))
+		err := os.Mkdir(dir, os.FileMode(0o777))
 		if err != nil {
 			return "", fmt.Errorf("create tmp dir '%s': %s", dir, err)
 		}
@@ -218,13 +218,13 @@ 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
+	// Initialize conversion webhooks manager.
 	op.ConversionWebhookManager = conversion.NewWebhookManager()
 	op.ConversionWebhookManager.KubeClient = op.KubeClient
 	op.ConversionWebhookManager.Settings = app.ConversionWebhookSettings
@@ -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/combine_binding_context.go b/pkg/shell-operator/combine_binding_context.go
index 2b84550..95665d2 100644
--- a/pkg/shell-operator/combine_binding_context.go
+++ b/pkg/shell-operator/combine_binding_context.go
@@ -28,17 +28,17 @@ func CombineBindingContextForHook(tqs *queue.TaskQueueSet, q *queue.TaskQueue, t
 	if q == nil {
 		return nil
 	}
-	var taskMeta = t.GetMetadata()
+	taskMeta := t.GetMetadata()
 	if taskMeta == nil {
 		// Ignore task without metadata
 		return nil
 	}
-	var hookName = taskMeta.(HookNameAccessor).GetHookName()
+	hookName := taskMeta.(HookNameAccessor).GetHookName()
 
-	var res = new(CombineResult)
+	res := new(CombineResult)
 
-	var otherTasks = make([]task.Task, 0)
-	var stopIterate = false
+	otherTasks := make([]task.Task, 0)
+	stopIterate := false
 	q.Iterate(func(tsk task.Task) {
 		if stopIterate {
 			return
@@ -74,9 +74,9 @@ func CombineBindingContextForHook(tqs *queue.TaskQueueSet, q *queue.TaskQueue, t
 	}
 
 	// Combine binding context and make a map to delete excess tasks
-	var combinedContext = make([]BindingContext, 0)
-	var monitorIDs = taskMeta.(MonitorIDAccessor).GetMonitorIDs()
-	var tasksFilter = make(map[string]bool)
+	combinedContext := make([]BindingContext, 0)
+	monitorIDs := taskMeta.(MonitorIDAccessor).GetMonitorIDs()
+	tasksFilter := make(map[string]bool)
 	// current task always remain in queue
 	combinedContext = append(combinedContext, taskMeta.(BindingContextAccessor).GetBindingContext()...)
 	tasksFilter[t.GetId()] = true
@@ -98,12 +98,12 @@ func CombineBindingContextForHook(tqs *queue.TaskQueueSet, q *queue.TaskQueue, t
 	})
 
 	// group is used to compact binding contexts when only snapshots are needed
-	var compactedContext = make([]BindingContext, 0)
+	compactedContext := make([]BindingContext, 0)
 	for i := 0; i < len(combinedContext); i++ {
-		var keep = true
+		keep := true
 
 		// Binding context is ignored if next binding context has the similar group.
-		var groupName = combinedContext[i].Metadata.Group
+		groupName := combinedContext[i].Metadata.Group
 		if groupName != "" && (i+1 <= len(combinedContext)-1) && combinedContext[i+1].Metadata.Group == groupName {
 			keep = false
 		}
diff --git a/pkg/shell-operator/kube_client.go b/pkg/shell-operator/kube_client.go
index 9927b23..fd49782 100644
--- a/pkg/shell-operator/kube_client.go
+++ b/pkg/shell-operator/kube_client.go
@@ -10,8 +10,10 @@ import (
 	"github.com/flant/shell-operator/pkg/metric_storage"
 )
 
-var DefaultMainKubeClientMetricLabels = map[string]string{"component": "main"}
-var DefaultObjectPatcherKubeClientMetricLabels = map[string]string{"component": "object_patcher"}
+var (
+	DefaultMainKubeClientMetricLabels          = map[string]string{"component": "main"}
+	DefaultObjectPatcherKubeClientMetricLabels = map[string]string{"component": "object_patcher"}
+)
 
 func DefaultIfEmpty(m map[string]string, def map[string]string) map[string]string {
 	if len(m) == 0 {
diff --git a/pkg/shell-operator/manager_events_handler.go b/pkg/shell-operator/manager_events_handler.go
index 16544bd..44ff9b2 100644
--- a/pkg/shell-operator/manager_events_handler.go
+++ b/pkg/shell-operator/manager_events_handler.go
@@ -63,7 +63,7 @@ func (m *ManagerEventsHandler) Start() {
 	go func() {
 		for {
 			var tailTasks []task.Task
-			var logEntry = log.WithField("operator.component", "handleEvents")
+			logEntry := log.WithField("operator.component", "handleEvents")
 			select {
 			case crontab := <-m.scheduleManager.Ch():
 				if m.scheduleCb != nil {
diff --git a/pkg/shell-operator/operator.go b/pkg/shell-operator/operator.go
index b2abc95..0de87a1 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,25 @@ 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) {
+		eventBindingType := op.HookManager.DetectAdmissionEventType(event)
 		logLabels := map[string]string{
 			"event.id": uuid.NewV4().String(),
-			"binding":  string(KubernetesValidating),
+			"event":    string(eventBindingType),
 		}
 		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'", eventBindingType, 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:    eventBindingType,
 					BindingContext: info.BindingContext,
 					AllowFailure:   info.AllowFailure,
 					Binding:        info.Binding,
@@ -211,22 +215,22 @@ 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)
+		admissionProp := tasks[0].GetProp("admissionResponse")
+		admissionResponse, ok := admissionProp.(*AdmissionResponse)
 		if !ok {
-			logEntry.Errorf("'validatingResponse' task prop is not of type *ValidatingResponse: %T", validatingProp)
+			logEntry.Errorf("'admissionResponse' task prop is not of type *AdmissionResponse: %T", admissionProp)
 			return nil, fmt.Errorf("hook task prop error")
 		}
-		return validatingResponse, nil
+		return admissionResponse, nil
 	})
 
-	err = op.ValidatingWebhookManager.Start()
+	err = op.AdmissionWebhookManager.Start()
 	if err != nil {
 		log.Errorf("ValidatingWebhookManager start: %v", err)
 	}
@@ -335,7 +339,7 @@ func (op *ShellOperator) ConversionEventHandler(event conversion.Event) (*conver
 
 			// Stop iterating if hook has converted all objects to a desiredAPIVersions.
 			newSourceVersions := conversion.ExtractAPIVersions(event.Objects)
-			//logEntry.Infof("Hook return conversion response: failMsg=%s, %d convertedObjects, versions:%v, desired: %s", response.FailedMessage, len(response.ConvertedObjects), newSourceVersions, event.Review.Request.DesiredAPIVersion)
+			// logEntry.Infof("Hook return conversion response: failMsg=%s, %d convertedObjects, versions:%v, desired: %s", response.FailedMessage, len(response.ConvertedObjects), newSourceVersions, event.Review.Request.DesiredAPIVersion)
 
 			if len(newSourceVersions) == 1 && newSourceVersions[0] == event.Review.Request.DesiredAPIVersion {
 				// success
@@ -383,8 +387,8 @@ func (op *ShellOperator) Start() {
 
 // TaskHandler
 func (op *ShellOperator) TaskHandler(t task.Task) queue.TaskResult {
-	var logEntry = log.WithField("operator.component", "taskRunner")
-	var hookMeta = HookMetadataAccessor(t)
+	logEntry := log.WithField("operator.component", "taskRunner")
+	hookMeta := HookMetadataAccessor(t)
 	var res queue.TaskResult
 
 	switch t.GetType() {
@@ -414,7 +418,7 @@ func (op *ShellOperator) TaskHandler(t task.Task) queue.TaskResult {
 
 // TaskHandleEnableKubernetesBindings creates task for each Kubernetes binding in the hook and queues them.
 func (op *ShellOperator) TaskHandleEnableKubernetesBindings(t task.Task) queue.TaskResult {
-	var hookMeta = HookMetadataAccessor(t)
+	hookMeta := HookMetadataAccessor(t)
 
 	metricLabels := map[string]string{
 		"hook": hookMeta.HookName,
@@ -482,7 +486,7 @@ func (op *ShellOperator) TaskHandleEnableKubernetesBindings(t task.Task) queue.T
 
 // TODO use Context to pass labels and a queue name
 func (op *ShellOperator) TaskHandleHookRun(t task.Task) queue.TaskResult {
-	var hookMeta = HookMetadataAccessor(t)
+	hookMeta := HookMetadataAccessor(t)
 	taskHook := op.HookManager.GetHook(hookMeta.HookName)
 
 	err := taskHook.RateLimitWait(context.Background())
@@ -631,9 +635,9 @@ func (op *ShellOperator) HandleRunHook(t task.Task, taskHook *hook.Hook, hookMet
 	}
 
 	// Save validatingResponse in task props for future use.
-	if result.ValidatingResponse != nil {
-		t.SetProp("validatingResponse", result.ValidatingResponse)
-		taskLogEntry.Infof("ValidatingResponse from hook: %s", result.ValidatingResponse.Dump())
+	if result.AdmissionResponse != nil {
+		t.SetProp("admissionResponse", result.AdmissionResponse)
+		taskLogEntry.Infof("AdmissionResponse from hook: %s", result.AdmissionResponse.Dump())
 	}
 
 	// Save conversionResponse in task props for future use.
@@ -657,17 +661,17 @@ func (op *ShellOperator) CombineBindingContextForHook(q *queue.TaskQueue, t task
 	if q == nil {
 		return nil
 	}
-	var taskMeta = t.GetMetadata()
+	taskMeta := t.GetMetadata()
 	if taskMeta == nil {
 		// Ignore task without metadata
 		return nil
 	}
-	var hookName = taskMeta.(HookNameAccessor).GetHookName()
+	hookName := taskMeta.(HookNameAccessor).GetHookName()
 
-	var res = new(CombineResult)
+	res := new(CombineResult)
 
-	var otherTasks = make([]task.Task, 0)
-	var stopIterate = false
+	otherTasks := make([]task.Task, 0)
+	stopIterate := false
 	q.Iterate(func(tsk task.Task) {
 		if stopIterate {
 			return
@@ -703,9 +707,9 @@ func (op *ShellOperator) CombineBindingContextForHook(q *queue.TaskQueue, t task
 	}
 
 	// Combine binding context and make a map to delete excess tasks
-	var combinedContext = make([]BindingContext, 0)
-	var monitorIDs = taskMeta.(MonitorIDAccessor).GetMonitorIDs()
-	var tasksFilter = make(map[string]bool)
+	combinedContext := make([]BindingContext, 0)
+	monitorIDs := taskMeta.(MonitorIDAccessor).GetMonitorIDs()
+	tasksFilter := make(map[string]bool)
 	// current task always remain in queue
 	combinedContext = append(combinedContext, taskMeta.(BindingContextAccessor).GetBindingContext()...)
 	tasksFilter[t.GetId()] = true
@@ -727,12 +731,12 @@ func (op *ShellOperator) CombineBindingContextForHook(q *queue.TaskQueue, t task
 	})
 
 	// group is used to compact binding contexts when only snapshots are needed
-	var compactedContext = make([]BindingContext, 0)
+	compactedContext := make([]BindingContext, 0)
 	for i := 0; i < len(combinedContext); i++ {
-		var keep = true
+		keep := true
 
 		// Binding context is ignored if next binding context has the similar group.
-		var groupName = combinedContext[i].Metadata.Group
+		groupName := combinedContext[i].Metadata.Group
 		if groupName != "" && (i+1 <= len(combinedContext)-1) && combinedContext[i+1].Metadata.Group == groupName {
 			keep = false
 		}
@@ -817,7 +821,6 @@ func (op *ShellOperator) BootstrapMainQueue(tqs *queue.TaskQueueSet) {
 			logEntry.Infof("queue task %s with hook %s", newTask.GetDescription(), hookName)
 		}
 	}
-
 }
 
 // InitAndStartHookQueues create all queues defined in hooks
diff --git a/pkg/task/dump/dump.go b/pkg/task/dump/dump.go
index 327a47a..7ea84ea 100644
--- a/pkg/task/dump/dump.go
+++ b/pkg/task/dump/dump.go
@@ -158,7 +158,7 @@ func TaskQueueToText(q *queue.TaskQueue) string {
 	buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", q.Name, q.Length(), q.Status))
 	buf.WriteString("\n")
 
-	var index = 1
+	index := 1
 	q.Iterate(func(task task.Task) {
 		buf.WriteString(fmt.Sprintf("%2d. ", index))
 		buf.WriteString(task.GetDescription())
diff --git a/pkg/task/queue/queue_set.go b/pkg/task/queue/queue_set.go
index 502f4a6..c1bee1f 100644
--- a/pkg/task/queue/queue_set.go
+++ b/pkg/task/queue/queue_set.go
@@ -84,10 +84,10 @@ func (tqs *TaskQueueSet) GetMain() *TaskQueue {
 	return tqs.GetByName(tqs.MainName)
 }
 
-/**
-taskQueueSet.DoWithLock(func(tqs *TaskQueueSet){
-   tqs.GetMain().Pop()
-})
+/*
+	taskQueueSet.DoWithLock(func(tqs *TaskQueueSet){
+	   tqs.GetMain().Pop()
+	})
 */
 func (tqs *TaskQueueSet) DoWithLock(fn func(tqs *TaskQueueSet)) {
 	tqs.m.Lock()
diff --git a/pkg/task/queue/task_queue.go b/pkg/task/queue/task_queue.go
index d555976..4f1592d 100644
--- a/pkg/task/queue/task_queue.go
+++ b/pkg/task/queue/task_queue.go
@@ -389,7 +389,7 @@ func (q *TaskQueue) Start() {
 		var sleepDelay time.Duration
 		for {
 			q.debugf("queue %s: wait for task, delay %d", q.Name, sleepDelay)
-			var t = q.waitForTask(sleepDelay)
+			t := q.waitForTask(sleepDelay)
 			if t == nil {
 				q.Status = "stop"
 				log.Infof("queue '%s' stopped", q.Name)
@@ -586,7 +586,7 @@ func (q *TaskQueue) Filter(filterFn func(task.Task) bool) {
 	defer q.MeasureActionTime("Filter")()
 
 	q.withLock(func() {
-		var newItems = make([]task.Task, 0)
+		newItems := make([]task.Task, 0)
 		for _, t := range q.items {
 			if filterFn(t) {
 				newItems = append(newItems, t)
@@ -602,7 +602,7 @@ func (q *TaskQueue) Filter(filterFn func(task.Task) bool) {
 func (q *TaskQueue) String() string {
 	var buf strings.Builder
 	var index int
-	var qLen = q.Length()
+	qLen := q.Length()
 	q.Iterate(func(t task.Task) {
 		buf.WriteString(fmt.Sprintf("[%s,id=%10.10s]", t.GetDescription(), t.GetId()))
 		index++
diff --git a/pkg/utils/checksum/checksum.go b/pkg/utils/checksum/checksum.go
index 129a341..ce1a4ae 100644
--- a/pkg/utils/checksum/checksum.go
+++ b/pkg/utils/checksum/checksum.go
@@ -44,7 +44,6 @@ func CalculateChecksumOfDirectory(path string) (string, error) {
 
 		return nil
 	})
-
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/utils/exponential_backoff/delay.go b/pkg/utils/exponential_backoff/delay.go
index cc4cb37..7fe0ab2 100644
--- a/pkg/utils/exponential_backoff/delay.go
+++ b/pkg/utils/exponential_backoff/delay.go
@@ -6,9 +6,11 @@ import (
 	"time"
 )
 
-const MaxExponentialBackoffDelay = time.Duration(32 * time.Second)
-const ExponentialDelayFactor float64 = 2.0 // Each delta delay is twice bigger.
-const ExponentialDelayRandomMs = 1000      // Each delay has random additional milliseconds.
+const (
+	MaxExponentialBackoffDelay         = time.Duration(32 * time.Second)
+	ExponentialDelayFactor     float64 = 2.0  // Each delta delay is twice bigger.
+	ExponentialDelayRandomMs           = 1000 // Each delay has random additional milliseconds.
+)
 
 // Count of exponential calculations before return max delay to prevent overflow with big numbers.
 var ExponentialCalculationsCount = int(math.Log(MaxExponentialBackoffDelay.Seconds()) / math.Log(ExponentialDelayFactor))
@@ -16,14 +18,15 @@ var ExponentialCalculationsCount = int(math.Log(MaxExponentialBackoffDelay.Secon
 // CalculateDelay returns delay distributed from initialDelay to default maxDelay (32s)
 //
 // Example:
-//   Retry 0: 5s
-//   Retry 1: 6s
-//   Retry 2: 7.8s
-//   Retry 3: 9.8s
-//   Retry 4: 13s
-//   Retry 5: 21s
-//   Retry 6: 32s
-//   Retry 7: 32s
+//
+//	Retry 0: 5s
+//	Retry 1: 6s
+//	Retry 2: 7.8s
+//	Retry 3: 9.8s
+//	Retry 4: 13s
+//	Retry 5: 21s
+//	Retry 6: 32s
+//	Retry 7: 32s
 func CalculateDelay(initialDelay time.Duration, retryCount int) time.Duration {
 	return CalculateDelayWithMax(initialDelay, MaxExponentialBackoffDelay, retryCount)
 }
diff --git a/pkg/utils/file/file.go b/pkg/utils/file/file.go
index 5dab09c..d33307d 100644
--- a/pkg/utils/file/file.go
+++ b/pkg/utils/file/file.go
@@ -30,7 +30,7 @@ func DirExists(path string) (bool, error) {
 }
 
 func IsFileExecutable(f os.FileInfo) bool {
-	return f.Mode()&0111 != 0
+	return f.Mode()&0o111 != 0
 }
 
 // RecursiveGetExecutablePaths finds recursively all executable files
@@ -64,7 +64,6 @@ func RecursiveGetExecutablePaths(dir string) ([]string, error) {
 		paths = append(paths, path)
 		return nil
 	})
-
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/utils/labels/labels.go b/pkg/utils/labels/labels.go
index 6da4ade..1eff1ab 100644
--- a/pkg/utils/labels/labels.go
+++ b/pkg/utils/labels/labels.go
@@ -1,8 +1,9 @@
 package utils
 
 import (
-	log "github.com/sirupsen/logrus"
 	"sort"
+
+	log "github.com/sirupsen/logrus"
 )
 
 // MergeLabels merges several maps into one. Last map keys overrides keys from first maps.
@@ -39,7 +40,7 @@ func LabelNames(labels map[string]string) []string {
 }
 
 func LabelValues(labels map[string]string, labelNames []string) []string {
-	var values = make([]string, 0)
+	values := make([]string, 0)
 	for _, name := range labelNames {
 		values = append(values, labels[name])
 	}
diff --git a/pkg/utils/structured-logger/structured-logger.go b/pkg/utils/structured-logger/structured-logger.go
index d6e0460..7bc82d5 100644
--- a/pkg/utils/structured-logger/structured-logger.go
+++ b/pkg/utils/structured-logger/structured-logger.go
@@ -33,7 +33,7 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
 	logFields["uri"] = r.RequestURI
 
 	entry.Logger = entry.Logger.WithFields(logFields)
-	//entry.Logger.Infoln("request started")
+	// entry.Logger.Infoln("request started")
 
 	return entry
 }
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 73%
rename from pkg/webhook/validating/handler.go
rename to pkg/webhook/admission/handler.go
index a138049..46215c0 100644
--- a/pkg/webhook/validating/handler.go
+++ b/pkg/webhook/admission/handler.go
@@ -1,4 +1,4 @@
-package validating
+package admission
 
 import (
 	"encoding/json"
@@ -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 {
@@ -28,7 +31,7 @@ func NewWebhookHandler() *WebhookHandler {
 	h := &WebhookHandler{
 		Router: rtr,
 	}
-	rtr.Use(structured_logger.NewStructuredLogger(log.StandardLogger(), "validatingWebhook"))
+	rtr.Use(structured_logger.NewStructuredLogger(log.StandardLogger(), "admissionWebhook"))
 	rtr.Use(middleware.Recoverer)
 	rtr.Use(middleware.AllowContentType("application/json"))
 	rtr.Post("/*", h.ServeReviewRequest)
@@ -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)
+	admissionResponse, err := h.Handler(event)
 	if err != nil {
 		response.Response.Allowed = false
 		response.Response.Result = &metav1.Status{
@@ -110,27 +113,39 @@ func (h *WebhookHandler) HandleReviewRequest(path string, body []byte) (*v1.Admi
 		return response, nil
 	}
 
-	if len(validatingResponse.Warnings) > 0 {
-		response.Response.Warnings = validatingResponse.Warnings
+	if len(admissionResponse.Warnings) > 0 {
+		response.Response.Warnings = admissionResponse.Warnings
 	}
 
-	if !validatingResponse.Allowed {
+	if !admissionResponse.Allowed {
 		response.Response.Allowed = false
 		response.Response.Result = &metav1.Status{
 			Code:    403,
-			Message: validatingResponse.Message,
+			Message: admissionResponse.Message,
 		}
 		return response, nil
 	}
 
 	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(admissionResponse.Patch) > 0 {
+		response.Response.Patch = admissionResponse.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..b2395e7
--- /dev/null
+++ b/pkg/webhook/admission/manager.go
@@ -0,0 +1,145 @@
+package admission
+
+import (
+	"os"
+
+	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 admission 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.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..b9a84e0
--- /dev/null
+++ b/pkg/webhook/admission/resource.go
@@ -0,0 +1,194 @@
+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 82%
rename from pkg/webhook/validating/types/event.go
rename to pkg/webhook/admission/types/event.go
index 45b9c7f..ba6f0ca 100644
--- a/pkg/webhook/validating/types/event.go
+++ b/pkg/webhook/admission/types/event.go
@@ -4,7 +4,7 @@ import (
 	v1 "k8s.io/api/admission/v1"
 )
 
-type ValidatingEvent struct {
+type AdmissionEvent struct {
 	WebhookId       string
 	ConfigurationId string
 	Review          *v1.AdmissionReview
diff --git a/pkg/webhook/validating/types/response.go b/pkg/webhook/admission/types/response.go
similarity index 52%
rename from pkg/webhook/validating/types/response.go
rename to pkg/webhook/admission/types/response.go
index f4f68d6..cd6f866 100644
--- a/pkg/webhook/validating/types/response.go
+++ b/pkg/webhook/admission/types/response.go
@@ -10,13 +10,14 @@ import (
 	"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) {
+func AdmissionResponseFromFile(filePath string) (*AdmissionResponse, error) {
 	data, err := os.ReadFile(filePath)
 	if err != nil {
 		return nil, fmt.Errorf("cannot read %s: %s", filePath, err)
@@ -25,37 +26,39 @@ 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("AdmissionResponse(allowed=")
 	b.WriteString(strconv.FormatBool(r.Allowed))
+	if len(r.Patch) > 0 {
+		b.WriteString(",patch=")
+		b.Write(r.Patch)
+	}
 	if r.Message != "" {
-		b.WriteString(",")
+		b.WriteString(",msg=")
 		b.WriteString(r.Message)
 	}
 	for _, warning := range r.Warnings {
-		b.WriteString(",")
+		b.WriteString(",warn=")
 		b.WriteString(warning)
 	}
 	b.WriteString(")")
diff --git a/pkg/webhook/conversion/handler.go b/pkg/webhook/conversion/handler.go
index d1dad20..f9a1ab9 100644
--- a/pkg/webhook/conversion/handler.go
+++ b/pkg/webhook/conversion/handler.go
@@ -134,7 +134,7 @@ func (h *WebhookHandler) HandleReviewRequest(path string, body []byte) (*v1.Conv
 	// Convert objects from hook into to array of runtime.RawExtension
 	rawObjects := make([]runtime.RawExtension, len(conversionResponse.ConvertedObjects))
 	for i, obj := range conversionResponse.ConvertedObjects {
-		var tmpObj = obj
+		tmpObj := obj
 		rawObjects[i] = runtime.RawExtension{Object: &tmpObj}
 	}
 	review.Response.ConvertedObjects = rawObjects
@@ -159,7 +159,7 @@ func ExtractAPIVersions(objs []unstructured.Unstructured) []string {
 	for _, obj := range objs {
 		verMap[obj.GetAPIVersion()] = true
 	}
-	var res = make([]string, 0)
+	res := make([]string, 0)
 	for ver := range verMap {
 		res = append(res, ver)
 	}
@@ -167,7 +167,7 @@ func ExtractAPIVersions(objs []unstructured.Unstructured) []string {
 }
 
 func RawExtensionToUnstructured(objects []runtime.RawExtension) ([]unstructured.Unstructured, error) {
-	var res = make([]unstructured.Unstructured, 0)
+	res := make([]unstructured.Unstructured, 0)
 
 	for _, obj := range objects {
 		cr := unstructured.Unstructured{}
diff --git a/pkg/webhook/conversion/response.go b/pkg/webhook/conversion/response.go
index 8db67c5..40c1f1a 100644
--- a/pkg/webhook/conversion/response.go
+++ b/pkg/webhook/conversion/response.go
@@ -63,7 +63,6 @@ func ResponseFromReader(r io.Reader) (*Response, error) {
 	dec := json.NewDecoder(r)
 
 	err := dec.Decode(response)
-
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/webhook/server/server.go b/pkg/webhook/server/server.go
index 7b6148d..b1f1b3b 100644
--- a/pkg/webhook/server/server.go
+++ b/pkg/webhook/server/server.go
@@ -19,8 +19,7 @@ type WebhookServer struct {
 	Router    chi.Router
 }
 
-// StartWebhookServer starts https server
-// to listen for AdmissionReview requests from cluster
+// Start runs https server to listen for AdmissionReview requests from the API-server.
 func (s *WebhookServer) Start() error {
 	// Load server certificate.
 	keyPair, err := tls.LoadX509KeyPair(
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
-}
diff --git a/pkg/webhook/validating/validation/validation.go b/pkg/webhook/validating/validation/validation.go
index ef6ded2..2ba032c 100644
--- a/pkg/webhook/validating/validation/validation.go
+++ b/pkg/webhook/validating/validation/validation.go
@@ -33,7 +33,7 @@ func ValidateValidatingWebhooks(e *v1.ValidatingWebhookConfiguration) error {
 	hookNames := make(map[string]struct{})
 	for i, hook := range e.Webhooks {
 		allErrors = multierror.Append(allErrors, ValidateValidatingWebhook(&hook, field.NewPath("webhooks").Index(i)))
-		//allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, opts.requireRecognizedAdmissionReviewVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
+		// allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, opts.requireRecognizedAdmissionReviewVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
 		if len(hook.Name) > 0 {
 			if _, has := hookNames[hook.Name]; has {
 				allErrors = multierror.Append(allErrors, field.Duplicate(field.NewPath("webhooks").Index(i).Child("name"), hook.Name))


Test patch:
diff --git a/.github/workflows/tests-labeled.yaml b/.github/workflows/tests-labeled.yaml
index cc5ee5d..e7e3553 100644
--- a/.github/workflows/tests-labeled.yaml
+++ b/.github/workflows/tests-labeled.yaml
@@ -73,7 +73,7 @@ jobs:
 
       - name: Restore Go modules
         id: go-modules-cache
-        uses: actions/cache@v3.2.1
+        uses: actions/cache@v3.2.4
         with:
           path: |
             ~/go/pkg/mod
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 9a200d7..40b26f5 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -19,7 +19,7 @@ jobs:
 
       - name: Restore Go modules
         id: go-modules-cache
-        uses: actions/cache@v3.2.1
+        uses: actions/cache@v3.2.4
         with:
           path: |
             ~/go/pkg/mod
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 32904a2..beb8f50 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -62,7 +62,6 @@ func TestConfig_OnChange(t *testing.T) {
 
 	c.Unset("log.level")
 	assert.Equal(t, "info", newValue, "onChange not called for Unset after SetTemporarily+Set")
-
 }
 
 func TestConfig_Errors(t *testing.T) {
diff --git a/pkg/hook/binding_context/binding_context_test.go b/pkg/hook/binding_context/binding_context_test.go
index 934f733..41188fb 100644
--- a/pkg/hook/binding_context/binding_context_test.go
+++ b/pkg/hook/binding_context/binding_context_test.go
@@ -3,17 +3,17 @@ package binding_context
 import (
 	"testing"
 
+	. "github.com/flant/libjq-go"
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/assert"
-
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
-	. "github.com/flant/libjq-go"
 	. "github.com/flant/shell-operator/pkg/hook/types"
 	. "github.com/flant/shell-operator/pkg/kube_events_manager/types"
 )
 
 func JqEqual(t *testing.T, input []byte, program string, expected string) {
+	// nolint:typecheck // Ignore false positive: undeclared name: `Jq`.
 	res, err := Jq().Program(program).Run(string(input))
 	if assert.NoError(t, err) {
 		assert.Equal(t, expected, res, "jq: '%s', json was '%s'", program, string(input))
@@ -82,7 +82,6 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 				assert.Equal(t, "Added", bcList[0]["watchEvent"])
 				assert.IsType(t, &unstructured.Unstructured{}, bcList[0]["object"])
 				assert.Contains(t, bcList[0]["object"].(*unstructured.Unstructured).Object, "metadata")
-
 			},
 			[][]string{
 				// JSON dump should has only 4 fields: binding, type, watchEvent and object.
@@ -143,7 +142,8 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 								"name":      "deployment-2",
 							},
 							"kind": "Deployment",
-						}},
+						},
+					},
 					FilterResult: `""`,
 				}
 				obj.Metadata.JqFilter = ".metadata.labels"
@@ -174,7 +174,7 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 		{
 			"binding context with group",
 			func() []BindingContext {
-				var bcs = []BindingContext{}
+				bcs := []BindingContext{}
 
 				bc := BindingContext{
 					Binding:    "monitor_pods",
@@ -254,7 +254,7 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 		{
 			"grouped Synchronization",
 			func() []BindingContext {
-				var bcs = []BindingContext{}
+				bcs := []BindingContext{}
 
 				bc := BindingContext{
 					Binding:   "monitor_config_maps",
@@ -376,7 +376,7 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			bcList = ConvertBindingContextList("v1", tt.bc())
-			//assert.Len(t, bcList, 1)
+			// assert.Len(t, bcList, 1)
 
 			var err error
 			bcJson, err = bcList.Json()
diff --git a/pkg/hook/config/config_test.go b/pkg/hook/config/config_test.go
index 068028b..e2599e8 100644
--- a/pkg/hook/config/config_test.go
+++ b/pkg/hook/config/config_test.go
@@ -91,7 +91,6 @@ func Test_HookConfig_VersionedConfig_LoadAndValidate(t *testing.T) {
 				g.Expect(sch1min.BindingName).To(Equal("each 1 min"))
 				sch5min := hookConfig.Schedules[1]
 				g.Expect(sch5min.BindingName).To(Equal("each 5 min"))
-
 			},
 		},
 
@@ -144,7 +143,6 @@ func Test_HookConfig_VersionedConfig_LoadAndValidate(t *testing.T) {
 
 				sch5min := hookConfig.Schedules[1]
 				g.Expect(sch5min.BindingName).To(Equal("each 5 min"))
-
 			},
 		},
 		{
@@ -178,7 +176,6 @@ func Test_HookConfig_VersionedConfig_LoadAndValidate(t *testing.T) {
 				sch5sec := hookConfig.Schedules[1]
 				g.Expect(sch5sec.BindingName).To(Equal("each 5 sec"))
 				g.Expect(sch5sec.Queue).To(Equal("off-schedule"))
-
 			},
 		},
 		{
@@ -325,7 +322,6 @@ schedule:
 				g.Expect(cfg.Monitor).ToNot(BeNil())
 				g.Expect(cfg.Monitor.NameSelector).ToNot(BeNil())
 				g.Expect(cfg.Monitor.NameSelector.MatchNames).ToNot(BeNil())
-
 			},
 		},
 		{
@@ -424,7 +420,7 @@ schedule:
 
 				// Schedule binding should has includeSnapshotsFrom
 				s := hookConfig.Schedules[0]
-				//g.Expect(s.BindingName).To(Equal("each 1 min"))
+				// g.Expect(s.BindingName).To(Equal("each 1 min"))
 				g.Expect(s.Queue).To(Equal("main"))
 				g.Expect(s.IncludeSnapshotsFrom).Should(HaveLen(2))
 
@@ -497,9 +493,9 @@ schedule:
 				g.Expect(secrets.ExecuteHookOnSynchronization).Should(BeTrue())
 				g.Expect(hookConfig.V1.OnKubernetesEvent[2].ExecuteHookOnSynchronization).Should(Equal(""))
 
-				//g.Expect(kPods.IncludeSnapshotsFrom).Should(HaveLen(2))
-				//kPods = hookConfig.OnKubernetesEvents[1]
-				//g.Expect(kPods.IncludeSnapshotsFrom).Should(HaveLen(2))
+				// g.Expect(kPods.IncludeSnapshotsFrom).Should(HaveLen(2))
+				// kPods = hookConfig.OnKubernetesEvents[1]
+				// g.Expect(kPods.IncludeSnapshotsFrom).Should(HaveLen(2))
 			},
 		},
 		{
@@ -617,7 +613,6 @@ kubernetesValidating:
 				g.Expect(*wh.SideEffects).To(Equal(v1.SideEffectClassNoneOnDryRun))
 				g.Expect(wh.TimeoutSeconds).ShouldNot(BeNil())
 				g.Expect(*wh.TimeoutSeconds).To(BeEquivalentTo(30))
-
 			},
 		},
 		{
diff --git a/pkg/hook/config/validator_test.go b/pkg/hook/config/validator_test.go
index d51670b..9635129 100644
--- a/pkg/hook/config/validator_test.go
+++ b/pkg/hook/config/validator_test.go
@@ -21,7 +21,7 @@ func Test_Validate_V1_With_Error(t *testing.T) {
 	var vu *VersionedUntyped
 	var err error
 
-	var tests = []struct {
+	tests := []struct {
 		name       string
 		configText string
 		fn         func()
@@ -149,7 +149,7 @@ func Test_Validate_V1_With_Error(t *testing.T) {
 			vu = prepareConfigObj(g, tt.configText)
 			s := GetSchema(vu.Version)
 			err = ValidateConfig(vu.Obj, s, "root")
-			//t.Logf("expected multierror was: %v", err)
+			// t.Logf("expected multierror was: %v", err)
 			tt.fn()
 		})
 	}
@@ -161,7 +161,7 @@ func Test_Validate_V1_KubernetesValidating(t *testing.T) {
 	var vu *VersionedUntyped
 	var err error
 
-	var tests = []struct {
+	tests := []struct {
 		name       string
 		configText string
 		fn         func()
@@ -198,7 +198,7 @@ kubernetesValidating:
 			t.Logf("version: %s", vu.Version)
 			s := GetSchema(vu.Version)
 			err = ValidateConfig(vu.Obj, s, "root")
-			//t.Logf("expected multierror was: %v", err)
+			// t.Logf("expected multierror was: %v", err)
 			tt.fn()
 		})
 	}
diff --git a/pkg/hook/config/versioned_untyped_test.go b/pkg/hook/config/versioned_untyped_test.go
index 038f8fe..93971f6 100644
--- a/pkg/hook/config/versioned_untyped_test.go
+++ b/pkg/hook/config/versioned_untyped_test.go
@@ -7,11 +7,11 @@ import (
 )
 
 func Test_VersionedUntyped_Load(t *testing.T) {
-	var g = NewWithT(t)
+	g := NewWithT(t)
 	var vu *VersionedUntyped
 	var err error
 
-	var tests = []struct {
+	tests := []struct {
 		name  string
 		input string
 		fn    func()
@@ -131,7 +131,6 @@ schedule:
 			"",
 			``,
 			func() {
-
 			},
 		},
 	}
@@ -144,5 +143,4 @@ schedule:
 			tt.fn()
 		})
 	}
-
 }
diff --git a/pkg/hook/hook_manager_test.go b/pkg/hook/hook_manager_test.go
index e9dd6af..ed2e8a7 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.Settings = app.ValidatingWebhookSettings
-	hm.WithValidatingWebhookManager(validatingManager)
+	admissionManager := admission.NewWebhookManager()
+	admissionManager.Settings = app.ValidatingWebhookSettings
+	hm.WithAdmissionWebhookManager(admissionManager)
 
 	return hm
 }
@@ -74,7 +74,6 @@ func Test_HookManager_GetHookNames(t *testing.T) {
 			t.Fatalf("Hook manager should have hook '%s' at index %d, %s", expectedName, i, names[i])
 		}
 	}
-
 }
 
 func TestHookController_HandleValidatingEvent(t *testing.T) {
@@ -87,26 +86,25 @@ 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)
 	})
 
 	g.Expect(infoList).Should(HaveLen(1))
-
 }
 
 func Test_HookManager_conversion_chains(t *testing.T) {
diff --git a/pkg/hook/hook_test.go b/pkg/hook/hook_test.go
index ca47559..2e190b4 100644
--- a/pkg/hook/hook_test.go
+++ b/pkg/hook/hook_test.go
@@ -128,7 +128,7 @@ func Test_Hook_WithConfig(t *testing.T) {
 				g.Expect(err).Should(HaveOccurred())
 				g.Expect(err.Error()).To(MatchRegexp("onStartup .*must be of type integer: \"string\""))
 
-				//t.Logf("expected validation error was: %v", err)
+				// t.Logf("expected validation error was: %v", err)
 			},
 		},
 	}
diff --git a/pkg/hook/task_metadata/task_metadata_test.go b/pkg/hook/task_metadata/task_metadata_test.go
index 8fd1ab5..406b730 100644
--- a/pkg/hook/task_metadata/task_metadata_test.go
+++ b/pkg/hook/task_metadata/task_metadata_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 func Test_HookMetadata_Access(t *testing.T) {
-	var g = NewWithT(t)
+	g := NewWithT(t)
 
 	Task := task.NewTask(HookRun).
 		WithMetadata(HookMetadata{
@@ -35,7 +35,6 @@ func Test_HookMetadata_Access(t *testing.T) {
 	g.Expect(hm.BindingContext).Should(HaveLen(2))
 	g.Expect(hm.BindingContext[0].Binding).Should(Equal("each_1_min"))
 	g.Expect(hm.BindingContext[1].Binding).Should(Equal("each_5_min"))
-
 }
 
 func Test_HookMetadata_QueueDump_Task_Description(t *testing.T) {
diff --git a/pkg/kube/object_patch/patch_test.go b/pkg/kube/object_patch/patch_test.go
index e39942a..e0dedb9 100644
--- a/pkg/kube/object_patch/patch_test.go
+++ b/pkg/kube/object_patch/patch_test.go
@@ -101,7 +101,7 @@ data:
 	)
 
 	// Filter func to add a new field.
-	var filter = func(u *unstructured.Unstructured) (*unstructured.Unstructured, error) {
+	filter := func(u *unstructured.Unstructured) (*unstructured.Unstructured, error) {
 		res := u.DeepCopy()
 		data := res.Object["data"].(map[string]interface{})
 		data[newField] = newValue
@@ -659,7 +659,6 @@ object:
 					return err
 				}
 				return patcher.ExecuteOperations(operations)
-
 			},
 			shouldNotCreateNew,
 			shouldBeError,
@@ -682,7 +681,6 @@ object:
 					return err
 				}
 				return patcher.ExecuteOperations(operations)
-
 			},
 			shouldNotCreateNew,
 			shouldNotBeError,
@@ -727,7 +725,6 @@ object: |
 					return err
 				}
 				return patcher.ExecuteOperations(operations)
-
 			},
 			shouldCreateNew,
 			shouldNotBeError,
@@ -746,7 +743,6 @@ object: |
 					return err
 				}
 				return patcher.ExecuteOperations(operations)
-
 			},
 			shouldCreateNew,
 			shouldNotBeError,
diff --git a/pkg/kube_events_manager/kube_events_manager_test.go b/pkg/kube_events_manager/kube_events_manager_test.go
index 9339974..b729d94 100644
--- a/pkg/kube_events_manager/kube_events_manager_test.go
+++ b/pkg/kube_events_manager/kube_events_manager_test.go
@@ -35,7 +35,7 @@ func Test_MainKubeEventsManager_Run(t *testing.T) {
 	}
 
 	fakeDiscovery.Resources = []*metav1.APIResourceList{
-		&metav1.APIResourceList{
+		{
 			GroupVersion: "v1",
 			APIResources: []metav1.APIResource{
 				{
@@ -110,7 +110,7 @@ func Test_MainKubeEventsManager_HandleEvents(t *testing.T) {
 	}
 
 	fakeDiscovery.Resources = []*metav1.APIResourceList{
-		&metav1.APIResourceList{
+		{
 			GroupVersion: "v1",
 			APIResources: []metav1.APIResource{
 				{
@@ -237,7 +237,6 @@ func Test_MainKubeEventsManager_HandleEvents(t *testing.T) {
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
-
 }
 
 // Test_FakeClient_CatchUpdates try to catch updates from fake client with additional WatchReactor
@@ -265,7 +264,7 @@ func Test_FakeClient_CatchUpdates(t *testing.T) {
 	}
 
 	fakeDiscovery.Resources = []*metav1.APIResourceList{
-		&metav1.APIResourceList{
+		{
 			GroupVersion: "v1",
 			APIResources: []metav1.APIResource{
 				{
@@ -353,7 +352,7 @@ func Test_FakeClient_CatchUpdates(t *testing.T) {
 		case ev := <-mgr.Ch():
 			eventCounter = eventCounter + 1
 			fmt.Printf("Got event: %d %#v\n", eventCounter, ev)
-			//t.Logf("Got event: %d %#v\n", eventCounter, ev)
+			// t.Logf("Got event: %d %#v\n", eventCounter, ev)
 
 			if !state.podsCreated {
 				assert.Equal(t, "Synchronization", ev.Type)
@@ -419,5 +418,4 @@ func Test_FakeClient_CatchUpdates(t *testing.T) {
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
-
 }
diff --git a/pkg/kube_events_manager/types/types_test.go b/pkg/kube_events_manager/types/types_test.go
index 81717e2..6be882c 100644
--- a/pkg/kube_events_manager/types/types_test.go
+++ b/pkg/kube_events_manager/types/types_test.go
@@ -31,7 +31,7 @@ func Test_ObjectAndFilterResult_ToJson(t *testing.T) {
 	data, err := json.Marshal(obj)
 	assert.NoError(t, err)
 	jsonStr := string(data)
-	//fmt.Printf("%s\n", jsonStr)
+	// fmt.Printf("%s\n", jsonStr)
 	assert.Contains(t, jsonStr, "filterResult")
 }
 
@@ -42,7 +42,7 @@ func Test_ObjectAndFilterResult_ToJson_EmptyFilterResult(t *testing.T) {
 	data, err := json.Marshal(obj)
 	assert.NoError(t, err)
 	jsonStr := string(data)
-	//fmt.Printf("%s\n", jsonStr)
+	// fmt.Printf("%s\n", jsonStr)
 	assert.Contains(t, jsonStr, `"filterResult":null`)
 }
 
@@ -53,7 +53,7 @@ func Test_ObjectAndFilterResult_ToJson_NullFilterResult(t *testing.T) {
 	data, err := json.Marshal(obj)
 	assert.NoError(t, err)
 	jsonStr := string(data)
-	//fmt.Printf("%s\n", jsonStr)
+	// fmt.Printf("%s\n", jsonStr)
 	assert.Contains(t, jsonStr, `"filterResult":null`)
 }
 
@@ -64,7 +64,7 @@ func Test_ObjectAndFilterResult_ToJson_Empty_JQFilter_Has_FilterResult(t *testin
 	data, err := json.Marshal(obj)
 	assert.NoError(t, err)
 	jsonStr := string(data)
-	//fmt.Printf("%s\n", jsonStr)
+	// fmt.Printf("%s\n", jsonStr)
 	assert.Contains(t, jsonStr, "filterResult")
 }
 
diff --git a/pkg/metric_storage/operation/operation_test.go b/pkg/metric_storage/operation/operation_test.go
index 4b45e4f..016508a 100644
--- a/pkg/metric_storage/operation/operation_test.go
+++ b/pkg/metric_storage/operation/operation_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func Test_ValidateMetricOperations(t *testing.T) {
-	//g := NewWithT(t)
+	// g := NewWithT(t)
 	tests := []struct {
 		name     string
 		op       string
@@ -46,7 +46,7 @@ func Test_ValidateMetricOperations(t *testing.T) {
 			if tt.expected {
 				g.Expect(err).ShouldNot(HaveOccurred())
 			} else {
-				//t.Logf("expected error: %v", err)
+				// t.Logf("expected error: %v", err)
 				g.Expect(err).Should(HaveOccurred())
 			}
 		})
diff --git a/pkg/metric_storage/vault/vault_test.go b/pkg/metric_storage/vault/vault_test.go
index 8779550..0a8ad21 100644
--- a/pkg/metric_storage/vault/vault_test.go
+++ b/pkg/metric_storage/vault/vault_test.go
@@ -98,5 +98,4 @@ metric2_total{lbl="val222"} 2
 `
 	err = promtest.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expect), "metric_total", "metric2_total")
 	g.Expect(err).ShouldNot(HaveOccurred())
-
 }
diff --git a/pkg/shell-operator/combine_binding_context_test.go b/pkg/shell-operator/combine_binding_context_test.go
index 61dd935..9afdbf4 100644
--- a/pkg/shell-operator/combine_binding_context_test.go
+++ b/pkg/shell-operator/combine_binding_context_test.go
@@ -25,7 +25,7 @@ func Test_CombineBindingContext_MultipleHooks(t *testing.T) {
 		}
 	})
 
-	var tasks = []task.Task{
+	tasks := []task.Task{
 		task.NewTask(HookRun).
 			WithQueueName("test_multiple_hooks").
 			WithMetadata(HookMetadata{
@@ -118,7 +118,7 @@ func Test_CombineBindingContext_Nil_On_NoCombine(t *testing.T) {
 		}
 	})
 
-	var tasks = []task.Task{
+	tasks := []task.Task{
 		task.NewTask(HookRun).
 			WithQueueName("test_no_combine").
 			WithMetadata(HookMetadata{
@@ -179,7 +179,7 @@ func Test_CombineBindingContext_Group_Compaction(t *testing.T) {
 	bcMeta := binding_context.BindingContext{}.Metadata
 	bcMeta.Group = "pods"
 
-	var tasks = []task.Task{
+	tasks := []task.Task{
 		// 3 tasks with Group should be compacted
 		task.NewTask(HookRun).
 			WithQueueName("test_multiple_hooks").
@@ -285,7 +285,7 @@ func Test_CombineBindingContext_Group_Type(t *testing.T) {
 	schMeta.Group = "pods"
 	schMeta.BindingType = types.Schedule
 
-	var tasks = []task.Task{
+	tasks := []task.Task{
 		task.NewTask(HookRun).
 			WithQueueName("test_multiple_hooks").
 			WithMetadata(HookMetadata{
diff --git a/pkg/task/queue/task_queue_test.go b/pkg/task/queue/task_queue_test.go
index 38bed0b..1869fb9 100644
--- a/pkg/task/queue/task_queue_test.go
+++ b/pkg/task/queue/task_queue_test.go
@@ -137,7 +137,6 @@ func Test_ExponentialBackoff(t *testing.T) {
 	g.Expect(mean).Should(BeNumerically(">", mockExponentialDelay),
 		"mean delay of %d fails should be more than %s, got %s. Check exponential delaying not broken in Start or waitForTask.",
 		fails, mockExponentialDelay.String(), mean.Truncate(100*time.Microsecond).String())
-
 }
 
 func calculateMeanDelay(in []time.Time) (mean time.Duration, deltas []int64) {
diff --git a/pkg/utils/exponential_backoff/delay_test.go b/pkg/utils/exponential_backoff/delay_test.go
index 464a0f1..b731feb 100644
--- a/pkg/utils/exponential_backoff/delay_test.go
+++ b/pkg/utils/exponential_backoff/delay_test.go
@@ -30,6 +30,6 @@ func Test_Delay(t *testing.T) {
 		}
 
 		// debug messages
-		//t.Logf("Delay for %02d retry: %s\n", i, delay)
+		// t.Logf("Delay for %02d retry: %s\n", i, delay)
 	}
 }
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..8d55a06 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"
@@ -18,7 +18,6 @@ func Test_Manager_AddWebhook(t *testing.T) {
 	m.Settings = vs
 
 	err := m.Init()
-
 	if err != nil {
 		t.Fatalf("WebhookManager should init: %v", err)
 	}
@@ -45,15 +44,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))
 		}
 	}
 }
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/conversion/chain_test.go b/pkg/webhook/conversion/chain_test.go
index cf9205a..83ec9df 100644
--- a/pkg/webhook/conversion/chain_test.go
+++ b/pkg/webhook/conversion/chain_test.go
@@ -1,9 +1,9 @@
 package conversion
 
 import (
-	. "github.com/onsi/gomega"
-
 	"testing"
+
+	. "github.com/onsi/gomega"
 )
 
 func Test_VersionsMatched(t *testing.T) {
diff --git a/test/hook/context/generator_test.go b/test/hook/context/generator_test.go
index 0f16266..d384666 100644
--- a/test/hook/context/generator_test.go
+++ b/test/hook/context/generator_test.go
@@ -178,7 +178,6 @@ spec:
 `)
 	g.Expect(err).ShouldNot(HaveOccurred())
 	g.Expect(bindingContexts.Rendered).To(ContainSubstring("MyResource"))
-
 }
 
 func Test_PreferredGVR(t *testing.T) {
@@ -354,7 +353,6 @@ metadata:
 	g.Expect(string(parsedBindingContexts[0].Type)).To(Equal("Synchronization"))
 	g.Expect(parsedBindingContexts[0].Objects).To(HaveLen(2))
 	g.Expect(parsedBindingContexts[0].Snapshots["selected_pods"]).To(HaveLen(2))
-
 }
 
 func Test_RunSchedule(t *testing.T) {
diff --git a/test/hook/context/state.go b/test/hook/context/state.go
index 1136fdb..d99a965 100644
--- a/test/hook/context/state.go
+++ b/test/hook/context/state.go
@@ -61,7 +61,7 @@ func (c *StateController) ChangeState(newRawState string) error {
 		return fmt.Errorf("error while changing state: %v", err)
 	}
 
-	var newState = make(map[string]manifest.Manifest)
+	newState := make(map[string]manifest.Manifest)
 
 	// Create new objects in FakeCluster
 	for _, m := range newManifests {
diff --git a/test/integration/kube_event_manager/kube_event_manager_test.go b/test/integration/kube_event_manager/kube_event_manager_test.go
index b76030b..bda2455 100644
--- a/test/integration/kube_event_manager/kube_event_manager_test.go
+++ b/test/integration/kube_event_manager/kube_event_manager_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package kube_event_manager_test
@@ -14,7 +15,7 @@ import (
 	"github.com/flant/shell-operator/pkg/kube_events_manager"
 	. "github.com/flant/shell-operator/pkg/kube_events_manager/types"
 	. "github.com/flant/shell-operator/test/integration/suite"
-	. "github.com/flant/shell-operator/test/utils"
+	testutils "github.com/flant/shell-operator/test/utils"
 )
 
 func Test(t *testing.T) {
@@ -74,7 +75,7 @@ var _ = Describe("Binding 'kubernetes' with kind 'Pod' should emit KubeEvent obj
 				m := KubeEventsManager.GetMonitor(monitorConfig.Metadata.MonitorId)
 				m.EnableKubeEventCb()
 
-				Kubectl(ContextName).Apply("default", "testdata/test-pod.yaml")
+				testutils.Kubectl(ContextName).Apply("default", "testdata/test-pod.yaml")
 			})
 
 			It("should return KubeEvent with type 'Event'", func() {
@@ -92,7 +93,7 @@ var _ = Describe("Binding 'kubernetes' with kind 'Pod' should emit KubeEvent obj
 			}, 25)
 
 			AfterEach(func() {
-				Kubectl(ContextName).Delete("default", "pod/test")
+				testutils.Kubectl(ContextName).Delete("default", "pod/test")
 			})
 		})
 	})
diff --git a/test/integration/kubeclient/kube_client_test.go b/test/integration/kubeclient/kube_client_test.go
index 19abf48..0510ac6 100644
--- a/test/integration/kubeclient/kube_client_test.go
+++ b/test/integration/kubeclient/kube_client_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package kubeclient_test
@@ -19,7 +20,6 @@ func Test(t *testing.T) {
 
 var _ = Describe("Kubernetes API client package", func() {
 	When("client connect outside of the cluster", func() {
-
 		It("should list deployments", func() {
 			list, err := KubeClient.AppsV1().Deployments("").List(context.TODO(), metav1.ListOptions{})
 			Ω(err).Should(Succeed())
diff --git a/test/integration/kubeclient/object_patch_test.go b/test/integration/kubeclient/object_patch_test.go
index 71f1e5f..788dac7 100644
--- a/test/integration/kubeclient/object_patch_test.go
+++ b/test/integration/kubeclient/object_patch_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package kubeclient_test
@@ -102,9 +103,7 @@ var _ = Describe("Kubernetes API object patching", func() {
 	})
 
 	Context("deleting an object", func() {
-		var (
-			testCM *corev1.ConfigMap
-		)
+		var testCM *corev1.ConfigMap
 
 		BeforeEach(func() {
 			testCM = testObject.DeepCopy()
@@ -119,7 +118,6 @@ var _ = Describe("Kubernetes API object patching", func() {
 		})
 
 		It("should successfully delete an object", func() {
-
 			err := ObjectPatcher.ExecuteOperation(object_patch.NewDeleteOperation(
 				testCM.APIVersion, testCM.Kind, testCM.Namespace, testCM.Name,
 				object_patch.InBackground()))
@@ -136,9 +134,7 @@ var _ = Describe("Kubernetes API object patching", func() {
 	})
 
 	Context("patching an object", func() {
-		var (
-			testCM *corev1.ConfigMap
-		)
+		var testCM *corev1.ConfigMap
 
 		BeforeEach(func() {
 			testCM = testObject.DeepCopy()
@@ -153,7 +149,6 @@ var _ = Describe("Kubernetes API object patching", func() {
 		})
 
 		It("should successfully JQPatch an object", func() {
-
 			err := ObjectPatcher.ExecuteOperation(object_patch.NewFromOperationSpec(object_patch.OperationSpec{
 				Operation:  object_patch.JQPatch,
 				ApiVersion: testCM.APIVersion,
diff --git a/test/integration/suite/run.go b/test/integration/suite/run.go
index 7008d42..afb1f98 100644
--- a/test/integration/suite/run.go
+++ b/test/integration/suite/run.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package suite
diff --git a/test/utils/assert.go b/test/utils/assert.go
index 8077bb0..a2c6bf4 100644
--- a/test/utils/assert.go
+++ b/test/utils/assert.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/assert_test.go b/test/utils/assert_test.go
index 28a4e02..9dadc01 100644
--- a/test/utils/assert_test.go
+++ b/test/utils/assert_test.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/jqmatcher.go b/test/utils/jqmatcher.go
index 16dbf2f..226821d 100644
--- a/test/utils/jqmatcher.go
+++ b/test/utils/jqmatcher.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
@@ -12,8 +13,7 @@ import (
 	. "github.com/flant/libjq-go"
 )
 
-//var _ = Ω(verBcs).To(MatchJq(`.[0] | has("objects")`, Equal(true)))
-
+// var _ = Ω(verBcs).To(MatchJq(`.[0] | has("objects")`, Equal(true)))
 func MatchJq(jqExpression string, matcher interface{}) types.GomegaMatcher {
 	return &matchJq{
 		JqExpr:  jqExpression,
@@ -42,6 +42,7 @@ func (matcher *matchJq) Match(actual interface{}) (success bool, err error) {
 		matcher.InputString = string(inputBytes)
 	}
 
+	// nolint:typecheck // Ignore false positive: undeclared name: `Jq`.
 	res, err := Jq().Program(matcher.JqExpr).Run(matcher.InputString)
 	if err != nil {
 		return false, fmt.Errorf("MatchJq apply jq expression: %s\n", err)
diff --git a/test/utils/jsonloganalyzer.go b/test/utils/jsonloganalyzer.go
index d3f9c7a..d24a722 100644
--- a/test/utils/jsonloganalyzer.go
+++ b/test/utils/jsonloganalyzer.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
@@ -96,7 +97,7 @@ func (a *JsonLogAnalyzer) HandleLine(line string) {
 	}
 
 	if a.finished && a.OnStopFn != nil {
-		//fmt.Printf("analyzer.HandleLine OnStopFn()\n")
+		// fmt.Printf("analyzer.HandleLine OnStopFn()\n")
 		a.OnStopFn()
 	}
 }
@@ -156,7 +157,6 @@ func NewJsonLogMatcher(steps ...*MatcherStep) *JsonLogMatcher {
 }
 
 func (m *JsonLogMatcher) HandleRecord(r JsonLogRecord) {
-
 }
 
 func (m *JsonLogMatcher) HandleLine(line string) {
@@ -177,19 +177,19 @@ func (m *JsonLogMatcher) HandleLine(line string) {
 	if m.finished && step.Matcher != nil {
 		return
 	}
-	//Ω(line).Should(HavePrefix("{"))
+	// Ω(line).Should(HavePrefix("{"))
 
 	logRecord, _ := NewJsonLogRecord().FromString(line)
-	//Ω(err).ShouldNot(HaveOccurred())
+	// Ω(err).ShouldNot(HaveOccurred())
 
 	var err error
 	if step.Matcher != nil {
 		res := step.Matcher(logRecord)
 		if res {
 			if step.OnMatch != nil {
-				//fmt.Printf("matcher.HandleLine OnMatch()\n")
+				// fmt.Printf("matcher.HandleLine OnMatch()\n")
 				err = step.OnMatch(logRecord)
-				//fmt.Printf("matcher.HandleLine OnMatchDone()\n")
+				// fmt.Printf("matcher.HandleLine OnMatchDone()\n")
 				if err != nil {
 					m.err = err
 					m.finished = true
diff --git a/test/utils/jsonlogrecord.go b/test/utils/jsonlogrecord.go
index 4d07e77..f8eb7e5 100644
--- a/test/utils/jsonlogrecord.go
+++ b/test/utils/jsonlogrecord.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/jsonlogrecord_test.go b/test/utils/jsonlogrecord_test.go
index 32f6106..b39d806 100644
--- a/test/utils/jsonlogrecord_test.go
+++ b/test/utils/jsonlogrecord_test.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/kubectl.go b/test/utils/kubectl.go
index 6c432a5..c63bb7a 100644
--- a/test/utils/kubectl.go
+++ b/test/utils/kubectl.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/prometheus.go b/test/utils/prometheus.go
index 8aa8437..926385d 100644
--- a/test/utils/prometheus.go
+++ b/test/utils/prometheus.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
@@ -127,11 +128,9 @@ func (p *PromScraper) Scrape() error {
 }
 
 func (p *PromScraper) HasMetric(name string) {
-
 }
 
 func (p *PromScraper) MetricEquals(name string) {
-
 }
 
 // FindExact searches metric by name and labels
diff --git a/test/utils/shell-operator.go b/test/utils/shell-operator.go
index 8ec94ae..3cc2e97 100644
--- a/test/utils/shell-operator.go
+++ b/test/utils/shell-operator.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
diff --git a/test/utils/streamedexec.go b/test/utils/streamedexec.go
index 7e5efa2..f610aa8 100644
--- a/test/utils/streamedexec.go
+++ b/test/utils/streamedexec.go
@@ -1,3 +1,4 @@
+//go:build test
 // +build test
 
 package utils
@@ -40,7 +41,7 @@ func StreamedExecCommand(cmd *exec.Cmd, opts CommandOptions) error {
 		return fmt.Errorf("unable to create os pipe for stderr: %s", err)
 	}
 
-	//outputReader := io.MultiReader(stdoutReadPipe, stderrReadPipe)
+	// outputReader := io.MultiReader(stdoutReadPipe, stderrReadPipe)
 
 	fmt.Printf("gexec.Start\n")
 	session, err := gexec.Start(cmd, stdoutWritePipe, stderrWritePipe)