Repository: kumahq/kuma
Pull request #1404 (2021-01-11 13:06:26)

feat(kumactl) new column 'NOTES' for displaying errors
### Summary

New column `NOTES` in `kumactl inspect dataplane` output for displaying potential errors.

### Documentation

- [ ] Link to the website [documentation PR](https://github.com/kumahq/kuma-website/pull/XXX)



Patch:
diff --git a/api/go.sum b/api/go.sum
index 87d90efea..ca7e9490f 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,5 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -15,7 +16,9 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/envoyproxy/protoc-gen-validate v0.4.1 h1:7dLaJvASGRD7X49jSCSXXHwKPm0ZN9r9kJD+p+vS7dM=
 github.com/envoyproxy/protoc-gen-validate v0.4.1/go.mod h1:E+IEazqdaWv3FrnGtZIu3b9fPFMK8AzeTTrk9SfVwWs=
 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=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -116,6 +119,7 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -141,6 +145,7 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/app/kumactl/cmd/inspect/inspect_dataplanes.go b/app/kumactl/cmd/inspect/inspect_dataplanes.go
index 94b74ff25..d78e97368 100644
--- a/app/kumactl/cmd/inspect/inspect_dataplanes.go
+++ b/app/kumactl/cmd/inspect/inspect_dataplanes.go
@@ -2,8 +2,10 @@ package inspect
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/pkg/errors"
@@ -64,29 +66,32 @@ func newInspectDataplanesCmd(pctx *inspectContext) *cobra.Command {
 	return cmd
 }
 
-func getStatus(dataplane *mesh_proto.Dataplane, dataplaneInsight *mesh_proto.DataplaneInsight) string {
+func getStatus(dataplane *mesh_proto.Dataplane, dataplaneInsight *mesh_proto.DataplaneInsight) (string, []string) {
 	proxyOnline := dataplaneInsight.IsOnline()
-	allInboundsOffline := true
-	allInboundsOnline := true
+
+	var errs []string
+
 	for _, inbound := range dataplane.Networking.Inbound {
-		if inbound.Health == nil || inbound.Health.Ready {
-			allInboundsOffline = false
-		} else {
-			allInboundsOnline = false
+		if inbound.Health != nil && !inbound.Health.Ready {
+			errs = append(errs, fmt.Sprintf("inbound[port=%d,svc=%s] is not ready", inbound.Port, inbound.Tags[mesh_proto.ServiceTag]))
 		}
 	}
+
+	allInboundsOffline := len(errs) == len(dataplane.Networking.Inbound)
+	allInboundsOnline := len(errs) == 0
+
 	if !proxyOnline || allInboundsOffline {
-		return "Offline"
+		return "Offline", errs
 	}
 	if !allInboundsOnline {
-		return "Partially degraded"
+		return "Partially degraded", errs
 	}
-	return "Online"
+	return "Online", nil
 }
 
 func printDataplaneOverviews(now time.Time, dataplaneInsights *mesh_core.DataplaneOverviewResourceList, out io.Writer) error {
 	data := printers.Table{
-		Headers: []string{"MESH", "NAME", "TAGS", "STATUS", "LAST CONNECTED AGO", "LAST UPDATED AGO", "TOTAL UPDATES", "TOTAL ERRORS", "CERT REGENERATED AGO", "CERT EXPIRATION", "CERT REGENERATIONS", "KUMA-DP VERSION", "ENVOY VERSION"},
+		Headers: []string{"MESH", "NAME", "TAGS", "STATUS", "LAST CONNECTED AGO", "LAST UPDATED AGO", "TOTAL UPDATES", "TOTAL ERRORS", "CERT REGENERATED AGO", "CERT EXPIRATION", "CERT REGENERATIONS", "KUMA-DP VERSION", "ENVOY VERSION", "NOTES"},
 		NextRow: func() func() []string {
 			i := 0
 			return func() []string {
@@ -105,7 +110,7 @@ func printDataplaneOverviews(now time.Time, dataplaneInsights *mesh_core.Datapla
 				totalResponsesRejected := dataplaneInsight.Sum(func(s *mesh_proto.DiscoverySubscription) uint64 {
 					return s.GetStatus().GetTotal().GetResponsesRejected()
 				})
-				status := getStatus(dataplane, dataplaneInsight)
+				status, errs := getStatus(dataplane, dataplaneInsight)
 				lastUpdated := util_proto.MustTimestampFromProto(lastSubscription.GetStatus().GetLastUpdateTime())
 
 				var certExpiration *time.Time
@@ -144,6 +149,7 @@ func printDataplaneOverviews(now time.Time, dataplaneInsights *mesh_core.Datapla
 					certRegenerations,                    // CERT REGENERATIONS
 					kumaDpVersion,                        // KUMA-DP VERSION
 					envoyVersion,                         // ENVOY VERSION
+					strings.Join(errs, ";"),              // NOTES
 				}
 			}
 		}(),
diff --git a/go.sum b/go.sum
index 1e507c1ad..2a4d1cc04 100644
--- a/go.sum
+++ b/go.sum
@@ -930,6 +930,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@@ -1624,6 +1625,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
 gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
@@ -1770,6 +1772,7 @@ sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
+sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM=
 sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
diff --git a/pkg/core/xds/resource.go b/pkg/core/xds/resource.go
index c7b88ced8..7a8c673ac 100644
--- a/pkg/core/xds/resource.go
+++ b/pkg/core/xds/resource.go
@@ -5,7 +5,8 @@ import (
 
 	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
-	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v2"
+	envoy_resource_v2 "github.com/envoyproxy/go-control-plane/pkg/resource/v2"
+	envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/ptypes"
 )
@@ -146,10 +147,15 @@ func (s *ResourceSet) List() ResourceList {
 		return nil
 	}
 	list := ResourceList{}
-	list = append(list, s.ListOf(envoy_resource.EndpointType)...)
-	list = append(list, s.ListOf(envoy_resource.ClusterType)...)
-	list = append(list, s.ListOf(envoy_resource.RouteType)...)
-	list = append(list, s.ListOf(envoy_resource.ListenerType)...)
-	list = append(list, s.ListOf(envoy_resource.SecretType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.EndpointType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.EndpointType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.ClusterType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.ClusterType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.RouteType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.RouteType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.ListenerType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.ListenerType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.SecretType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.SecretType)...)
 	return list
 }
diff --git a/pkg/plugins/resources/k8s/native/go.sum b/pkg/plugins/resources/k8s/native/go.sum
index 23c4ed2e4..de88f60be 100644
--- a/pkg/plugins/resources/k8s/native/go.sum
+++ b/pkg/plugins/resources/k8s/native/go.sum
@@ -1,5 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
@@ -24,6 +25,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@@ -45,6 +47,7 @@ github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@@ -71,6 +74,7 @@ github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQo
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 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=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -78,7 +82,9 @@ github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54=
 github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
 github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -126,9 +132,11 @@ github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85n
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -154,6 +162,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -164,6 +173,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
 github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@@ -174,6 +184,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -211,12 +222,15 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -248,11 +262,14 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -275,6 +292,7 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -301,8 +319,11 @@ go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL
 go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -347,6 +368,7 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/Lt
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -385,6 +407,7 @@ golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -404,7 +427,9 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0=
 gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -440,10 +465,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -476,9 +503,11 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8
 k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 h1:v8ud2Up6QK1lNOKFgiIVrZdMg7MpmSnvtrOieolJKoE=
@@ -487,6 +516,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT
 sigs.k8s.io/controller-runtime v0.6.4 h1:4013CKsBs5bEqo+LevzDett+LLxag/FjQWG94nVZ/9g=
 sigs.k8s.io/controller-runtime v0.6.4/go.mod h1:WlZNXcM0++oyaQt4B7C2lEE5JYRs8vJUzRP4N4JpdAY=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
index b5019132c..a7e5a1fab 100644
--- a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
@@ -16,6 +16,7 @@ func (e *EdsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
 	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_EDS}
 	c.EdsClusterConfig = &envoy_cluster.Cluster_EdsClusterConfig{
 		EdsConfig: &envoy_core.ConfigSource{
+			ResourceApiVersion: envoy_core.ApiVersion_V3,
 			ConfigSourceSpecifier: &envoy_core.ConfigSource_Ads{
 				Ads: &envoy_core.AggregatedConfigSource{},
 			},
diff --git a/pkg/xds/generator/modifications/modifications.go b/pkg/xds/generator/modifications/modifications.go
index c9eead727..9f5f0a665 100644
--- a/pkg/xds/generator/modifications/modifications.go
+++ b/pkg/xds/generator/modifications/modifications.go
@@ -5,40 +5,18 @@ import (
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	modifications_v2 "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
+	modifications_v3 "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
 )
 
-// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
-const OriginProxyTemplateModifications = "proxy-template-modifications"
-
-type modificator interface {
-	apply(*core_xds.ResourceSet) error
-}
-
-func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
-	for i, modification := range modifications {
-		var modificator modificator
-		switch modification.Type.(type) {
-		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
-			mod := clusterModificator(*modification.GetCluster())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
-			mod := listenerModificator(*modification.GetListener())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
-			mod := networkFilterModificator(*modification.GetNetworkFilter())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
-			mod := httpFilterModificator(*modification.GetHttpFilter())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
-			mod := virtualHostModificator(*modification.GetVirtualHost())
-			modificator = &mod
-		default:
-			return errors.Errorf("invalid modification type %T", modification.Type)
-		}
-		if err := modificator.apply(resources); err != nil {
-			return errors.Wrapf(err, "could not apply %d modification", i)
-		}
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications, apiVersion envoy_common.APIVersion) error {
+	switch apiVersion {
+	case envoy_common.APIV2:
+		return modifications_v2.Apply(resources, modifications)
+	case envoy_common.APIV3:
+		return modifications_v3.Apply(resources, modifications)
+	default:
+		return errors.Errorf("unknown API version %s", apiVersion)
 	}
-	return nil
 }
diff --git a/pkg/xds/generator/modifications/cluster.go b/pkg/xds/generator/modifications/v2/cluster.go
similarity index 98%
rename from pkg/xds/generator/modifications/cluster.go
rename to pkg/xds/generator/modifications/v2/cluster.go
index 083ad04f5..8c0cedf63 100644
--- a/pkg/xds/generator/modifications/cluster.go
+++ b/pkg/xds/generator/modifications/v2/cluster.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/http_filter.go b/pkg/xds/generator/modifications/v2/http_filter.go
similarity index 99%
rename from pkg/xds/generator/modifications/http_filter.go
rename to pkg/xds/generator/modifications/v2/http_filter.go
index 7f749e991..b52b8667d 100644
--- a/pkg/xds/generator/modifications/http_filter.go
+++ b/pkg/xds/generator/modifications/v2/http_filter.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/listener.go b/pkg/xds/generator/modifications/v2/listener.go
similarity index 98%
rename from pkg/xds/generator/modifications/listener.go
rename to pkg/xds/generator/modifications/v2/listener.go
index 19c7332b9..fc5616bf0 100644
--- a/pkg/xds/generator/modifications/listener.go
+++ b/pkg/xds/generator/modifications/v2/listener.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/v2/modifications.go b/pkg/xds/generator/modifications/v2/modifications.go
new file mode 100644
index 000000000..5239c77c8
--- /dev/null
+++ b/pkg/xds/generator/modifications/v2/modifications.go
@@ -0,0 +1,44 @@
+package v2
+
+import (
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+)
+
+// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
+const OriginProxyTemplateModifications = "proxy-template-modifications"
+
+type modificator interface {
+	apply(*core_xds.ResourceSet) error
+}
+
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
+	for i, modification := range modifications {
+		var modificator modificator
+		switch modification.Type.(type) {
+		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
+			mod := clusterModificator(*modification.GetCluster())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
+			mod := listenerModificator(*modification.GetListener())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
+			mod := networkFilterModificator(*modification.GetNetworkFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
+			mod := httpFilterModificator(*modification.GetHttpFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+			mod := virtualHostModificator(*modification.GetVirtualHost())
+			modificator = &mod
+		default:
+			return errors.Errorf("invalid modification type %T", modification.Type)
+		}
+		if err := modificator.apply(resources); err != nil {
+			return errors.Wrapf(err, "could not apply %d modification", i)
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/generator/modifications/network_filter.go b/pkg/xds/generator/modifications/v2/network_filter.go
similarity index 99%
rename from pkg/xds/generator/modifications/network_filter.go
rename to pkg/xds/generator/modifications/v2/network_filter.go
index dfe478a6f..0fb3c4e9e 100644
--- a/pkg/xds/generator/modifications/network_filter.go
+++ b/pkg/xds/generator/modifications/v2/network_filter.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/virtual_host.go b/pkg/xds/generator/modifications/v2/virtual_host.go
similarity index 99%
rename from pkg/xds/generator/modifications/virtual_host.go
rename to pkg/xds/generator/modifications/v2/virtual_host.go
index 897d25bac..3fe072334 100644
--- a/pkg/xds/generator/modifications/virtual_host.go
+++ b/pkg/xds/generator/modifications/v2/virtual_host.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/v3/cluster.go b/pkg/xds/generator/modifications/v3/cluster.go
new file mode 100644
index 000000000..7a23333a3
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/cluster.go
@@ -0,0 +1,66 @@
+package v3
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/proto"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type clusterModificator mesh_proto.ProxyTemplate_Modifications_Cluster
+
+func (c *clusterModificator) apply(resources *core_xds.ResourceSet) error {
+	clusterMod := &envoy_cluster.Cluster{}
+	if err := util_proto.FromYAML([]byte(c.Value), clusterMod); err != nil {
+		return err
+	}
+	switch c.Operation {
+	case mesh_proto.OpAdd:
+		c.add(resources, clusterMod)
+	case mesh_proto.OpRemove:
+		c.remove(resources)
+	case mesh_proto.OpPatch:
+		c.patch(resources, clusterMod)
+	default:
+		return errors.Errorf("invalid operation: %s", c.Operation)
+	}
+	return nil
+}
+
+func (c *clusterModificator) patch(resources *core_xds.ResourceSet, clusterMod *envoy_cluster.Cluster) {
+	for _, cluster := range resources.Resources(envoy_resource.ClusterType) {
+		if c.clusterMatches(cluster) {
+			proto.Merge(cluster.Resource, clusterMod)
+		}
+	}
+}
+
+func (c *clusterModificator) remove(resources *core_xds.ResourceSet) {
+	for name, resource := range resources.Resources(envoy_resource.ClusterType) {
+		if c.clusterMatches(resource) {
+			resources.Remove(envoy_resource.ClusterType, name)
+		}
+	}
+}
+
+func (c *clusterModificator) add(resources *core_xds.ResourceSet, clusterMod *envoy_cluster.Cluster) *core_xds.ResourceSet {
+	return resources.Add(&core_xds.Resource{
+		Name:     clusterMod.Name,
+		Origin:   OriginProxyTemplateModifications,
+		Resource: clusterMod,
+	})
+}
+
+func (c *clusterModificator) clusterMatches(cluster *core_xds.Resource) bool {
+	if c.Match.GetName() != "" && c.Match.GetName() != cluster.Name {
+		return false
+	}
+	if c.Match.GetOrigin() != "" && c.Match.GetOrigin() != cluster.Origin {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/modifications/v3/http_filter.go b/pkg/xds/generator/modifications/v3/http_filter.go
new file mode 100644
index 000000000..1f7082fb1
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/http_filter.go
@@ -0,0 +1,147 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/ptypes"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type httpFilterModificator mesh_proto.ProxyTemplate_Modifications_HttpFilter
+
+func (h *httpFilterModificator) apply(resources *core_xds.ResourceSet) error {
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if h.listenerMatches(resource) {
+			listener := resource.Resource.(*envoy_listener.Listener)
+			for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+				for _, networkFilter := range chain.Filters {
+					if networkFilter.Name == "envoy.filters.network.http_connection_manager" {
+						hcm := &envoy_hcm.HttpConnectionManager{}
+						err := ptypes.UnmarshalAny(networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig, hcm)
+						if err != nil {
+							return err
+						}
+						if err := h.applyHCMModification(hcm); err != nil {
+							return err
+						}
+						any, err := util_proto.MarshalAnyDeterministic(hcm)
+						if err != nil {
+							return err
+						}
+						networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig = any
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) applyHCMModification(hcm *envoy_hcm.HttpConnectionManager) error {
+	filter := &envoy_hcm.HttpFilter{}
+	if err := util_proto.FromYAML([]byte(h.Value), filter); err != nil {
+		return err
+	}
+	switch h.Operation {
+	case mesh_proto.OpAddFirst:
+		h.addFirst(hcm, filter)
+	case mesh_proto.OpAddLast:
+		h.addLast(hcm, filter)
+	case mesh_proto.OpAddAfter:
+		h.addAfter(hcm, filter)
+	case mesh_proto.OpAddBefore:
+		h.addBefore(hcm, filter)
+	case mesh_proto.OpRemove:
+		h.remove(hcm)
+	case mesh_proto.OpPatch:
+		if err := h.patch(hcm, filter); err != nil {
+			return errors.Wrap(err, "could not patch the resource")
+		}
+	default:
+		return errors.Errorf("invalid operation: %s", h.Operation)
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) patch(hcm *envoy_hcm.HttpConnectionManager, filterPatch *envoy_hcm.HttpFilter) error {
+	for _, filter := range hcm.HttpFilters {
+		if h.filterMatches(filter) {
+			any, err := util_proto.MergeAnys(filter.GetTypedConfig(), filterPatch.GetTypedConfig())
+			if err != nil {
+				return err
+			}
+
+			filter.ConfigType = &envoy_hcm.HttpFilter_TypedConfig{
+				TypedConfig: any,
+			}
+		}
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) remove(hcm *envoy_hcm.HttpConnectionManager) {
+	var filters []*envoy_hcm.HttpFilter
+	for _, filter := range hcm.HttpFilters {
+		if !h.filterMatches(filter) {
+			filters = append(filters, filter)
+		}
+	}
+	hcm.HttpFilters = filters
+}
+
+func (h *httpFilterModificator) addBefore(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	idx := h.indexOfMatchedFilter(hcm)
+	if idx != -1 {
+		hcm.HttpFilters = append(hcm.HttpFilters, nil)
+		copy(hcm.HttpFilters[idx+1:], hcm.HttpFilters[idx:])
+		hcm.HttpFilters[idx] = filterMod
+	}
+}
+
+func (h *httpFilterModificator) addAfter(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	idx := h.indexOfMatchedFilter(hcm)
+	if idx != -1 {
+		hcm.HttpFilters = append(hcm.HttpFilters, nil)
+		copy(hcm.HttpFilters[idx+2:], hcm.HttpFilters[idx+1:])
+		hcm.HttpFilters[idx+1] = filterMod
+	}
+}
+
+func (h *httpFilterModificator) addLast(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	hcm.HttpFilters = append(hcm.HttpFilters, filterMod)
+}
+
+func (h *httpFilterModificator) addFirst(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	hcm.HttpFilters = append([]*envoy_hcm.HttpFilter{filterMod}, hcm.HttpFilters...)
+}
+
+func (h *httpFilterModificator) filterMatches(filter *envoy_hcm.HttpFilter) bool {
+	if h.Match.GetName() != "" && h.Match.GetName() != filter.Name {
+		return false
+	}
+	return true
+}
+
+func (h *httpFilterModificator) listenerMatches(resource *core_xds.Resource) bool {
+	if h.Match.GetListenerName() != "" && h.Match.GetListenerName() != resource.Name {
+		return false
+	}
+	if h.Match.GetOrigin() != "" && h.Match.GetOrigin() != resource.Origin {
+		return false
+	}
+	return true
+}
+
+func (h *httpFilterModificator) indexOfMatchedFilter(hcm *envoy_hcm.HttpConnectionManager) int {
+	for i, filter := range hcm.HttpFilters {
+		if filter.Name == h.Match.Name {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/pkg/xds/generator/modifications/v3/listener.go b/pkg/xds/generator/modifications/v3/listener.go
new file mode 100644
index 000000000..6df0cf586
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/listener.go
@@ -0,0 +1,66 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/proto"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type listenerModificator mesh_proto.ProxyTemplate_Modifications_Listener
+
+func (l *listenerModificator) apply(resources *core_xds.ResourceSet) error {
+	listener := &envoy_listener.Listener{}
+	if err := util_proto.FromYAML([]byte(l.Value), listener); err != nil {
+		return err
+	}
+	switch l.Operation {
+	case mesh_proto.OpAdd:
+		l.app(resources, listener)
+	case mesh_proto.OpRemove:
+		l.remove(resources)
+	case mesh_proto.OpPatch:
+		l.patch(resources, listener)
+	default:
+		return errors.Errorf("invalid operation: %s", l.Operation)
+	}
+	return nil
+}
+
+func (l *listenerModificator) patch(resources *core_xds.ResourceSet, listenerPatch *envoy_listener.Listener) {
+	for _, listener := range resources.Resources(envoy_resource.ListenerType) {
+		if l.listenerMatches(listener) {
+			proto.Merge(listener.Resource, listenerPatch)
+		}
+	}
+}
+
+func (l *listenerModificator) remove(resources *core_xds.ResourceSet) {
+	for name, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if l.listenerMatches(resource) {
+			resources.Remove(envoy_resource.ListenerType, name)
+		}
+	}
+}
+
+func (l *listenerModificator) app(resources *core_xds.ResourceSet, listener *envoy_listener.Listener) *core_xds.ResourceSet {
+	return resources.Add(&core_xds.Resource{
+		Name:     listener.Name,
+		Origin:   OriginProxyTemplateModifications,
+		Resource: listener,
+	})
+}
+
+func (l *listenerModificator) listenerMatches(listener *core_xds.Resource) bool {
+	if l.Match.GetName() != "" && l.Match.GetName() != listener.Name {
+		return false
+	}
+	if l.Match.GetOrigin() != "" && l.Match.GetOrigin() != listener.Origin {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/modifications/v3/modifications.go b/pkg/xds/generator/modifications/v3/modifications.go
new file mode 100644
index 000000000..e79c37fb5
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/modifications.go
@@ -0,0 +1,44 @@
+package v3
+
+import (
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+)
+
+// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
+const OriginProxyTemplateModifications = "proxy-template-modifications"
+
+type modificator interface {
+	apply(*core_xds.ResourceSet) error
+}
+
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
+	for i, modification := range modifications {
+		var modificator modificator
+		switch modification.Type.(type) {
+		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
+			mod := clusterModificator(*modification.GetCluster())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
+			mod := listenerModificator(*modification.GetListener())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
+			mod := networkFilterModificator(*modification.GetNetworkFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
+			mod := httpFilterModificator(*modification.GetHttpFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+			mod := virtualHostModificator(*modification.GetVirtualHost())
+			modificator = &mod
+		default:
+			return errors.Errorf("invalid modification type %T", modification.Type)
+		}
+		if err := modificator.apply(resources); err != nil {
+			return errors.Wrapf(err, "could not apply %d modification", i)
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/generator/modifications/v3/network_filter.go b/pkg/xds/generator/modifications/v3/network_filter.go
new file mode 100644
index 000000000..ef0e104b8
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/network_filter.go
@@ -0,0 +1,124 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type networkFilterModificator mesh_proto.ProxyTemplate_Modifications_NetworkFilter
+
+func (n *networkFilterModificator) apply(resources *core_xds.ResourceSet) error {
+	filter := &envoy_listener.Filter{}
+	if err := util_proto.FromYAML([]byte(n.Value), filter); err != nil {
+		return err
+	}
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if n.listenerMatches(resource) {
+			listener := resource.Resource.(*envoy_listener.Listener)
+			for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+				switch n.Operation {
+				case mesh_proto.OpAddFirst:
+					n.addFirst(chain, filter)
+				case mesh_proto.OpAddLast:
+					n.addLast(chain, filter)
+				case mesh_proto.OpAddAfter:
+					n.addAfter(chain, filter)
+				case mesh_proto.OpAddBefore:
+					n.addBefore(chain, filter)
+				case mesh_proto.OpRemove:
+					n.remove(chain)
+				case mesh_proto.OpPatch:
+					if err := n.patch(chain, filter); err != nil {
+						return errors.Wrap(err, "could not patch the resource")
+					}
+				default:
+					return errors.Errorf("invalid operation: %s", n.Operation)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (n *networkFilterModificator) addFirst(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	chain.Filters = append([]*envoy_listener.Filter{filter}, chain.Filters...)
+}
+
+func (n *networkFilterModificator) addLast(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	chain.Filters = append(chain.Filters, filter)
+}
+
+func (n *networkFilterModificator) addAfter(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	idx := n.indexOfMatchedFilter(chain)
+	if idx != -1 {
+		chain.Filters = append(chain.Filters, nil)
+		copy(chain.Filters[idx+2:], chain.Filters[idx+1:])
+		chain.Filters[idx+1] = filter
+	}
+}
+
+func (n *networkFilterModificator) addBefore(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	idx := n.indexOfMatchedFilter(chain)
+	if idx != -1 {
+		chain.Filters = append(chain.Filters, nil)
+		copy(chain.Filters[idx+1:], chain.Filters[idx:])
+		chain.Filters[idx] = filter
+	}
+}
+
+func (n *networkFilterModificator) remove(chain *envoy_listener.FilterChain) {
+	var filters []*envoy_listener.Filter
+	for _, filter := range chain.Filters {
+		if !n.filterMatches(filter) {
+			filters = append(filters, filter)
+		}
+	}
+	chain.Filters = filters
+}
+
+func (n *networkFilterModificator) patch(chain *envoy_listener.FilterChain, filterPatch *envoy_listener.Filter) error {
+	for _, filter := range chain.Filters {
+		if n.filterMatches(filter) {
+			any, err := util_proto.MergeAnys(filter.GetTypedConfig(), filterPatch.GetTypedConfig())
+			if err != nil {
+				return err
+			}
+
+			filter.ConfigType = &envoy_listener.Filter_TypedConfig{
+				TypedConfig: any,
+			}
+		}
+	}
+	return nil
+}
+
+func (n *networkFilterModificator) filterMatches(filter *envoy_listener.Filter) bool {
+	if n.Match.GetName() != "" && n.Match.GetName() != filter.Name {
+		return false
+	}
+	return true
+}
+
+func (n *networkFilterModificator) listenerMatches(resource *core_xds.Resource) bool {
+	if n.Match.GetListenerName() != "" && n.Match.GetListenerName() != resource.Name {
+		return false
+	}
+	if n.Match.GetOrigin() != "" && n.Match.GetOrigin() != resource.Origin {
+		return false
+	}
+	return true
+}
+
+func (n *networkFilterModificator) indexOfMatchedFilter(chain *envoy_listener.FilterChain) int {
+	for i, filter := range chain.Filters {
+		if filter.Name == n.Match.Name {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/pkg/xds/generator/modifications/v3/virtual_host.go b/pkg/xds/generator/modifications/v3/virtual_host.go
new file mode 100644
index 000000000..53c4aa943
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/virtual_host.go
@@ -0,0 +1,114 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/ptypes"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+// virtualHostModificator assumes that the routes are specified as `routeConfig` in Listeners, not through RDS
+// If we ever change it to RDS we need to modify RouteConfiguration objects
+type virtualHostModificator mesh_proto.ProxyTemplate_Modifications_VirtualHost
+
+func (c *virtualHostModificator) apply(resources *core_xds.ResourceSet) error {
+	virtualHost := &envoy_route.VirtualHost{}
+	if err := util_proto.FromYAML([]byte(c.Value), virtualHost); err != nil {
+		return err
+	}
+
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		listener := resource.Resource.(*envoy_listener.Listener)
+		if !c.originMatches(resource) {
+			continue
+		}
+		for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+			for _, networkFilter := range chain.Filters {
+				if networkFilter.Name == "envoy.filters.network.http_connection_manager" {
+					hcm := &envoy_hcm.HttpConnectionManager{}
+					err := ptypes.UnmarshalAny(networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig, hcm)
+					if err != nil {
+						return err
+					}
+					if err := c.applyHCMModification(hcm, virtualHost); err != nil {
+						return err
+					}
+					any, err := util_proto.MarshalAnyDeterministic(hcm)
+					if err != nil {
+						return err
+					}
+					networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig = any
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (c *virtualHostModificator) applyHCMModification(hcm *envoy_hcm.HttpConnectionManager, virtualHost *envoy_route.VirtualHost) error {
+	routeCfg := hcm.GetRouteConfig()
+	if routeCfg == nil {
+		return nil // ignore HCMs without embedded routes
+	}
+	if !c.routeConfigurationMatches(routeCfg) {
+		return nil
+	}
+	switch c.Operation {
+	case mesh_proto.OpAdd:
+		c.add(routeCfg, virtualHost)
+	case mesh_proto.OpRemove:
+		c.remove(routeCfg)
+	case mesh_proto.OpPatch:
+		c.patch(routeCfg, virtualHost)
+	default:
+		return errors.Errorf("invalid operation: %s", c.Operation)
+	}
+	return nil
+}
+
+func (c *virtualHostModificator) patch(routeCfg *envoy_route.RouteConfiguration, vHostPatch *envoy_route.VirtualHost) {
+	for _, vHost := range routeCfg.VirtualHosts {
+		if c.virtualHostMatches(vHost) {
+			proto.Merge(vHost, vHostPatch)
+		}
+	}
+}
+
+func (c *virtualHostModificator) remove(routeCfg *envoy_route.RouteConfiguration) {
+	var vHosts []*envoy_route.VirtualHost
+	for _, vHost := range routeCfg.VirtualHosts {
+		if !c.virtualHostMatches(vHost) {
+			vHosts = append(vHosts, vHost)
+		}
+	}
+	routeCfg.VirtualHosts = vHosts
+}
+
+func (c *virtualHostModificator) add(routeCfg *envoy_route.RouteConfiguration, vHost *envoy_route.VirtualHost) {
+	routeCfg.VirtualHosts = append(routeCfg.VirtualHosts, vHost)
+}
+
+func (c *virtualHostModificator) virtualHostMatches(vHost *envoy_route.VirtualHost) bool {
+	if c.Match.GetName() != "" && c.Match.GetName() != vHost.Name {
+		return false
+	}
+	return true
+}
+
+func (c *virtualHostModificator) originMatches(routeCfg *core_xds.Resource) bool {
+	return c.Match.GetOrigin() == "" || (c.Match.GetOrigin() == routeCfg.Origin)
+}
+
+func (c *virtualHostModificator) routeConfigurationMatches(routeCfg *envoy_route.RouteConfiguration) bool {
+	if c.Match.GetRouteConfigurationName() != "" && c.Match.GetRouteConfigurationName() != routeCfg.Name {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/proxy_template.go b/pkg/xds/generator/proxy_template.go
index c921b3143..9e03b91e8 100644
--- a/pkg/xds/generator/proxy_template.go
+++ b/pkg/xds/generator/proxy_template.go
@@ -34,7 +34,7 @@ func (g *ProxyTemplateGenerator) Generate(ctx xds_context.Context, proxy *model.
 	} else {
 		resources.AddSet(rs)
 	}
-	if err := modifications.Apply(resources, g.ProxyTemplate.GetConf().GetModifications()); err != nil {
+	if err := modifications.Apply(resources, g.ProxyTemplate.GetConf().GetModifications(), proxy.APIVersion); err != nil {
 		return nil, errors.Wrap(err, "could not apply modifications")
 	}
 	return resources, nil


Test patch:
diff --git a/app/kumactl/cmd/inspect/inspect_dataplanes_test.go b/app/kumactl/cmd/inspect/inspect_dataplanes_test.go
index ea3c075f8..722512254 100644
--- a/app/kumactl/cmd/inspect/inspect_dataplanes_test.go
+++ b/app/kumactl/cmd/inspect/inspect_dataplanes_test.go
@@ -80,16 +80,16 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									Port:        8080,
 									ServicePort: 80,
 									Tags: map[string]string{
-										"service": "mobile",
-										"version": "v1",
+										mesh_proto.ServiceTag: "mobile",
+										"version":             "v1",
 									},
 								},
 								{
 									Port:        8090,
 									ServicePort: 90,
 									Tags: map[string]string{
-										"service": "metrics",
-										"version": "v1",
+										mesh_proto.ServiceTag: "metrics",
+										"version":             "v1",
 									},
 								},
 							},
@@ -172,7 +172,7 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									Port:        8080,
 									ServicePort: 80,
 									Tags: map[string]string{
-										"service": "example",
+										mesh_proto.ServiceTag: "example",
 									},
 								},
 								{
@@ -180,7 +180,7 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									ServicePort: 81,
 									Health:      &mesh_proto.Dataplane_Networking_Inbound_Health{Ready: false},
 									Tags: map[string]string{
-										"service": "example",
+										mesh_proto.ServiceTag: "example",
 									},
 								},
 							},
@@ -264,7 +264,7 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									ServicePort: 80,
 									Health:      &mesh_proto.Dataplane_Networking_Inbound_Health{Ready: false},
 									Tags: map[string]string{
-										"service": "example",
+										mesh_proto.ServiceTag: "example",
 									},
 								},
 								{
@@ -272,7 +272,7 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									ServicePort: 81,
 									Health:      &mesh_proto.Dataplane_Networking_Inbound_Health{Ready: false},
 									Tags: map[string]string{
-										"service": "example",
+										mesh_proto.ServiceTag: "example",
 									},
 								},
 							},
@@ -355,7 +355,7 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 									Port:        8080,
 									ServicePort: 80,
 									Tags: map[string]string{
-										"service": "example",
+										"kuma.io/service": "example",
 									},
 								},
 							},
@@ -417,6 +417,15 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 			matcher      func(interface{}) gomega_types.GomegaMatcher
 		}
 
+		byLine := func(s string) []string {
+			lines := strings.Split(s, "\n")
+			var trimmedLines []string
+			for _, line := range lines {
+				trimmedLines = append(trimmedLines, strings.TrimSpace(line))
+			}
+			return trimmedLines
+		}
+
 		DescribeTable("kumactl inspect dataplanes -o table|json|yaml",
 			func(given testCase) {
 				// given
@@ -440,14 +449,14 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 				outputFormat: "",
 				goldenFile:   "inspect-dataplanes.golden.txt",
 				matcher: func(expected interface{}) gomega_types.GomegaMatcher {
-					return WithTransform(strings.TrimSpace, Equal(strings.TrimSpace(string(expected.([]byte)))))
+					return WithTransform(byLine, Equal(byLine(string(expected.([]byte)))))
 				},
 			}),
 			Entry("should support Table output explicitly", testCase{
 				outputFormat: "-otable",
 				goldenFile:   "inspect-dataplanes.golden.txt",
 				matcher: func(expected interface{}) gomega_types.GomegaMatcher {
-					return WithTransform(strings.TrimSpace, Equal(strings.TrimSpace(string(expected.([]byte)))))
+					return WithTransform(byLine, Equal(byLine(string(expected.([]byte)))))
 				},
 			}),
 			Entry("should support JSON output", testCase{
@@ -467,14 +476,14 @@ var _ = Describe("kumactl inspect dataplanes", func() {
 				// given
 				rootCmd.SetArgs([]string{
 					"--config-file", filepath.Join("..", "testdata", "sample-kumactl.config.yaml"),
-					"inspect", "dataplanes", "--tag", "service=mobile", "--tag", "version=v1"})
+					"inspect", "dataplanes", "--tag", "kuma.io/service=mobile", "--tag", "version=v1"})
 
 				// when
 				err := rootCmd.Execute()
 				// then
 				Expect(err).ToNot(HaveOccurred())
 				// and
-				Expect(testClient.receivedTags).To(HaveKeyWithValue("service", "mobile"))
+				Expect(testClient.receivedTags).To(HaveKeyWithValue(mesh_proto.ServiceTag, "mobile"))
 				Expect(testClient.receivedTags).To(HaveKeyWithValue("version", "v1"))
 			})
 		})
diff --git a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.json b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.json
index 10ee50577..f971183dd 100644
--- a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.json
+++ b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.json
@@ -15,7 +15,7 @@
               "port": 8080,
               "servicePort": 80,
               "tags": {
-                "service": "mobile",
+                "kuma.io/service": "mobile",
                 "version": "v1"
               }
             },
@@ -23,7 +23,7 @@
               "port": 8090,
               "servicePort": 90,
               "tags": {
-                "service": "metrics",
+                "kuma.io/service": "metrics",
                 "version": "v1"
               }
             }
@@ -100,14 +100,14 @@
               "port": 8080,
               "servicePort": 80,
               "tags": {
-                "service": "example"
+                "kuma.io/service": "example"
               }
             },
             {
               "port": 9001,
               "servicePort": 81,
               "tags": {
-                "service": "example"
+                "kuma.io/service": "example"
               },
               "health": {}
             }
@@ -184,7 +184,7 @@
               "port": 8080,
               "servicePort": 80,
               "tags": {
-                "service": "example"
+                "kuma.io/service": "example"
               },
               "health": {}
             },
@@ -192,7 +192,7 @@
               "port": 9001,
               "servicePort": 81,
               "tags": {
-                "service": "example"
+                "kuma.io/service": "example"
               },
               "health": {}
             }
@@ -269,7 +269,7 @@
               "port": 8080,
               "servicePort": 80,
               "tags": {
-                "service": "example"
+                "kuma.io/service": "example"
               }
             }
           ]
diff --git a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.txt b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.txt
index fe59d52c3..d12fddca5 100644
--- a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.txt
+++ b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.txt
@@ -1,5 +1,5 @@
-MESH      NAME          TAGS                                STATUS               LAST CONNECTED AGO   LAST UPDATED AGO   TOTAL UPDATES   TOTAL ERRORS   CERT REGENERATED AGO   CERT EXPIRATION       CERT REGENERATIONS   KUMA-DP VERSION   ENVOY VERSION
-default   experiment    service=metrics,mobile version=v1   Online               2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1
-default   degraded-dp   service=example                     Partially degraded   2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1
-default   offline-dp    service=example                     Offline              2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1
-default   example       service=example                     Offline              never                never              0               0              never                  -                     0
+MESH      NAME          TAGS                                        STATUS               LAST CONNECTED AGO   LAST UPDATED AGO   TOTAL UPDATES   TOTAL ERRORS   CERT REGENERATED AGO   CERT EXPIRATION       CERT REGENERATIONS   KUMA-DP VERSION   ENVOY VERSION   NOTES
+default   experiment    kuma.io/service=metrics,mobile version=v1   Online               2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1
+default   degraded-dp   kuma.io/service=example                     Partially degraded   2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1          inbound[port=9001,svc=example] is not ready
+default   offline-dp    kuma.io/service=example                     Offline              2h                   never              30              3              22h                    2020-05-08 08:28:22   10                   1.0.2             1.16.1          inbound[port=8080,svc=example] is not ready;inbound[port=9001,svc=example] is not ready
+default   example       kuma.io/service=example                     Offline              never                never              0               0              never                  -                     0
diff --git a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.yaml b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.yaml
index 8c391f6f1..9ccca3c15 100644
--- a/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.yaml
+++ b/app/kumactl/cmd/inspect/testdata/inspect-dataplanes.golden.yaml
@@ -7,12 +7,12 @@ items:
           - port: 8080
             servicePort: 80
             tags:
-              service: mobile
+              kuma.io/service: mobile
               version: v1
           - port: 8090
             servicePort: 90
             tags:
-              service: metrics
+              kuma.io/service: metrics
               version: v1
     dataplaneInsight:
       mTLS:
@@ -64,12 +64,12 @@ items:
           - port: 8080
             servicePort: 80
             tags:
-              service: example
+              kuma.io/service: example
           - health: {}
             port: 9001
             servicePort: 81
             tags:
-              service: example
+              kuma.io/service: example
     dataplaneInsight:
       mTLS:
         certificateExpirationTime: "2020-05-08T08:28:22Z"
@@ -121,12 +121,12 @@ items:
             port: 8080
             servicePort: 80
             tags:
-              service: example
+              kuma.io/service: example
           - health: {}
             port: 9001
             servicePort: 81
             tags:
-              service: example
+              kuma.io/service: example
     dataplaneInsight:
       mTLS:
         certificateExpirationTime: "2020-05-08T08:28:22Z"
@@ -177,7 +177,7 @@ items:
           - port: 8080
             servicePort: 80
             tags:
-              service: example
+              kuma.io/service: example
     dataplaneInsight:
       subscriptions:
         - controlPlaneInstanceId: node-001
diff --git a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
index de9323588..6c173bc0c 100644
--- a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
@@ -51,6 +51,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 100
               enforcingConsecutiveGatewayFailure: 0
@@ -74,6 +75,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 100
@@ -97,6 +99,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -122,6 +125,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               consecutive5xx: 21
               consecutiveGatewayFailure: 11
@@ -148,6 +152,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -176,6 +181,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -203,6 +209,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -231,6 +238,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
diff --git a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
index 841a0d8bf..71b1604a7 100644
--- a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
@@ -76,6 +76,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocket:
               name: envoy.transport_sockets.tls
@@ -162,6 +163,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocketMatches:
             - match:
@@ -295,6 +297,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocket:
               name: envoy.transport_sockets.tls
diff --git a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
index 0380aa1f6..3d45caffe 100644
--- a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
@@ -54,6 +54,7 @@ var _ = Describe("ClientSideTLSConfigurer", func() {
         edsClusterConfig:
           edsConfig:
             ads: {}
+            resourceApiVersion: V3
         name: testCluster
         transportSocketMatches:
         - match: {}
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
index 20a2e7c6f..3cb3ae94b 100644
--- a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
@@ -20,6 +20,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
         edsClusterConfig:
           edsConfig:
             ads: {}
+            resourceApiVersion: V3
         name: test:cluster
         type: EDS`
 
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
index 49510cf98..55e5ee489 100644
--- a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
@@ -48,6 +48,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             type: EDS`,
 		}),
@@ -74,6 +75,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             healthChecks:
             - healthyThreshold: 2
               interval: 5s
@@ -117,6 +119,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             healthChecks:
             - healthyThreshold: 2
               interval: 5s
@@ -161,6 +164,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             healthChecks:
             - healthyThreshold: 2
               interval: 5s
@@ -214,6 +218,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             healthChecks:
             - healthyThreshold: 2
               interval: 5s
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
index 08aaa8b69..47e3b9982 100644
--- a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
@@ -41,6 +41,7 @@ var _ = Describe("LbSubset", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: backend
             type: EDS`,
 		}),
@@ -55,6 +56,7 @@ var _ = Describe("LbSubset", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             lbSubsetConfig:
               fallbackPolicy: ANY_ENDPOINT
               subsetSelectors:
diff --git a/pkg/xds/generator/modifications/cluster_test.go b/pkg/xds/generator/modifications/v2/cluster_test.go
similarity index 98%
rename from pkg/xds/generator/modifications/cluster_test.go
rename to pkg/xds/generator/modifications/v2/cluster_test.go
index 8a20cd69f..f721bcf3b 100644
--- a/pkg/xds/generator/modifications/cluster_test.go
+++ b/pkg/xds/generator/modifications/v2/cluster_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/http_filter_test.go b/pkg/xds/generator/modifications/v2/http_filter_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/http_filter_test.go
rename to pkg/xds/generator/modifications/v2/http_filter_test.go
index 066a9f77a..a84e49476 100644
--- a/pkg/xds/generator/modifications/http_filter_test.go
+++ b/pkg/xds/generator/modifications/v2/http_filter_test.go
@@ -1,11 +1,11 @@
-package modifications_test
+package v2_test
 
 import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
diff --git a/pkg/xds/generator/modifications/listener_test.go b/pkg/xds/generator/modifications/v2/listener_test.go
similarity index 98%
rename from pkg/xds/generator/modifications/listener_test.go
rename to pkg/xds/generator/modifications/v2/listener_test.go
index 95666652d..7014c1330 100644
--- a/pkg/xds/generator/modifications/listener_test.go
+++ b/pkg/xds/generator/modifications/v2/listener_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/modifications_suite_test.go b/pkg/xds/generator/modifications/v2/modifications_suite_test.go
similarity index 86%
rename from pkg/xds/generator/modifications/modifications_suite_test.go
rename to pkg/xds/generator/modifications/v2/modifications_suite_test.go
index 9db07ae4d..169cbc554 100644
--- a/pkg/xds/generator/modifications/modifications_suite_test.go
+++ b/pkg/xds/generator/modifications/v2/modifications_suite_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	"testing"
@@ -14,6 +14,6 @@ func TestModifications(t *testing.T) {
 	RegisterFailHandler(Fail)
 
 	RunSpecsWithDefaultAndCustomReporters(t,
-		"Modifications Suite",
+		"Modifications V2 Suite",
 		[]Reporter{util_test.NewlineReporter{}})
 }
diff --git a/pkg/xds/generator/modifications/network_filter_test.go b/pkg/xds/generator/modifications/v2/network_filter_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/network_filter_test.go
rename to pkg/xds/generator/modifications/v2/network_filter_test.go
index 1bf94fc60..257000372 100644
--- a/pkg/xds/generator/modifications/network_filter_test.go
+++ b/pkg/xds/generator/modifications/v2/network_filter_test.go
@@ -1,11 +1,11 @@
-package modifications_test
+package v2_test
 
 import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
diff --git a/pkg/xds/generator/modifications/virtual_host_test.go b/pkg/xds/generator/modifications/v2/virtual_host_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/virtual_host_test.go
rename to pkg/xds/generator/modifications/v2/virtual_host_test.go
index be78a7dd5..411ba821b 100644
--- a/pkg/xds/generator/modifications/virtual_host_test.go
+++ b/pkg/xds/generator/modifications/v2/virtual_host_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/v3/cluster_test.go b/pkg/xds/generator/modifications/v3/cluster_test.go
new file mode 100644
index 000000000..f05b7c69c
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/cluster_test.go
@@ -0,0 +1,218 @@
+package v3_test
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Cluster modifications", func() {
+
+	type testCase struct {
+		clusters      []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, clusterYAML := range given.clusters {
+				cluster := &envoy_cluster.Cluster{}
+				err := util_proto.FromYAML([]byte(clusterYAML), cluster)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     cluster.Name,
+					Origin:   generator.OriginInbound,
+					Resource: cluster,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add cluster", testCase{
+			modifications: []string{`
+                cluster:
+                   operation: add
+                   value: |
+                     edsClusterConfig:
+                       edsConfig:
+                         ads: {}
+                     name: test:cluster
+                     type: EDS`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                edsClusterConfig:
+                  edsConfig:
+                    ads: {}
+                name: test:cluster
+                type: EDS`,
+		}),
+		Entry("should replace cluster", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: add
+                   value: |
+                     edsClusterConfig:
+                       edsConfig:
+                         ads: {}
+                     name: test:cluster
+                     type: EDS`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                edsClusterConfig:
+                  edsConfig:
+                    ads: {}
+                name: test:cluster
+                type: EDS`,
+		}),
+		Entry("should remove cluster matching all", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should remove cluster matching name", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster2
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove
+                   match:
+                     name: test:cluster`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster2
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster2
+                type: ORIGINAL_DST`,
+		}),
+		Entry("should remove all inbound clusters", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove
+                   match:
+                     origin: inbound`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should patch cluster matching name", testCase{
+			clusters: []string{
+				`
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                outlierDetection:
+                  enforcingConsecutive5xx: 100
+                  enforcingConsecutiveGatewayFailure: 0
+                  enforcingConsecutiveLocalOriginFailure: 0
+                  enforcingFailurePercentage: 0
+                  enforcingSuccessRate: 0
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: patch
+                   match:
+                     name: test:cluster
+                   value: |
+                     connectTimeout: 5s
+                     httpProtocolOptions:
+                       acceptHttp10: true
+                     outlierDetection:
+                       enforcingSuccessRate: 100`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                connectTimeout: 5s
+                httpProtocolOptions:
+                  acceptHttp10: true
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                outlierDetection:
+                  enforcingConsecutive5xx: 100
+                  enforcingConsecutiveGatewayFailure: 0
+                  enforcingConsecutiveLocalOriginFailure: 0
+                  enforcingFailurePercentage: 0
+                  enforcingSuccessRate: 100
+                type: ORIGINAL_DST`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/http_filter_test.go b/pkg/xds/generator/modifications/v3/http_filter_test.go
new file mode 100644
index 000000000..57b54b360
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/http_filter_test.go
@@ -0,0 +1,554 @@
+package v3_test
+
+import (
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("HTTP Filter modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add filter as a first", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                      httpFilters:
+                      - name: envoy.filters.http.router`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addLast
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from all listeners when there is no match section", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from all listeners when there is inbound match section", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     origin: inbound
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from picked listener", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     listenerName: inbound:192.168.0.1:8080
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters of given name from all listeners", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should add filter after already defined", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.http.router
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should not add filter when name is not matched", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.http.gzip
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should add filter before already defined", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.http.gzip
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      - name: envoy.filters.http.cors
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should patch resource matching filter name", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.config.filter.http.router.v2.Router
+                          startChildSpan: true
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: patch
+                   match:
+                     name: envoy.filters.http.router
+                   value: |
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.http.router.v2.Router
+                       dynamicStats: false
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.config.filter.http.router.v2.Router
+                          startChildSpan: true
+                          dynamicStats: false
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/listener_test.go b/pkg/xds/generator/modifications/v3/listener_test.go
new file mode 100644
index 000000000..4265fd6e1
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/listener_test.go
@@ -0,0 +1,234 @@
+package v3_test
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Listener modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add listener", testCase{
+			modifications: []string{`
+                listener:
+                   operation: add
+                   value: |
+                     name: inbound:192.168.0.1:8080
+                     trafficDirection: INBOUND
+                     address:
+                       socketAddress:
+                         address: 192.168.0.1
+                         portValue: 8080
+                     filterChains:
+                     - filters:
+                       - name: envoy.filters.network.tcp_proxy
+                         typedConfig:
+                           '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                           cluster: localhost:8080
+                           statPrefix: localhost_8080`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: localhost:8080
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should replace listener", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: add
+                   value: |
+                     name: inbound:192.168.0.1:8080
+                     address:
+                       socketAddress:
+                         address: 192.168.0.2
+                         portValue: 8090`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                name: inbound:192.168.0.1:8080
+                address:
+                  socketAddress:
+                    address: 192.168.0.2
+                    portValue: 8090`,
+		}),
+		Entry("should remove listener matching all", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should remove listener matching name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+				`
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove
+                   match:
+                     name: inbound:192.168.0.1:8080`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all inbound listeners", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove
+                   match:
+                     origin: inbound`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should patch listener matching name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: patch
+                   match:
+                     name: inbound:192.168.0.1:8080
+                   value: |
+                     tcpFastOpenQueueLength: 32`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                name: inbound:192.168.0.1:8080
+                tcpFastOpenQueueLength: 32
+                trafficDirection: INBOUND`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/modifications_suite_test.go b/pkg/xds/generator/modifications/v3/modifications_suite_test.go
new file mode 100644
index 000000000..4d5dcfa53
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/modifications_suite_test.go
@@ -0,0 +1,19 @@
+package v3_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_test "github.com/kumahq/kuma/pkg/util/test"
+	_ "github.com/kumahq/kuma/pkg/xds/envoy"
+)
+
+func TestModifications(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Modifications V3 Suite",
+		[]Reporter{util_test.NewlineReporter{}})
+}
diff --git a/pkg/xds/generator/modifications/v3/network_filter_test.go b/pkg/xds/generator/modifications/v3/network_filter_test.go
new file mode 100644
index 000000000..a7c61f481
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/network_filter_test.go
@@ -0,0 +1,617 @@
+package v3_test
+
+import (
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Network Filter modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should not add filter when there is no filter match", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addFirst
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter as a first", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addFirst
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should remove all filters from all listeners when there is no match section", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters from all listeners when there is inbound match section", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     origin: inbound
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters from picked listener", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     listenerName: inbound:192.168.0.1:8080
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters of given name from all listeners", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     name: envoy.filters.network.tcp_proxy
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should add filter after already defined (last)", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter after already defined", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.echo
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.echo
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should not add filter when name is not matched", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - {}`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter before already defined (first)", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter before already defined", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.network.tcp_proxy
+                   value: |
+                     name: envoy.filters.network.echo
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.echo
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should patch resource matching filter name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      stat_prefix: backend
+                      rds:
+                        configSource:
+                          ads: {}
+                        routeConfigName: outbound:backend
+                      httpFilters:
+                      - name: router
+`,
+			},
+			modifications: []string{`
+               networkFilter:
+                 operation: patch
+                 match:
+                   name: envoy.filters.network.http_connection_manager
+                 value: |
+                   name: envoy.filters.network.http_connection_manager
+                   typedConfig:
+                     '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                     streamIdleTimeout: 5s
+                     requestTimeout: 2s
+                     drainTimeout: 10s`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      drainTimeout: 10s
+                      httpFilters:
+                      - name: router
+                      rds:
+                        configSource:
+                          ads: {}
+                        routeConfigName: outbound:backend
+                      requestTimeout: 2s
+                      statPrefix: backend
+                      streamIdleTimeout: 5s
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should patch resource providing config", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+`,
+			},
+			modifications: []string{`
+               networkFilter:
+                 operation: patch
+                 match:
+                   name: envoy.filters.network.http_connection_manager
+                 value: |
+                   name: envoy.filters.network.http_connection_manager
+                   typedConfig:
+                     '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                     statPrefix: backend`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/virtual_host_test.go b/pkg/xds/generator/modifications/v3/virtual_host_test.go
new file mode 100644
index 000000000..517502e45
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/virtual_host_test.go
@@ -0,0 +1,335 @@
+package v3_test
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Virtual Host modifications", func() {
+
+	type testCase struct {
+		routeCfgs     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, routeCfgYAML := range given.routeCfgs {
+				routeCfg := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(routeCfgYAML), routeCfg)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     routeCfg.Name,
+					Origin:   generator.OriginOutbound,
+					Resource: routeCfg,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add virtual host", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      routeConfig:
+                        name: outbound:backend
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: add
+                   value: |
+                     name: backend
+                     domains:
+                     - backend.com
+                     routes:
+                     - match:
+                         prefix: /
+                       route:
+                         cluster: backend`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    filterChains:
+                    - filters:
+                      - name: envoy.filters.network.http_connection_manager
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                          httpFilters:
+                          - name: envoy.filters.http.router
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND
+`,
+		}),
+		Entry("should remove virtual host", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: remove
+                   match:
+                     name: backend`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    filterChains:
+                    - filters:
+                      - name: envoy.filters.network.http_connection_manager
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                          httpFilters:
+                          - name: envoy.filters.http.router
+                          routeConfig:
+                            name: outbound:backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+		Entry("should patch a virtual host", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     origin: outbound
+                   value: |
+                     retryPolicy:
+                       retryOn: 5xx
+                       numRetries: 3`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    filterChains:
+                    - filters:
+                      - name: envoy.filters.network.http_connection_manager
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                          httpFilters:
+                          - name: envoy.filters.http.router
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              retryPolicy:
+                                numRetries: 3
+                                retryOn: 5xx
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+		Entry("should patch a virtual host adding new route", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     routeConfigurationName: outbound:backend
+                   value: |
+                     routes:
+                     - match:
+                         prefix: /web
+                       route:
+                         cluster: web`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    filterChains:
+                    - filters:
+                      - name: envoy.filters.network.http_connection_manager
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                          httpFilters:
+                          - name: envoy.filters.http.router
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                              - match:
+                                  prefix: /web
+                                route:
+                                  cluster: web
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+	)
+})