Repository: kumahq/kuma
Pull request #1312 (2020-12-14 09:33:58)

v3 migration - clusters, endpoints
# Cluster / Endpoints / TLS / Metadata V3 migration

This PR enables generation of V3 config for Cluster and Endpoints

# Concept of V3 migration

Most of the go-control-plane V2 imports is in `pkg/xds/envoy` and are structures are hidden behind builders.
Then, builders are used in proxy generators which don't really care about the actual type, they care that it is a resource that can be added to `ResourceSet` which will be put into the cache. You will have to now pass the API Version to the builder as a mandatory parameter so the builder can choose which version of the resource it will return.

V2 -> V3 migration will result in the copy-pasting of a massive amount of the code, but at least here it is hidden behind builders, so the copy-paste will be in one place and it will be easy to remove in the future.

Of course, it's not that obvious, V2 imports are spread around a code
![image](https://user-images.githubusercontent.com/5459824/102064256-5e1b5c80-3df7-11eb-820d-9994966846ee.png)
some of the tricky parts will be
* maintaining two SnapshotCaches and servers
* Callbacks that supports V2 and V3
* proxy template modifications

but here is a good start for the actual V3 config generation

nickolaev: So far this approach is great. We'll support v2 and v3 and smoothly migrate.
jakubdyszkiewicz: I'll fix the imports in the next PR

Patch:
diff --git a/api/go.sum b/api/go.sum
index b40ab4324..e7289a1af 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -18,6 +18,7 @@ github.com/envoyproxy/protoc-gen-validate v0.3.0-java.0.20200311152155-ab56c3dd1
 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=
@@ -117,6 +118,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 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/go.mod b/go.mod
index 76087a685..31d3a709a 100644
--- a/go.mod
+++ b/go.mod
@@ -38,7 +38,6 @@ require (
 	github.com/prometheus/prometheus v0.0.0-00010101000000-000000000000
 	github.com/sethvargo/go-retry v0.1.0
 	github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
-	github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
 	github.com/slok/go-http-metrics v0.9.0
 	github.com/spf13/cobra v1.0.0
 	github.com/spiffe/go-spiffe v0.0.0-20190820222348-6adcf1eecbcc
diff --git a/go.sum b/go.sum
index 0b149851e..f06a5310f 100644
--- a/go.sum
+++ b/go.sum
@@ -23,6 +23,7 @@ github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
 github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v38.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-autorest v13.3.2+incompatible h1:VxzPyuhtnlBOzc4IWCZHqpyH2d+QMLQEuy3wREyY4oc=
 github.com/Azure/go-autorest v13.3.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@@ -50,9 +51,12 @@ github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocm
 github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
 github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
 github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
+github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
 github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
+github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
@@ -201,6 +205,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
@@ -211,6 +216,7 @@ github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2
 github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
 github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
 github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As=
@@ -218,6 +224,7 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA
 github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dhui/dktest v0.3.1 h1:NVUdB50k8tml431Ho1hcQBNeC52Qe8oSDPAjseA67Y8=
@@ -294,6 +301,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
 github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
@@ -312,7 +320,9 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+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-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
@@ -365,12 +375,14 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
 github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
 github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
 github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
 github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@@ -438,13 +450,16 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 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.2.2/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 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -476,15 +491,18 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMW
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
 github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
@@ -493,6 +511,7 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
@@ -524,6 +543,7 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
 github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imkira/go-observer v1.0.3/go.mod h1:zLzElv2cGTHufQG17IEILJMPDg32TD85fFgKyFv00wU=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
@@ -534,6 +554,7 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
 github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
@@ -564,9 +585,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJ
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
@@ -609,6 +632,7 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+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/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -618,6 +642,7 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
@@ -631,9 +656,11 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQ
 github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 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/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -721,6 +748,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=
@@ -777,6 +805,7 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
 github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
 github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
+github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -795,8 +824,6 @@ github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06B
 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU=
-github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@@ -828,6 +855,7 @@ github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzu
 github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/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/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
@@ -843,6 +871,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -1141,6 +1170,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
@@ -1213,6 +1243,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -1221,11 +1252,13 @@ gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
+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/square/go-jose.v2 v2.3.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=
@@ -1240,6 +1273,7 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
 helm.sh/helm/v3 v3.3.4/go.mod h1:CyCGQa53/k1JFxXvXveGwtfJ4cuB9zkaBSGa5rnAiHU=
@@ -1286,6 +1320,7 @@ k8s.io/gengo v0.0.0-20190822140433-26a664648505/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=
@@ -1326,6 +1361,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.1 h1:cP2l8fkA3O9vekpy5Ks8mmA0NW/F7yBdXf8brkWhVrs=
 sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U=
 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/types.go b/pkg/core/xds/types.go
index 40839b2d5..ba234e91c 100644
--- a/pkg/core/xds/types.go
+++ b/pkg/core/xds/types.go
@@ -44,9 +44,9 @@ type DestinationMap map[ServiceName]TagSelectorSet
 
 type ExternalService struct {
 	TLSEnabled bool
-	CaCert     *envoy_core.DataSource
-	ClientCert *envoy_core.DataSource
-	ClientKey  *envoy_core.DataSource
+	CaCert     []byte
+	ClientCert []byte
+	ClientKey  []byte
 }
 
 type Locality struct {
diff --git a/pkg/plugins/resources/k8s/native/go.sum b/pkg/plugins/resources/k8s/native/go.sum
index d59d3cfb4..c8a907ab5 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=
@@ -48,6 +49,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=
@@ -77,12 +79,15 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 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 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 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=
@@ -164,10 +169,12 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 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=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 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=
@@ -199,9 +206,11 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -213,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=
@@ -246,15 +258,18 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/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 +290,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=
@@ -299,8 +315,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=
@@ -339,6 +358,7 @@ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARV
 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/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=
@@ -377,6 +397,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=
@@ -394,10 +415,12 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -423,13 +446,16 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 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=
@@ -460,6 +486,7 @@ 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=
diff --git a/pkg/xds/cache/cla/cache.go b/pkg/xds/cache/cla/cache.go
index 631076999..5504342f3 100644
--- a/pkg/xds/cache/cla/cache.go
+++ b/pkg/xds/cache/cla/cache.go
@@ -21,7 +21,7 @@ import (
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
 	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
 	"github.com/kumahq/kuma/pkg/xds/cache/once"
-	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 	"github.com/kumahq/kuma/pkg/xds/topology"
 )
 
diff --git a/pkg/xds/envoy/api_version.go b/pkg/xds/envoy/api_version.go
new file mode 100644
index 000000000..c53d11c3a
--- /dev/null
+++ b/pkg/xds/envoy/api_version.go
@@ -0,0 +1,8 @@
+package envoy
+
+type APIVersion string
+
+const (
+	APIV2 APIVersion = "v2"
+	APIV3 APIVersion = "v3"
+)
diff --git a/pkg/xds/envoy/clusters/cluster_builder.go b/pkg/xds/envoy/clusters/cluster_builder.go
index 9a6fcf0c9..b8fddbcf7 100644
--- a/pkg/xds/envoy/clusters/cluster_builder.go
+++ b/pkg/xds/envoy/clusters/cluster_builder.go
@@ -1,15 +1,15 @@
 package clusters
 
 import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
+	"errors"
 
-// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
-// such as filter chain, transparent proxying, etc.
-type ClusterConfigurer interface {
-	// Configure configures a single aspect on a given Envoy cluster.
-	Configure(cluster *envoy_api.Cluster) error
-}
+	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v2"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v3"
+)
 
 // ClusterBuilderOpt is a configuration option for ClusterBuilder.
 //
@@ -19,14 +19,17 @@ type ClusterBuilderOpt interface {
 	ApplyTo(config *ClusterBuilderConfig)
 }
 
-func NewClusterBuilder() *ClusterBuilder {
-	return &ClusterBuilder{}
+func NewClusterBuilder(apiVersion envoy.APIVersion) *ClusterBuilder {
+	return &ClusterBuilder{
+		apiVersion: apiVersion,
+	}
 }
 
 // ClusterBuilder is responsible for generating an Envoy cluster
 // by applying a series of ClusterConfigurers.
 type ClusterBuilder struct {
-	config ClusterBuilderConfig
+	apiVersion envoy.APIVersion
+	config     ClusterBuilderConfig
 }
 
 // Configure configures ClusterBuilder by adding individual ClusterConfigurers.
@@ -38,25 +41,44 @@ func (b *ClusterBuilder) Configure(opts ...ClusterBuilderOpt) *ClusterBuilder {
 }
 
 // Build generates an Envoy cluster by applying a series of ClusterConfigurers.
-func (b *ClusterBuilder) Build() (*envoy_api.Cluster, error) {
-	cluster := envoy_api.Cluster{}
-	for _, configurer := range b.config.Configurers {
-		if err := configurer.Configure(&cluster); err != nil {
-			return nil, err
+func (b *ClusterBuilder) Build() (envoy.NamedResource, error) {
+	switch b.apiVersion {
+	case envoy.APIV2:
+		cluster := envoy_api_v2.Cluster{}
+		for _, configurer := range b.config.ConfigurersV2 {
+			if err := configurer.Configure(&cluster); err != nil {
+				return nil, err
+			}
 		}
+		return &cluster, nil
+	case envoy.APIV3:
+		cluster := envoy_api.Cluster{}
+		for _, configurer := range b.config.ConfigurersV3 {
+			if err := configurer.Configure(&cluster); err != nil {
+				return nil, err
+			}
+		}
+		return &cluster, nil
+	default:
+		return nil, errors.New("unknown API")
 	}
-	return &cluster, nil
 }
 
 // ClusterBuilderConfig holds configuration of a ClusterBuilder.
 type ClusterBuilderConfig struct {
 	// A series of ClusterConfigurers to apply to Envoy cluster.
-	Configurers []ClusterConfigurer
+	ConfigurersV2 []v2.ClusterConfigurer
+	ConfigurersV3 []v3.ClusterConfigurer
+}
+
+// Add appends a given ClusterConfigurer to the end of the chain.
+func (c *ClusterBuilderConfig) AddV2(configurer v2.ClusterConfigurer) {
+	c.ConfigurersV2 = append(c.ConfigurersV2, configurer)
 }
 
 // Add appends a given ClusterConfigurer to the end of the chain.
-func (c *ClusterBuilderConfig) Add(configurer ClusterConfigurer) {
-	c.Configurers = append(c.Configurers, configurer)
+func (c *ClusterBuilderConfig) AddV3(configurer v3.ClusterConfigurer) {
+	c.ConfigurersV3 = append(c.ConfigurersV3, configurer)
 }
 
 // ClusterBuilderOptFunc is a convenience type adapter.
diff --git a/pkg/xds/envoy/clusters/configurers.go b/pkg/xds/envoy/clusters/configurers.go
new file mode 100644
index 000000000..da29a0543
--- /dev/null
+++ b/pkg/xds/envoy/clusters/configurers.go
@@ -0,0 +1,206 @@
+package clusters
+
+import (
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v2"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v3"
+)
+
+func OutlierDetection(circuitBreaker *mesh_core.CircuitBreakerResource) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.OutlierDetectionConfigurer{CircuitBreaker: circuitBreaker})
+		config.AddV3(&v3.OutlierDetectionConfigurer{CircuitBreaker: circuitBreaker})
+	})
+}
+
+func ClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, clientService string, tags []envoy.Tags) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: clientService,
+			Tags:          tags,
+		})
+		config.AddV3(&v3.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: clientService,
+			Tags:          tags,
+		})
+	})
+}
+
+// UnknownDestinationClientSideMTLS configures cluster with mTLS for a mesh but without extensive destination verification (only Mesh is verified)
+func UnknownDestinationClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: "*",
+			Tags:          nil,
+		})
+		config.AddV3(&v3.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: "*",
+			Tags:          nil,
+		})
+	})
+}
+
+func ClientSideTLS(endpoints []core_xds.Endpoint) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideTLSConfigurer{
+			Endpoints: endpoints,
+		})
+		config.AddV3(&v3.ClientSideTLSConfigurer{
+			Endpoints: endpoints,
+		})
+	})
+}
+
+func DNSCluster(name string, address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.DnsClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.DnsClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func EdsCluster(name string) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.EdsClusterConfigurer{
+			Name: name,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.EdsClusterConfigurer{
+			Name: name,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func HealthCheck(healthCheck *mesh_core.HealthCheckResource) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.HealthCheckConfigurer{
+			HealthCheck: healthCheck,
+		})
+		config.AddV3(&v3.HealthCheckConfigurer{
+			HealthCheck: healthCheck,
+		})
+	})
+}
+
+// LbSubset is required for MetadataMatch in Weighted Cluster in TCP Proxy to work.
+// Subset loadbalancing is used in two use cases
+// 1) TrafficRoute for splitting traffic. Example: TrafficRoute that splits 10% of the traffic to version 1 of the service backend and 90% traffic to version 2 of the service backend
+// 2) Multiple outbound sections with the same service
+//    Example:
+//    type: Dataplane
+//    networking:
+//      outbound:
+//      - port: 1234
+//        tags:
+//          kuma.io/service: backend
+//      - port: 1234
+//        tags:
+//          kuma.io/service: backend
+//          version: v1
+//    Only one cluster "backend" is generated for such dataplane, but with lb subset by version.
+func LbSubset(keySets [][]string) ClusterBuilderOptFunc {
+	return func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.LbSubsetConfigurer{
+			KeySets: keySets,
+		})
+		config.AddV3(&v3.LbSubsetConfigurer{
+			KeySets: keySets,
+		})
+	}
+}
+
+func PassThroughCluster(name string) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.PassThroughClusterConfigurer{
+			Name: name,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.PassThroughClusterConfigurer{
+			Name: name,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func StaticCluster(name string, address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.StaticClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.StaticClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func StrictDNSCluster(name string, endpoints []core_xds.Endpoint) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.StrictDNSClusterConfigurer{
+			Name:      name,
+			Endpoints: endpoints,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.StrictDNSClusterConfigurer{
+			Name:      name,
+			Endpoints: endpoints,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func UpstreamBindConfig(address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.UpstreamBindConfigConfigurer{
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.UpstreamBindConfigConfigurer{
+			Address: address,
+			Port:    port,
+		})
+	})
+}
+
+func Http2() ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.Http2Configurer{})
+		config.AddV3(&v3.Http2Configurer{})
+	})
+}
diff --git a/pkg/xds/envoy/clusters/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/dns_cluster_configurer.go
deleted file mode 100644
index 4def95243..000000000
--- a/pkg/xds/envoy/clusters/dns_cluster_configurer.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func DNSCluster(name string, address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&dnsClusterConfigurer{
-			name:    name,
-			address: address,
-			port:    port,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type dnsClusterConfigurer struct {
-	name    string
-	address string
-	port    uint32
-}
-
-func (e *dnsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
-	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
-	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.name, e.address, e.port)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/http2_configurer.go b/pkg/xds/envoy/clusters/http2_configurer.go
deleted file mode 100644
index f7ecf74ca..000000000
--- a/pkg/xds/envoy/clusters/http2_configurer.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-)
-
-func Http2() ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&http2Configurer{})
-	})
-}
-
-type http2Configurer struct {
-}
-
-func (p *http2Configurer) Configure(c *envoy_api.Cluster) error {
-	c.Http2ProtocolOptions = &envoy_api_v2_core.Http2ProtocolOptions{}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/lb_subset_configurer.go b/pkg/xds/envoy/clusters/lb_subset_configurer.go
deleted file mode 100644
index b558da571..000000000
--- a/pkg/xds/envoy/clusters/lb_subset_configurer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
-
-// LbSubset is required for MetadataMatch in Weighted Cluster in TCP Proxy to work.
-// Subset loadbalancing is used in two use cases
-// 1) TrafficRoute for splitting traffic. Example: TrafficRoute that splits 10% of the traffic to version 1 of the service backend and 90% traffic to version 2 of the service backend
-// 2) Multiple outbound sections with the same service
-//    Example:
-//    type: Dataplane
-//    networking:
-//      outbound:
-//      - port: 1234
-//        tags:
-//          kuma.io/service: backend
-//      - port: 1234
-//        tags:
-//          kuma.io/service: backend
-//          version: v1
-//    Only one cluster "backend" is generated for such dataplane, but with lb subset by version.
-func LbSubset(keySets [][]string) ClusterBuilderOptFunc {
-	return func(config *ClusterBuilderConfig) {
-		config.Add(&lbSubsetConfigurer{
-			keySets: keySets,
-		})
-	}
-}
-
-type lbSubsetConfigurer struct {
-	keySets [][]string
-}
-
-func (e *lbSubsetConfigurer) Configure(c *envoy_api.Cluster) error {
-	var selectors []*envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector
-	for _, keys := range e.keySets {
-		if len(keys) == 0 {
-			continue
-		}
-		selectors = append(selectors, &envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector{
-			Keys: keys,
-			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
-			FallbackPolicy: envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
-		})
-	}
-	if len(selectors) > 0 {
-		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
-		c.LbSubsetConfig = &envoy_api.Cluster_LbSubsetConfig{
-			FallbackPolicy:  envoy_api.Cluster_LbSubsetConfig_ANY_ENDPOINT,
-			SubsetSelectors: selectors,
-		}
-	}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go
deleted file mode 100644
index a88944613..000000000
--- a/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
-
-func PassThroughCluster(name string) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&passThroughClusterConfigurer{
-			name: name,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type passThroughClusterConfigurer struct {
-	name string
-}
-
-func (p *passThroughClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = p.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_ORIGINAL_DST}
-	c.LbPolicy = envoy_api.Cluster_CLUSTER_PROVIDED
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/static_cluster_configurer.go b/pkg/xds/envoy/clusters/static_cluster_configurer.go
deleted file mode 100644
index cb903f2e7..000000000
--- a/pkg/xds/envoy/clusters/static_cluster_configurer.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package clusters
-
-import (
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func StaticCluster(name string, address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&staticClusterConfigurer{
-			name:    name,
-			address: address,
-			port:    port,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type staticClusterConfigurer struct {
-	name    string
-	address string
-	port    uint32
-}
-
-func (e *staticClusterConfigurer) Configure(c *v2.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &v2.Cluster_Type{Type: v2.Cluster_STATIC}
-	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.name, e.address, e.port)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go
deleted file mode 100644
index cabb22ac2..000000000
--- a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	"github.com/kumahq/kuma/pkg/core/xds"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func StrictDNSCluster(name string, endpoints []xds.Endpoint) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&StricDNSClusterConfigurer{
-			name:      name,
-			endpoints: endpoints,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type StricDNSClusterConfigurer struct {
-	name      string
-	endpoints []xds.Endpoint
-}
-
-func (e *StricDNSClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
-	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
-	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.name, e.endpoints)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go
deleted file mode 100644
index eae6ede7e..000000000
--- a/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-)
-
-func UpstreamBindConfig(address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&upstreamBindConfigConfigurer{
-			address: address,
-			port:    port,
-		})
-	})
-}
-
-type upstreamBindConfigConfigurer struct {
-	address string
-	port    uint32
-}
-
-func (u *upstreamBindConfigConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.UpstreamBindConfig = &envoy_core.BindConfig{
-		SourceAddress: &envoy_core.SocketAddress{
-			Address: u.address,
-			PortSpecifier: &envoy_core.SocketAddress_PortValue{
-				PortValue: u.port,
-			},
-		},
-	}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/alt_stat_name_configurer.go b/pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
similarity index 55%
rename from pkg/xds/envoy/clusters/alt_stat_name_configurer.go
rename to pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
index af2ee641a..b98c6c15e 100644
--- a/pkg/xds/envoy/clusters/alt_stat_name_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
@@ -6,16 +6,12 @@ import (
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
 )
 
-func AltStatName() ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&altStatNameConfigurer{})
-	})
+type AltStatNameConfigurer struct {
 }
 
-type altStatNameConfigurer struct {
-}
+var _ ClusterConfigurer = &AltStatNameConfigurer{}
 
-func (e *altStatNameConfigurer) Configure(cluster *envoy_api.Cluster) error {
+func (e *AltStatNameConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	sanitizedName := util_xds.SanitizeMetric(cluster.Name)
 	if sanitizedName != cluster.Name {
 		cluster.AltStatName = sanitizedName
diff --git a/pkg/xds/envoy/clusters/circuit_breaker_configurer.go b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
similarity index 94%
rename from pkg/xds/envoy/clusters/circuit_breaker_configurer.go
rename to pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
index 1cb12b37c..c6afcca06 100644
--- a/pkg/xds/envoy/clusters/circuit_breaker_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
@@ -8,18 +8,12 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 )
 
-func OutlierDetection(circuitBreaker *mesh_core.CircuitBreakerResource) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&OutlierDetectionConfigurer{
-			CircuitBreaker: circuitBreaker,
-		})
-	})
-}
-
 type OutlierDetectionConfigurer struct {
 	CircuitBreaker *mesh_core.CircuitBreakerResource
 }
 
+var _ ClusterConfigurer = &OutlierDetectionConfigurer{}
+
 func (c *OutlierDetectionConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	if c.CircuitBreaker == nil {
 		return nil
diff --git a/pkg/xds/envoy/clusters/client_side_mtls_configurer.go b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
similarity index 56%
rename from pkg/xds/envoy/clusters/client_side_mtls_configurer.go
rename to pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
index 2f59a6c4e..965eb3ff1 100644
--- a/pkg/xds/envoy/clusters/client_side_mtls_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
@@ -9,46 +9,27 @@ import (
 	"github.com/kumahq/kuma/pkg/util/proto"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls_v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 )
 
-func ClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, clientService string, tags []envoy.Tags) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideMTLSConfigurer{
-			ctx:           ctx,
-			metadata:      metadata,
-			clientService: clientService,
-			tags:          tags,
-		})
-	})
+type ClientSideMTLSConfigurer struct {
+	Ctx           xds_context.Context
+	Metadata      *core_xds.DataplaneMetadata
+	ClientService string
+	Tags          []envoy.Tags
 }
 
-// UnknownDestinationClientSideMTLS configures cluster with mTLS for a mesh but without extensive destination verification (only Mesh is verified)
-func UnknownDestinationClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideMTLSConfigurer{
-			ctx:           ctx,
-			metadata:      metadata,
-			clientService: "*",
-			tags:          nil,
-		})
-	})
-}
-
-type clientSideMTLSConfigurer struct {
-	ctx           xds_context.Context
-	metadata      *core_xds.DataplaneMetadata
-	clientService string
-	tags          []envoy.Tags
-}
+var _ ClusterConfigurer = &ClientSideMTLSConfigurer{}
 
-func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	if !c.ctx.Mesh.Resource.MTLSEnabled() {
+func (c *ClientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	if !c.Ctx.Mesh.Resource.MTLSEnabled() {
 		return nil
 	}
-	mesh := c.ctx.Mesh.Resource.GetMeta().GetName()
+	mesh := c.Ctx.Mesh.Resource.GetMeta().GetName()
 	// there might be a situation when there are multiple sam tags passed here for example two outbound listeners with the same tags, therefore we need to distinguish between them.
-	distinctTags := envoy.DistinctTags(c.tags)
+	distinctTags := envoy.DistinctTags(c.Tags)
 	switch {
 	case len(distinctTags) == 0:
 		transportSocket, err := c.createTransportSocket("")
@@ -57,7 +38,7 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 		}
 		cluster.TransportSocket = transportSocket
 	case len(distinctTags) == 1:
-		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.tags[0].WithTags("mesh", mesh)))
+		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.Tags[0].WithTags("mesh", mesh)))
 		if err != nil {
 			return err
 		}
@@ -72,7 +53,7 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_api.Cluster_TransportSocketMatch{
 				Name: sni,
 				Match: &pstruct.Struct{
-					Fields: envoy.MetadataFields(tags),
+					Fields: envoy_metadata.MetadataFields(tags),
 				},
 				TransportSocket: transportSocket,
 			})
@@ -81,8 +62,8 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	return nil
 }
 
-func (c *clientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
-	tlsContext, err := tls.CreateUpstreamTlsContext(c.ctx, c.metadata, c.clientService, sni)
+func (c *ClientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
+	tlsContext, err := tls_v2.CreateUpstreamTlsContext(c.Ctx, c.Metadata, c.ClientService, sni)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/xds/envoy/clusters/client_side_tls_configurer.go b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
similarity index 65%
rename from pkg/xds/envoy/clusters/client_side_tls_configurer.go
rename to pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
index c04e89d80..8ad81fe47 100644
--- a/pkg/xds/envoy/clusters/client_side_tls_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
@@ -5,29 +5,22 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 	pstruct "github.com/golang/protobuf/ptypes/struct"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
-
 	"github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
-	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
+	tls_v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 )
 
-func ClientSideTLS(endpoints []xds.Endpoint) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideTLSConfigurer{
-			endpoints: endpoints,
-		})
-	})
+type ClientSideTLSConfigurer struct {
+	Endpoints []xds.Endpoint
 }
 
-type clientSideTLSConfigurer struct {
-	endpoints []xds.Endpoint
-}
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
 
-func (c *clientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	for _, ep := range c.endpoints {
+func (c *ClientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	for _, ep := range c.Endpoints {
 		if ep.ExternalService.TLSEnabled {
-			tlsContext, err := tls.UpstreamTlsContextOutsideMesh(
+			tlsContext, err := tls_v2.UpstreamTlsContextOutsideMesh(
 				ep.ExternalService.CaCert,
 				ep.ExternalService.ClientCert,
 				ep.ExternalService.ClientKey,
@@ -51,7 +44,7 @@ func (c *clientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_api.Cluster_TransportSocketMatch{
 				Name: ep.Target,
 				Match: &pstruct.Struct{
-					Fields: envoy.MetadataFields(ep.Tags),
+					Fields: envoy_metadata.MetadataFields(ep.Tags),
 				},
 				TransportSocket: transportSocket,
 			})
diff --git a/pkg/xds/envoy/clusters/v2/configurer.go b/pkg/xds/envoy/clusters/v2/configurer.go
new file mode 100644
index 000000000..f34d8b43d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/configurer.go
@@ -0,0 +1,10 @@
+package clusters
+
+import envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
+// such as filter chain, transparent proxying, etc.
+type ClusterConfigurer interface {
+	// Configure configures a single aspect on a given Envoy cluster.
+	Configure(cluster *envoy_api.Cluster) error
+}
diff --git a/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go
new file mode 100644
index 000000000..282c2c982
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type DnsClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &DnsClusterConfigurer{}
+
+func (e *DnsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
similarity index 56%
rename from pkg/xds/envoy/clusters/eds_cluster_configurer.go
rename to pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
index abe008f75..cd596e606 100644
--- a/pkg/xds/envoy/clusters/eds_cluster_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
@@ -5,22 +5,14 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 )
 
-func EdsCluster(name string) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&edsClusterConfigurer{
-			name: name,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
+type EdsClusterConfigurer struct {
+	Name string
 }
 
-type edsClusterConfigurer struct {
-	name string
-}
+var _ ClusterConfigurer = &EdsClusterConfigurer{}
 
-func (e *edsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
+func (e *EdsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
 	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_EDS}
 	c.EdsClusterConfig = &envoy_api.Cluster_EdsClusterConfig{
 		EdsConfig: &envoy_core.ConfigSource{
diff --git a/pkg/xds/envoy/clusters/health_check_configurer.go b/pkg/xds/envoy/clusters/v2/health_check_configurer.go
similarity index 87%
rename from pkg/xds/envoy/clusters/health_check_configurer.go
rename to pkg/xds/envoy/clusters/v2/health_check_configurer.go
index bd2cea1bc..6f5b0bee8 100644
--- a/pkg/xds/envoy/clusters/health_check_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/health_check_configurer.go
@@ -12,17 +12,11 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 )
 
-func HealthCheck(healthCheck *mesh_core.HealthCheckResource) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&healthCheckConfigurer{
-			healthCheck: healthCheck,
-		})
-	})
+type HealthCheckConfigurer struct {
+	HealthCheck *mesh_core.HealthCheckResource
 }
 
-type healthCheckConfigurer struct {
-	healthCheck *mesh_core.HealthCheckResource
-}
+var _ ClusterConfigurer = &HealthCheckConfigurer{}
 
 func mapUInt32ToInt64Range(value uint32) *envoy_type.Int64Range {
 	return &envoy_type.Int64Range{
@@ -100,11 +94,11 @@ func httpHealthCheck(
 	}
 }
 
-func (e *healthCheckConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	if e.healthCheck == nil || e.healthCheck.Spec.Conf == nil {
+func (e *HealthCheckConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	if e.HealthCheck == nil || e.HealthCheck.Spec.Conf == nil {
 		return nil
 	}
-	activeChecks := e.healthCheck.Spec.Conf
+	activeChecks := e.HealthCheck.Spec.Conf
 	healthCheck := envoy_core.HealthCheck{
 		HealthChecker: &envoy_core.HealthCheck_TcpHealthCheck_{
 			TcpHealthCheck: &envoy_core.HealthCheck_TcpHealthCheck{},
diff --git a/pkg/xds/envoy/clusters/v2/http2_configurer.go b/pkg/xds/envoy/clusters/v2/http2_configurer.go
new file mode 100644
index 000000000..bbebe6a3c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/http2_configurer.go
@@ -0,0 +1,16 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+)
+
+type Http2Configurer struct {
+}
+
+var _ ClusterConfigurer = &Http2Configurer{}
+
+func (p *Http2Configurer) Configure(c *envoy_api.Cluster) error {
+	c.Http2ProtocolOptions = &envoy_core.Http2ProtocolOptions{}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go b/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go
new file mode 100644
index 000000000..a8da6376b
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go
@@ -0,0 +1,33 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+)
+
+type LbSubsetConfigurer struct {
+	KeySets [][]string
+}
+
+var _ ClusterConfigurer = &LbSubsetConfigurer{}
+
+func (e *LbSubsetConfigurer) Configure(c *envoy_api.Cluster) error {
+	var selectors []*envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector
+	for _, keys := range e.KeySets {
+		if len(keys) == 0 {
+			continue
+		}
+		selectors = append(selectors, &envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector{
+			Keys: keys,
+			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
+			FallbackPolicy: envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
+		})
+	}
+	if len(selectors) > 0 {
+		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
+		c.LbSubsetConfig = &envoy_api.Cluster_LbSubsetConfig{
+			FallbackPolicy:  envoy_api.Cluster_LbSubsetConfig_ANY_ENDPOINT,
+			SubsetSelectors: selectors,
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go
new file mode 100644
index 000000000..71d7311c5
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go
@@ -0,0 +1,18 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+)
+
+type PassThroughClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &PassThroughClusterConfigurer{}
+
+func (p *PassThroughClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = p.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_ORIGINAL_DST}
+	c.LbPolicy = envoy_api.Cluster_CLUSTER_PROVIDED
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go
new file mode 100644
index 000000000..dcabc6901
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go
@@ -0,0 +1,22 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type StaticClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &StaticClusterConfigurer{}
+
+func (e *StaticClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STATIC}
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go
new file mode 100644
index 000000000..7ab4ad6e0
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go
@@ -0,0 +1,24 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type StrictDNSClusterConfigurer struct {
+	Name      string
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &StrictDNSClusterConfigurer{}
+
+func (e *StrictDNSClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.Name, e.Endpoints)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/timeout_configurer.go b/pkg/xds/envoy/clusters/v2/timeout_configurer.go
similarity index 57%
rename from pkg/xds/envoy/clusters/timeout_configurer.go
rename to pkg/xds/envoy/clusters/v2/timeout_configurer.go
index 7388ac1c9..8f4536ce0 100644
--- a/pkg/xds/envoy/clusters/timeout_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/timeout_configurer.go
@@ -9,19 +9,13 @@ import (
 
 const defaultConnectTimeout = 5 * time.Second
 
-func ConnectTimeout(timeout time.Duration) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&timeoutConfigurer{
-			connectTimeout: timeout,
-		})
-	})
-}
-
-type timeoutConfigurer struct {
+type TimeoutConfigurer struct {
 	connectTimeout time.Duration
 }
 
-func (t *timeoutConfigurer) Configure(cluster *envoy_api.Cluster) error {
+var _ ClusterConfigurer = &TimeoutConfigurer{}
+
+func (t *TimeoutConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	if t.connectTimeout.Nanoseconds() == 0 {
 		t.connectTimeout = defaultConnectTimeout
 	}
diff --git a/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go
new file mode 100644
index 000000000..e8b1a199c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+)
+
+type UpstreamBindConfigConfigurer struct {
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &UpstreamBindConfigConfigurer{}
+
+func (u *UpstreamBindConfigConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.UpstreamBindConfig = &envoy_core.BindConfig{
+		SourceAddress: &envoy_core.SocketAddress{
+			Address: u.Address,
+			PortSpecifier: &envoy_core.SocketAddress_PortValue{
+				PortValue: u.Port,
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go b/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go
new file mode 100644
index 000000000..16b428c55
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go
@@ -0,0 +1,20 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+)
+
+type AltStatNameConfigurer struct {
+}
+
+var _ ClusterConfigurer = &AltStatNameConfigurer{}
+
+func (e *AltStatNameConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	sanitizedName := util_xds.SanitizeMetric(cluster.Name)
+	if sanitizedName != cluster.Name {
+		cluster.AltStatName = sanitizedName
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go
new file mode 100644
index 000000000..9e1c57770
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go
@@ -0,0 +1,87 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+)
+
+type OutlierDetectionConfigurer struct {
+	CircuitBreaker *mesh_core.CircuitBreakerResource
+}
+
+var _ ClusterConfigurer = &OutlierDetectionConfigurer{}
+
+func (c *OutlierDetectionConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if c.CircuitBreaker == nil {
+		return nil
+	}
+
+	cluster.OutlierDetection = &envoy_cluster.OutlierDetection{
+		Interval:                       c.CircuitBreaker.Spec.GetConf().Interval,
+		BaseEjectionTime:               c.CircuitBreaker.Spec.GetConf().BaseEjectionTime,
+		MaxEjectionPercent:             c.CircuitBreaker.Spec.GetConf().MaxEjectionPercent,
+		SplitExternalLocalOriginErrors: c.CircuitBreaker.Spec.GetConf().SplitExternalAndLocalErrors,
+	}
+	c.configureTotalErrorDetector(cluster.OutlierDetection)
+	c.configureGatewayErrorDetector(cluster.OutlierDetection)
+	c.configureLocalErrorDetector(cluster.OutlierDetection)
+	c.configureStandardDeviationDetector(cluster.OutlierDetection)
+	c.configureFailureDetector(cluster.OutlierDetection)
+	return nil
+}
+
+func (c *OutlierDetectionConfigurer) configureTotalErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if total := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetTotalErrors(); total != nil {
+		outlierDetection.Consecutive_5Xx = total.GetConsecutive()
+		outlierDetection.EnforcingConsecutive_5Xx = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutive_5Xx = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureGatewayErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if gateway := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetGatewayErrors(); gateway != nil {
+		outlierDetection.ConsecutiveGatewayFailure = gateway.GetConsecutive()
+		outlierDetection.EnforcingConsecutiveGatewayFailure = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutiveGatewayFailure = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureLocalErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if local := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetLocalErrors(); local != nil {
+		outlierDetection.ConsecutiveLocalOriginFailure = local.GetConsecutive()
+		outlierDetection.EnforcingConsecutiveLocalOriginFailure = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutiveLocalOriginFailure = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureStandardDeviationDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if stdev := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetStandardDeviation(); stdev != nil {
+		outlierDetection.SuccessRateRequestVolume = stdev.GetRequestVolume()
+		outlierDetection.SuccessRateMinimumHosts = stdev.GetMinimumHosts()
+		if factor := stdev.GetFactor(); factor != nil {
+			outlierDetection.SuccessRateStdevFactor = &wrappers.UInt32Value{Value: uint32(factor.GetValue() * 1000)}
+		}
+		outlierDetection.EnforcingSuccessRate = &wrappers.UInt32Value{Value: 100}
+		outlierDetection.EnforcingLocalOriginSuccessRate = &wrappers.UInt32Value{Value: 100} // takes effect only when SplitExternalLocalOriginErrors is true
+	} else {
+		outlierDetection.EnforcingSuccessRate = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureFailureDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if failure := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetFailure(); failure != nil {
+		outlierDetection.FailurePercentageRequestVolume = failure.GetRequestVolume()
+		outlierDetection.FailurePercentageMinimumHosts = failure.GetMinimumHosts()
+		outlierDetection.FailurePercentageThreshold = failure.GetThreshold()
+
+		outlierDetection.EnforcingFailurePercentage = &wrappers.UInt32Value{Value: 100}
+		outlierDetection.EnforcingFailurePercentageLocalOrigin = &wrappers.UInt32Value{Value: 100} // takes effect only when SplitExternalLocalOriginErrors is true
+	} else {
+		outlierDetection.EnforcingFailurePercentage = &wrappers.UInt32Value{Value: 0}
+	}
+}
diff --git a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go
new file mode 100644
index 000000000..df2a8476a
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go
@@ -0,0 +1,84 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls_v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+)
+
+type ClientSideMTLSConfigurer struct {
+	Ctx           xds_context.Context
+	Metadata      *core_xds.DataplaneMetadata
+	ClientService string
+	Tags          []envoy.Tags
+}
+
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
+
+func (c *ClientSideMTLSConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if !c.Ctx.Mesh.Resource.MTLSEnabled() {
+		return nil
+	}
+	mesh := c.Ctx.Mesh.Resource.GetMeta().GetName()
+	// there might be a situation when there are multiple sam tags passed here for example two outbound listeners with the same tags, therefore we need to distinguish between them.
+	distinctTags := envoy.DistinctTags(c.Tags)
+	switch {
+	case len(distinctTags) == 0:
+		transportSocket, err := c.createTransportSocket("")
+		if err != nil {
+			return err
+		}
+		cluster.TransportSocket = transportSocket
+	case len(distinctTags) == 1:
+		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.Tags[0].WithTags("mesh", mesh)))
+		if err != nil {
+			return err
+		}
+		cluster.TransportSocket = transportSocket
+	default:
+		for _, tags := range distinctTags {
+			sni := tls.SNIFromTags(tags.WithTags("mesh", mesh))
+			transportSocket, err := c.createTransportSocket(sni)
+			if err != nil {
+				return err
+			}
+			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_cluster.Cluster_TransportSocketMatch{
+				Name: sni,
+				Match: &pstruct.Struct{
+					Fields: envoy_metadata.MetadataFields(tags),
+				},
+				TransportSocket: transportSocket,
+			})
+		}
+	}
+	return nil
+}
+
+func (c *ClientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
+	tlsContext, err := tls_v3.CreateUpstreamTlsContext(c.Ctx, c.Metadata, c.ClientService, sni)
+	if err != nil {
+		return nil, err
+	}
+	if tlsContext == nil {
+		return nil, nil
+	}
+	pbst, err := proto.MarshalAnyDeterministic(tlsContext)
+	if err != nil {
+		return nil, err
+	}
+	transportSocket := &envoy_core.TransportSocket{
+		Name: "envoy.transport_sockets.tls",
+		ConfigType: &envoy_core.TransportSocket_TypedConfig{
+			TypedConfig: pbst,
+		},
+	}
+	return transportSocket, nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go
new file mode 100644
index 000000000..1440111ce
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go
@@ -0,0 +1,56 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+	tls_v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type ClientSideTLSConfigurer struct {
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
+
+func (c *ClientSideTLSConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	for _, ep := range c.Endpoints {
+		if ep.ExternalService.TLSEnabled {
+			tlsContext, err := tls_v3.UpstreamTlsContextOutsideMesh(
+				ep.ExternalService.CaCert,
+				ep.ExternalService.ClientCert,
+				ep.ExternalService.ClientKey,
+				ep.Target)
+			if err != nil {
+				return err
+			}
+
+			pbst, err := proto.MarshalAnyDeterministic(tlsContext)
+			if err != nil {
+				return err
+			}
+
+			transportSocket := &envoy_core.TransportSocket{
+				Name: "envoy.transport_sockets.tls",
+				ConfigType: &envoy_core.TransportSocket_TypedConfig{
+					TypedConfig: pbst,
+				},
+			}
+
+			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_cluster.Cluster_TransportSocketMatch{
+				Name: ep.Target,
+				Match: &pstruct.Struct{
+					Fields: envoy_metadata.MetadataFields(ep.Tags),
+				},
+				TransportSocket: transportSocket,
+			})
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/configurer.go b/pkg/xds/envoy/clusters/v3/configurer.go
new file mode 100644
index 000000000..e148b5375
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/configurer.go
@@ -0,0 +1,12 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
+// such as filter chain, transparent proxying, etc.
+type ClusterConfigurer interface {
+	// Configure configures a single aspect on a given Envoy cluster.
+	Configure(cluster *envoy_cluster.Cluster) error
+}
diff --git a/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go
new file mode 100644
index 000000000..23efffa86
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type DnsClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &DnsClusterConfigurer{}
+
+func (e *DnsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_cluster.Cluster_ROUND_ROBIN
+	c.LoadAssignment = endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
new file mode 100644
index 000000000..b5019132c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+)
+
+type EdsClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &EdsClusterConfigurer{}
+
+func (e *EdsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_EDS}
+	c.EdsClusterConfig = &envoy_cluster.Cluster_EdsClusterConfig{
+		EdsConfig: &envoy_core.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core.ConfigSource_Ads{
+				Ads: &envoy_core.AggregatedConfigSource{},
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer.go b/pkg/xds/envoy/clusters/v3/health_check_configurer.go
new file mode 100644
index 000000000..eaff1d251
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer.go
@@ -0,0 +1,122 @@
+package clusters
+
+import (
+	"encoding/hex"
+
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+)
+
+type HealthCheckConfigurer struct {
+	HealthCheck *mesh_core.HealthCheckResource
+}
+
+var _ ClusterConfigurer = &HealthCheckConfigurer{}
+
+func mapUInt32ToInt64Range(value uint32) *envoy_type.Int64Range {
+	return &envoy_type.Int64Range{
+		Start: int64(value),
+		End:   int64(value) + 1,
+	}
+}
+
+func mapHttpHeaders(
+	headers []*mesh_proto.HealthCheck_Conf_Http_HeaderValueOption,
+) []*envoy_core.HeaderValueOption {
+	var envoyHeaders []*envoy_core.HeaderValueOption
+	for _, header := range headers {
+		envoyHeaders = append(envoyHeaders, &envoy_core.HeaderValueOption{
+			Header: &envoy_core.HeaderValue{
+				Key:   header.Header.Key,
+				Value: header.Header.Value,
+			},
+			Append: header.Append,
+		})
+	}
+	return envoyHeaders
+}
+
+func tcpHealthCheck(
+	tcpConf *mesh_proto.HealthCheck_Conf_Tcp,
+) *envoy_core.HealthCheck_TcpHealthCheck_ {
+	tcpHealthCheck := envoy_core.HealthCheck_TcpHealthCheck{}
+
+	if tcpConf.Send != nil {
+		tcpHealthCheck.Send = &envoy_core.HealthCheck_Payload{
+			Payload: &envoy_core.HealthCheck_Payload_Text{
+				Text: hex.EncodeToString(tcpConf.Send.Value),
+			},
+		}
+	}
+
+	if tcpConf.Receive != nil {
+		var receive []*envoy_core.HealthCheck_Payload
+		for _, r := range tcpConf.Receive {
+			receive = append(receive, &envoy_core.HealthCheck_Payload{
+				Payload: &envoy_core.HealthCheck_Payload_Text{
+					Text: hex.EncodeToString(r.Value),
+				},
+			})
+		}
+		tcpHealthCheck.Receive = receive
+	}
+
+	return &envoy_core.HealthCheck_TcpHealthCheck_{
+		TcpHealthCheck: &tcpHealthCheck,
+	}
+}
+
+func httpHealthCheck(
+	httpConf *mesh_proto.HealthCheck_Conf_Http,
+) *envoy_core.HealthCheck_HttpHealthCheck_ {
+	var expectedStatuses []*envoy_type.Int64Range
+	for _, status := range httpConf.ExpectedStatuses {
+		expectedStatuses = append(
+			expectedStatuses,
+			mapUInt32ToInt64Range(status.Value),
+		)
+	}
+
+	httpHealthCheck := envoy_core.HealthCheck_HttpHealthCheck{
+		Path:                httpConf.Path,
+		RequestHeadersToAdd: mapHttpHeaders(httpConf.RequestHeadersToAdd),
+		ExpectedStatuses:    expectedStatuses,
+		CodecClientType:     envoy_type.CodecClientType_HTTP2,
+	}
+
+	return &envoy_core.HealthCheck_HttpHealthCheck_{
+		HttpHealthCheck: &httpHealthCheck,
+	}
+}
+
+func (e *HealthCheckConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if e.HealthCheck == nil || e.HealthCheck.Spec.Conf == nil {
+		return nil
+	}
+	activeChecks := e.HealthCheck.Spec.Conf
+	healthCheck := envoy_core.HealthCheck{
+		HealthChecker: &envoy_core.HealthCheck_TcpHealthCheck_{
+			TcpHealthCheck: &envoy_core.HealthCheck_TcpHealthCheck{},
+		},
+		Interval:           activeChecks.Interval,
+		Timeout:            activeChecks.Timeout,
+		UnhealthyThreshold: &wrappers.UInt32Value{Value: activeChecks.UnhealthyThreshold},
+		HealthyThreshold:   &wrappers.UInt32Value{Value: activeChecks.HealthyThreshold},
+	}
+
+	if tcp := activeChecks.GetTcp(); tcp != nil {
+		healthCheck.HealthChecker = tcpHealthCheck(tcp)
+	}
+
+	if http := activeChecks.GetHttp(); http != nil {
+		healthCheck.HealthChecker = httpHealthCheck(http)
+	}
+
+	cluster.HealthChecks = append(cluster.HealthChecks, &healthCheck)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/http2_configurer.go b/pkg/xds/envoy/clusters/v3/http2_configurer.go
new file mode 100644
index 000000000..84cff553d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/http2_configurer.go
@@ -0,0 +1,36 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_upstream_http "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
+	"github.com/golang/protobuf/ptypes/any"
+
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type Http2Configurer struct {
+}
+
+var _ ClusterConfigurer = &Http2Configurer{}
+
+func (p *Http2Configurer) Configure(c *envoy_cluster.Cluster) error {
+	options := &envoy_upstream_http.HttpProtocolOptions{
+		UpstreamProtocolOptions: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig_{
+			ExplicitHttpConfig: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig{
+				ProtocolConfig: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{
+					Http2ProtocolOptions: &envoy_core.Http2ProtocolOptions{},
+				},
+			},
+		},
+	}
+
+	pbst, err := proto.MarshalAnyDeterministic(options)
+	if err != nil {
+		return err
+	}
+	c.TypedExtensionProtocolOptions = map[string]*any.Any{
+		"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": pbst,
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go
new file mode 100644
index 000000000..508dae5dc
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go
@@ -0,0 +1,33 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+type LbSubsetConfigurer struct {
+	KeySets [][]string
+}
+
+var _ ClusterConfigurer = &LbSubsetConfigurer{}
+
+func (e *LbSubsetConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	var selectors []*envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector
+	for _, keys := range e.KeySets {
+		if len(keys) == 0 {
+			continue
+		}
+		selectors = append(selectors, &envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector{
+			Keys: keys,
+			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
+			FallbackPolicy: envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
+		})
+	}
+	if len(selectors) > 0 {
+		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
+		c.LbSubsetConfig = &envoy_cluster.Cluster_LbSubsetConfig{
+			FallbackPolicy:  envoy_cluster.Cluster_LbSubsetConfig_ANY_ENDPOINT,
+			SubsetSelectors: selectors,
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go
new file mode 100644
index 000000000..b5415e650
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go
@@ -0,0 +1,18 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+type PassThroughClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &PassThroughClusterConfigurer{}
+
+func (p *PassThroughClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = p.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_ORIGINAL_DST}
+	c.LbPolicy = envoy_cluster.Cluster_CLUSTER_PROVIDED
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go
new file mode 100644
index 000000000..7729b38ec
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go
@@ -0,0 +1,22 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type StaticClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &StaticClusterConfigurer{}
+
+func (e *StaticClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STATIC}
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go
new file mode 100644
index 000000000..e7f877530
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type StrictDNSClusterConfigurer struct {
+	Name      string
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &StrictDNSClusterConfigurer{}
+
+func (e *StrictDNSClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_cluster.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.Name, e.Endpoints)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/timeout_configurer.go b/pkg/xds/envoy/clusters/v3/timeout_configurer.go
new file mode 100644
index 000000000..690b28c80
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/timeout_configurer.go
@@ -0,0 +1,24 @@
+package clusters
+
+import (
+	"time"
+
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	"github.com/golang/protobuf/ptypes"
+)
+
+const defaultConnectTimeout = 5 * time.Second
+
+type TimeoutConfigurer struct {
+	connectTimeout time.Duration
+}
+
+var _ ClusterConfigurer = &TimeoutConfigurer{}
+
+func (t *TimeoutConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if t.connectTimeout.Nanoseconds() == 0 {
+		t.connectTimeout = defaultConnectTimeout
+	}
+	cluster.ConnectTimeout = ptypes.DurationProto(t.connectTimeout)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go
new file mode 100644
index 000000000..5bb388695
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+)
+
+type UpstreamBindConfigConfigurer struct {
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &UpstreamBindConfigConfigurer{}
+
+func (u *UpstreamBindConfigConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.UpstreamBindConfig = &envoy_core.BindConfig{
+		SourceAddress: &envoy_core.SocketAddress{
+			Address: u.Address,
+			PortSpecifier: &envoy_core.SocketAddress_PortValue{
+				PortValue: u.Port,
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/endpoints/endpoints.go b/pkg/xds/envoy/endpoints/v2/endpoints.go
similarity index 97%
rename from pkg/xds/envoy/endpoints/endpoints.go
rename to pkg/xds/envoy/endpoints/v2/endpoints.go
index 5f9652c96..1e5341f34 100644
--- a/pkg/xds/envoy/endpoints/endpoints.go
+++ b/pkg/xds/envoy/endpoints/v2/endpoints.go
@@ -9,7 +9,7 @@ import (
 	proto_wrappers "github.com/golang/protobuf/ptypes/wrappers"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
-	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func CreateStaticEndpoint(clusterName string, address string, port uint32) *envoy_api.ClusterLoadAssignment {
@@ -46,7 +46,7 @@ func CreateClusterLoadAssignment(clusterName string, endpoints []core_xds.Endpoi
 			LoadBalancingWeight: &proto_wrappers.UInt32Value{
 				Value: ep.Weight,
 			},
-			Metadata: envoy_common.EndpointMetadata(ep.Tags),
+			Metadata: envoy_metadata.EndpointMetadata(ep.Tags),
 			HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
 				Endpoint: &envoy_endpoint.Endpoint{
 					Address: &envoy_core.Address{
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints.go b/pkg/xds/envoy/endpoints/v3/endpoints.go
new file mode 100644
index 000000000..32dd49d91
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints.go
@@ -0,0 +1,130 @@
+package endpoints
+
+import (
+	"sort"
+
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
+	proto_wrappers "github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	envoy "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+)
+
+func CreateStaticEndpoint(clusterName string, address string, port uint32) *envoy_endpoint.ClusterLoadAssignment {
+	return &envoy_endpoint.ClusterLoadAssignment{
+		ClusterName: clusterName,
+		Endpoints: []*envoy_endpoint.LocalityLbEndpoints{{
+			LbEndpoints: []*envoy_endpoint.LbEndpoint{{
+				HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
+					Endpoint: &envoy_endpoint.Endpoint{
+						Address: &envoy_core.Address{
+							Address: &envoy_core.Address_SocketAddress{
+								SocketAddress: &envoy_core.SocketAddress{
+									Protocol: envoy_core.SocketAddress_TCP,
+									Address:  address,
+									PortSpecifier: &envoy_core.SocketAddress_PortValue{
+										PortValue: port,
+									},
+								},
+							},
+						},
+					},
+				},
+			}},
+		}},
+	}
+}
+
+func CreateClusterLoadAssignment(clusterName string, endpoints []core_xds.Endpoint) *envoy_endpoint.ClusterLoadAssignment {
+	localityLbEndpoints := LocalityLbEndpointsMap{}
+
+	for _, ep := range endpoints {
+		lbEndpoints := localityLbEndpoints.Get(ep)
+		lbEndpoints.LbEndpoints = append(lbEndpoints.LbEndpoints, &envoy_endpoint.LbEndpoint{
+			LoadBalancingWeight: &proto_wrappers.UInt32Value{
+				Value: ep.Weight,
+			},
+			Metadata: envoy.EndpointMetadata(ep.Tags),
+			HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
+				Endpoint: &envoy_endpoint.Endpoint{
+					Address: &envoy_core.Address{
+						Address: &envoy_core.Address_SocketAddress{
+							SocketAddress: &envoy_core.SocketAddress{
+								Protocol: envoy_core.SocketAddress_TCP,
+								Address:  ep.Target,
+								PortSpecifier: &envoy_core.SocketAddress_PortValue{
+									PortValue: ep.Port,
+								},
+							},
+						},
+					},
+				}},
+		})
+	}
+
+	for _, lbEndpoints := range localityLbEndpoints {
+		// sort the slice to ensure stable Envoy configuration
+		sortLbEndpoints(lbEndpoints.LbEndpoints)
+	}
+
+	return &envoy_endpoint.ClusterLoadAssignment{
+		ClusterName: clusterName,
+		Endpoints:   localityLbEndpoints.AsSlice(),
+	}
+}
+
+func sortLbEndpoints(lbEndpoints []*envoy_endpoint.LbEndpoint) {
+	sort.Slice(lbEndpoints, func(i, j int) bool {
+		left, right := lbEndpoints[i], lbEndpoints[j]
+		leftAddr := left.GetEndpoint().GetAddress().GetSocketAddress().GetAddress()
+		rightAddr := right.GetEndpoint().GetAddress().GetSocketAddress().GetAddress()
+		if leftAddr == rightAddr {
+			return left.GetEndpoint().GetAddress().GetSocketAddress().GetPortValue() < right.GetEndpoint().GetAddress().GetSocketAddress().GetPortValue()
+		}
+		return leftAddr < rightAddr
+	})
+}
+
+type LocalityLbEndpointsMap map[string]*envoy_endpoint.LocalityLbEndpoints
+
+func (l LocalityLbEndpointsMap) Get(ep core_xds.Endpoint) *envoy_endpoint.LocalityLbEndpoints {
+	key := ep.LocalityString()
+	if _, ok := l[key]; !ok {
+		var locality *envoy_core.Locality
+		priority := uint32(0)
+		if ep.HasLocality() {
+			locality = &envoy_core.Locality{
+				Region:  ep.Locality.Region,
+				Zone:    ep.Locality.Zone,
+				SubZone: ep.Locality.SubZone,
+			}
+			priority = ep.Locality.Priority
+		}
+
+		l[key] = &envoy_endpoint.LocalityLbEndpoints{
+			LbEndpoints: make([]*envoy_endpoint.LbEndpoint, 0),
+			Locality:    locality,
+			Priority:    priority,
+		}
+	}
+
+	return l[key]
+}
+
+func (l LocalityLbEndpointsMap) AsSlice() []*envoy_endpoint.LocalityLbEndpoints {
+	slice := make([]*envoy_endpoint.LocalityLbEndpoints, 0)
+
+	for _, lle := range l {
+		slice = append(slice, lle)
+	}
+
+	// sort the slice to ensure stable Envoy configuration
+	sort.Slice(slice, func(i, j int) bool {
+		left, right := slice[i], slice[j]
+		return (left.Locality.Region + left.Locality.Zone + left.Locality.SubZone) >
+			(right.Locality.Region + right.Locality.Zone + right.Locality.SubZone)
+	})
+
+	return slice
+}
diff --git a/pkg/xds/envoy/listeners/network_rbac_configurer.go b/pkg/xds/envoy/listeners/network_rbac_configurer.go
index b00ff80f3..73f1f1202 100644
--- a/pkg/xds/envoy/listeners/network_rbac_configurer.go
+++ b/pkg/xds/envoy/listeners/network_rbac_configurer.go
@@ -3,7 +3,7 @@ package listeners
 import (
 	"fmt"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
 	rbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rbac/v2"
diff --git a/pkg/xds/envoy/listeners/server_mtls_configurer.go b/pkg/xds/envoy/listeners/server_mtls_configurer.go
index 3644c63b9..9cc803bef 100644
--- a/pkg/xds/envoy/listeners/server_mtls_configurer.go
+++ b/pkg/xds/envoy/listeners/server_mtls_configurer.go
@@ -4,7 +4,7 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
diff --git a/pkg/xds/envoy/listeners/tcp_proxy_configurer.go b/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
index 97658bf3a..86896f6f2 100644
--- a/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
+++ b/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
@@ -7,6 +7,7 @@ import (
 	"github.com/kumahq/kuma/pkg/util/proto"
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func TcpProxy(statsName string, clusters ...envoy_common.ClusterSubset) FilterChainBuilderOpt {
@@ -52,14 +53,14 @@ func (c *TcpProxyConfigurer) tcpProxy() *envoy_tcp.TcpProxy {
 		proxy.ClusterSpecifier = &envoy_tcp.TcpProxy_Cluster{
 			Cluster: c.clusters[0].ClusterName,
 		}
-		proxy.MetadataMatch = envoy_common.LbMetadata(c.clusters[0].Tags)
+		proxy.MetadataMatch = envoy_metadata.LbMetadata(c.clusters[0].Tags)
 	} else {
 		var weightedClusters []*envoy_tcp.TcpProxy_WeightedCluster_ClusterWeight
 		for _, cluster := range c.clusters {
 			weightedClusters = append(weightedClusters, &envoy_tcp.TcpProxy_WeightedCluster_ClusterWeight{
 				Name:          cluster.ClusterName,
 				Weight:        cluster.Weight,
-				MetadataMatch: envoy_common.LbMetadata(cluster.Tags),
+				MetadataMatch: envoy_metadata.LbMetadata(cluster.Tags),
 			})
 		}
 		proxy.ClusterSpecifier = &envoy_tcp.TcpProxy_WeightedClusters{
diff --git a/pkg/xds/envoy/metadata.go b/pkg/xds/envoy/metadata/v2/metadata.go
similarity index 82%
rename from pkg/xds/envoy/metadata.go
rename to pkg/xds/envoy/metadata/v2/metadata.go
index 6c5856d8a..d40d92cc8 100644
--- a/pkg/xds/envoy/metadata.go
+++ b/pkg/xds/envoy/metadata/v2/metadata.go
@@ -5,9 +5,11 @@ import (
 	pstruct "github.com/golang/protobuf/ptypes/struct"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
 )
 
-func EndpointMetadata(tags Tags) *envoy_core.Metadata {
+func EndpointMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
 	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
 	if len(tags) == 0 {
 		return nil
@@ -26,7 +28,7 @@ func EndpointMetadata(tags Tags) *envoy_core.Metadata {
 	return metadata
 }
 
-func LbMetadata(tags Tags) *envoy_core.Metadata {
+func LbMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
 	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
 	if len(tags) == 0 {
 		return nil
@@ -42,7 +44,7 @@ func LbMetadata(tags Tags) *envoy_core.Metadata {
 	return metadata
 }
 
-func MetadataFields(tags Tags) map[string]*pstruct.Value {
+func MetadataFields(tags envoy_common.Tags) map[string]*pstruct.Value {
 	fields := map[string]*pstruct.Value{}
 	for key, value := range tags {
 		if key == mesh_proto.ServiceTag {
diff --git a/pkg/xds/envoy/metadata_test.go b/pkg/xds/envoy/metadata/v2/metadata_test.go
similarity index 100%
rename from pkg/xds/envoy/metadata_test.go
rename to pkg/xds/envoy/metadata/v2/metadata_test.go
diff --git a/pkg/xds/envoy/metadata/v3/metadata.go b/pkg/xds/envoy/metadata/v3/metadata.go
new file mode 100644
index 000000000..82408a2e9
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata.go
@@ -0,0 +1,60 @@
+package envoy
+
+import (
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+)
+
+func EndpointMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
+	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
+	if len(tags) == 0 {
+		return nil
+	}
+	fields := MetadataFields(tags)
+	metadata := &envoy_core.Metadata{
+		FilterMetadata: map[string]*pstruct.Struct{
+			"envoy.lb": {
+				Fields: fields,
+			},
+			"envoy.transport_socket_match": {
+				Fields: fields,
+			},
+		},
+	}
+	return metadata
+}
+
+func LbMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
+	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
+	if len(tags) == 0 {
+		return nil
+	}
+	fields := MetadataFields(tags)
+	metadata := &envoy_core.Metadata{
+		FilterMetadata: map[string]*pstruct.Struct{
+			"envoy.lb": {
+				Fields: fields,
+			},
+		},
+	}
+	return metadata
+}
+
+func MetadataFields(tags envoy_common.Tags) map[string]*pstruct.Value {
+	fields := map[string]*pstruct.Value{}
+	for key, value := range tags {
+		if key == mesh_proto.ServiceTag {
+			continue
+		}
+		fields[key] = &pstruct.Value{
+			Kind: &pstruct.Value_StringValue{
+				StringValue: value,
+			},
+		}
+	}
+	return fields
+}
diff --git a/pkg/xds/envoy/routes/default_route_configurer.go b/pkg/xds/envoy/routes/default_route_configurer.go
index 017d129fe..744d6c72c 100644
--- a/pkg/xds/envoy/routes/default_route_configurer.go
+++ b/pkg/xds/envoy/routes/default_route_configurer.go
@@ -7,6 +7,7 @@ import (
 	envoy_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
 
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func DefaultRoute(subsets ...envoy_common.ClusterSubset) VirtualHostBuilderOpt {
@@ -53,7 +54,7 @@ func (c RouteConfigurer) routeAction() *envoy_route.RouteAction {
 		routeAction.ClusterSpecifier = &envoy_route.RouteAction_Cluster{
 			Cluster: c.subsets[0].ClusterName,
 		}
-		routeAction.MetadataMatch = envoy_common.LbMetadata(c.subsets[0].Tags)
+		routeAction.MetadataMatch = envoy_metadata.LbMetadata(c.subsets[0].Tags)
 	} else {
 		var weightedClusters []*envoy_route.WeightedCluster_ClusterWeight
 		var totalWeight uint32
@@ -61,7 +62,7 @@ func (c RouteConfigurer) routeAction() *envoy_route.RouteAction {
 			weightedClusters = append(weightedClusters, &envoy_route.WeightedCluster_ClusterWeight{
 				Name:          subset.ClusterName,
 				Weight:        &wrappers.UInt32Value{Value: subset.Weight},
-				MetadataMatch: envoy_common.LbMetadata(subset.Tags),
+				MetadataMatch: envoy_metadata.LbMetadata(subset.Tags),
 			})
 			totalWeight += subset.Weight
 		}
diff --git a/pkg/xds/envoy/tls/tls.go b/pkg/xds/envoy/tls/tls.go
index 53b65c4bb..a42601355 100644
--- a/pkg/xds/envoy/tls/tls.go
+++ b/pkg/xds/envoy/tls/tls.go
@@ -2,17 +2,6 @@ package tls
 
 import (
 	"fmt"
-
-	envoy_auth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-	envoy_grpc_credential "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
-	envoy_type_matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
-	"github.com/golang/protobuf/ptypes/wrappers"
-
-	core_xds "github.com/kumahq/kuma/pkg/core/xds"
-	"github.com/kumahq/kuma/pkg/util/proto"
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
-	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 )
 
 const (
@@ -20,200 +9,10 @@ const (
 	IdentityCertResource = "identity_cert"
 )
 
-// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
-// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
-// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
-// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
-func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_auth.DownstreamTlsContext, error) {
-	if !ctx.Mesh.Resource.MTLSEnabled() {
-		return nil, nil
-	}
-	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
-	commonTlsContext, err := CreateCommonTlsContext(ctx, metadata, validationSANMatcher)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.DownstreamTlsContext{
-		CommonTlsContext:         commonTlsContext,
-		RequireClientCertificate: &wrappers.BoolValue{Value: true},
-	}, nil
-}
-
-// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
-// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
-// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
-// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
-// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
-//
-// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
-func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_auth.UpstreamTlsContext, error) {
-	if !ctx.Mesh.Resource.MTLSEnabled() {
-		return nil, nil
-	}
-	var validationSANMatcher *envoy_type_matcher.StringMatcher
-	if upstreamService == "*" {
-		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
-	} else {
-		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
-	}
-	commonTlsContext, err := CreateCommonTlsContext(ctx, metadata, validationSANMatcher)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.UpstreamTlsContext{
-		CommonTlsContext: commonTlsContext,
-		Sni:              sni,
-	}, nil
-}
-
-func CreateCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher.StringMatcher) (*envoy_auth.CommonTlsContext, error) {
-	meshCaSecret, err := sdsSecretConfig(ctx, MeshCaResource, metadata)
-	if err != nil {
-		return nil, err
-	}
-	identitySecret, err := sdsSecretConfig(ctx, IdentityCertResource, metadata)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.CommonTlsContext{
-		ValidationContextType: &envoy_auth.CommonTlsContext_CombinedValidationContext{
-			CombinedValidationContext: &envoy_auth.CommonTlsContext_CombinedCertificateValidationContext{
-				DefaultValidationContext: &envoy_auth.CertificateValidationContext{
-					MatchSubjectAltNames: []*envoy_type_matcher.StringMatcher{validationSANMatcher},
-				},
-				ValidationContextSdsSecretConfig: meshCaSecret,
-			},
-		},
-		TlsCertificateSdsSecretConfigs: []*envoy_auth.SdsSecretConfig{
-			identitySecret,
-		},
-	}, nil
-}
-
-func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_auth.SdsSecretConfig, error) {
-	withCallCredentials := func(grpc *envoy_core.GrpcService_GoogleGrpc) (*envoy_core.GrpcService_GoogleGrpc, error) {
-		if metadata.GetDataplaneTokenPath() == "" {
-			return grpc, nil
-		}
-
-		config := &envoy_grpc_credential.FileBasedMetadataConfig{
-			SecretData: &envoy_core.DataSource{
-				Specifier: &envoy_core.DataSource_Filename{
-					Filename: metadata.GetDataplaneTokenPath(),
-				},
-			},
-		}
-		typedConfig, err := proto.MarshalAnyDeterministic(config)
-		if err != nil {
-			return nil, err
-		}
-
-		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core.GrpcService_GoogleGrpc_CallCredentials{
-			CredentialSpecifier: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
-				FromPlugin: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
-					Name: "envoy.grpc_credentials.file_based_metadata",
-					ConfigType: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
-						TypedConfig: typedConfig,
-					},
-				},
-			},
-		})
-		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
-
-		return grpc, nil
-	}
-	googleGrpc, err := withCallCredentials(&envoy_core.GrpcService_GoogleGrpc{
-		TargetUri:  context.SDSLocation(),
-		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
-		ChannelCredentials: &envoy_core.GrpcService_GoogleGrpc_ChannelCredentials{
-			CredentialSpecifier: &envoy_core.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
-				SslCredentials: &envoy_core.GrpcService_GoogleGrpc_SslCredentials{
-					RootCerts: &envoy_core.DataSource{
-						Specifier: &envoy_core.DataSource_InlineBytes{
-							InlineBytes: context.ControlPlane.SdsTlsCert,
-						},
-					},
-				},
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.SdsSecretConfig{
-		Name: name,
-		SdsConfig: &envoy_core.ConfigSource{
-			ConfigSourceSpecifier: &envoy_core.ConfigSource_ApiConfigSource{
-				ApiConfigSource: &envoy_core.ApiConfigSource{
-					ApiType: envoy_core.ApiConfigSource_GRPC,
-					GrpcServices: []*envoy_core.GrpcService{
-						{
-							TargetSpecifier: &envoy_core.GrpcService_GoogleGrpc_{
-								GoogleGrpc: googleGrpc,
-							},
-						},
-					},
-				},
-			},
-		},
-	}, nil
-}
-
 func MeshSpiffeIDPrefix(mesh string) string {
 	return fmt.Sprintf("spiffe://%s/", mesh)
 }
 
-func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher.StringMatcher {
-	return &envoy_type_matcher.StringMatcher{
-		MatchPattern: &envoy_type_matcher.StringMatcher_Prefix{
-			Prefix: MeshSpiffeIDPrefix(mesh),
-		},
-	}
-}
-
 func ServiceSpiffeID(mesh string, service string) string {
 	return fmt.Sprintf("spiffe://%s/%s", mesh, service)
 }
-
-func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher.StringMatcher {
-	return &envoy_type_matcher.StringMatcher{
-		MatchPattern: &envoy_type_matcher.StringMatcher_Exact{
-			Exact: ServiceSpiffeID(mesh, service),
-		},
-	}
-}
-
-func UpstreamTlsContextOutsideMesh(ca, cert, key *envoy_core.DataSource, hostname string) (*envoy_auth.UpstreamTlsContext, error) {
-	var tlsCertificates []*envoy_auth.TlsCertificate
-	if cert != nil && key != nil {
-		tlsCertificates = []*envoy_auth.TlsCertificate{
-			{
-				CertificateChain: cert,
-				PrivateKey:       key,
-			},
-		}
-	}
-
-	var validationContextType *envoy_auth.CommonTlsContext_ValidationContext
-	if ca != nil {
-		validationContextType = &envoy_auth.CommonTlsContext_ValidationContext{
-			ValidationContext: &envoy_auth.CertificateValidationContext{
-				TrustedCa: ca,
-				MatchSubjectAltNames: []*envoy_type_matcher.StringMatcher{
-					{
-						MatchPattern: &envoy_type_matcher.StringMatcher_Exact{
-							Exact: hostname,
-						},
-					},
-				},
-			},
-		}
-	}
-
-	return &envoy_auth.UpstreamTlsContext{
-		CommonTlsContext: &envoy_auth.CommonTlsContext{
-			TlsCertificates:       tlsCertificates,
-			ValidationContextType: validationContextType,
-		},
-	}, nil
-}
diff --git a/pkg/xds/envoy/tls/v2/tls.go b/pkg/xds/envoy/tls/v2/tls.go
new file mode 100644
index 000000000..c67366676
--- /dev/null
+++ b/pkg/xds/envoy/tls/v2/tls.go
@@ -0,0 +1,213 @@
+package v2
+
+import (
+	envoy_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
+	envoy_core_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_grpc_credential_v2 "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
+	envoy_type_matcher_v2 "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+)
+
+// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
+// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
+// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
+// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
+func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_auth_v2.DownstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.DownstreamTlsContext{
+		CommonTlsContext:         commonTlsContext,
+		RequireClientCertificate: &wrappers.BoolValue{Value: true},
+	}, nil
+}
+
+// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
+// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
+// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
+// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
+// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
+//
+// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
+func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_auth_v2.UpstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	var validationSANMatcher *envoy_type_matcher_v2.StringMatcher
+	if upstreamService == "*" {
+		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	} else {
+		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
+	}
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.UpstreamTlsContext{
+		CommonTlsContext: commonTlsContext,
+		Sni:              sni,
+	}, nil
+}
+
+func createCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher_v2.StringMatcher) (*envoy_auth_v2.CommonTlsContext, error) {
+	meshCaSecret, err := sdsSecretConfig(ctx, tls.MeshCaResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	identitySecret, err := sdsSecretConfig(ctx, tls.IdentityCertResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.CommonTlsContext{
+		ValidationContextType: &envoy_auth_v2.CommonTlsContext_CombinedValidationContext{
+			CombinedValidationContext: &envoy_auth_v2.CommonTlsContext_CombinedCertificateValidationContext{
+				DefaultValidationContext: &envoy_auth_v2.CertificateValidationContext{
+					MatchSubjectAltNames: []*envoy_type_matcher_v2.StringMatcher{validationSANMatcher},
+				},
+				ValidationContextSdsSecretConfig: meshCaSecret,
+			},
+		},
+		TlsCertificateSdsSecretConfigs: []*envoy_auth_v2.SdsSecretConfig{
+			identitySecret,
+		},
+	}, nil
+}
+
+func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_auth_v2.SdsSecretConfig, error) {
+	withCallCredentials := func(grpc *envoy_core_v2.GrpcService_GoogleGrpc) (*envoy_core_v2.GrpcService_GoogleGrpc, error) {
+		if metadata.GetDataplaneTokenPath() == "" {
+			return grpc, nil
+		}
+
+		config := &envoy_grpc_credential_v2.FileBasedMetadataConfig{
+			SecretData: &envoy_core_v2.DataSource{
+				Specifier: &envoy_core_v2.DataSource_Filename{
+					Filename: metadata.GetDataplaneTokenPath(),
+				},
+			},
+		}
+		typedConfig, err := proto.MarshalAnyDeterministic(config)
+		if err != nil {
+			return nil, err
+		}
+
+		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials{
+			CredentialSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
+				FromPlugin: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
+					Name: "envoy.grpc_credentials.file_based_metadata",
+					ConfigType: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
+						TypedConfig: typedConfig,
+					},
+				},
+			},
+		})
+		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
+
+		return grpc, nil
+	}
+	googleGrpc, err := withCallCredentials(&envoy_core_v2.GrpcService_GoogleGrpc{
+		TargetUri:  context.SDSLocation(),
+		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
+		ChannelCredentials: &envoy_core_v2.GrpcService_GoogleGrpc_ChannelCredentials{
+			CredentialSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
+				SslCredentials: &envoy_core_v2.GrpcService_GoogleGrpc_SslCredentials{
+					RootCerts: &envoy_core_v2.DataSource{
+						Specifier: &envoy_core_v2.DataSource_InlineBytes{
+							InlineBytes: context.ControlPlane.SdsTlsCert,
+						},
+					},
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.SdsSecretConfig{
+		Name: name,
+		SdsConfig: &envoy_core_v2.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core_v2.ConfigSource_ApiConfigSource{
+				ApiConfigSource: &envoy_core_v2.ApiConfigSource{
+					ApiType: envoy_core_v2.ApiConfigSource_GRPC,
+					GrpcServices: []*envoy_core_v2.GrpcService{
+						{
+							TargetSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_{
+								GoogleGrpc: googleGrpc,
+							},
+						},
+					},
+				},
+			},
+		},
+	}, nil
+}
+
+func UpstreamTlsContextOutsideMesh(ca, cert, key []byte, hostname string) (*envoy_auth_v2.UpstreamTlsContext, error) {
+	var tlsCertificates []*envoy_auth_v2.TlsCertificate
+	if cert != nil && key != nil {
+		tlsCertificates = []*envoy_auth_v2.TlsCertificate{
+			{
+				CertificateChain: dataSourceFromBytes(cert),
+				PrivateKey:       dataSourceFromBytes(key),
+			},
+		}
+	}
+
+	var validationContextType *envoy_auth_v2.CommonTlsContext_ValidationContext
+	if ca != nil {
+		validationContextType = &envoy_auth_v2.CommonTlsContext_ValidationContext{
+			ValidationContext: &envoy_auth_v2.CertificateValidationContext{
+				TrustedCa: dataSourceFromBytes(ca),
+				MatchSubjectAltNames: []*envoy_type_matcher_v2.StringMatcher{
+					{
+						MatchPattern: &envoy_type_matcher_v2.StringMatcher_Exact{
+							Exact: hostname,
+						},
+					},
+				},
+			},
+		}
+	}
+
+	return &envoy_auth_v2.UpstreamTlsContext{
+		CommonTlsContext: &envoy_auth_v2.CommonTlsContext{
+			TlsCertificates:       tlsCertificates,
+			ValidationContextType: validationContextType,
+		},
+	}, nil
+}
+
+func dataSourceFromBytes(bytes []byte) *envoy_core_v2.DataSource {
+	return &envoy_core_v2.DataSource{
+		Specifier: &envoy_core_v2.DataSource_InlineBytes{
+			InlineBytes: bytes,
+		},
+	}
+}
+
+func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher_v2.StringMatcher {
+	return &envoy_type_matcher_v2.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v2.StringMatcher_Prefix{
+			Prefix: tls.MeshSpiffeIDPrefix(mesh),
+		},
+	}
+}
+
+func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher_v2.StringMatcher {
+	return &envoy_type_matcher_v2.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v2.StringMatcher_Exact{
+			Exact: tls.ServiceSpiffeID(mesh, service),
+		},
+	}
+}
diff --git a/pkg/xds/envoy/tls/v3/tls.go b/pkg/xds/envoy/tls/v3/tls.go
new file mode 100644
index 000000000..b3b839ece
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls.go
@@ -0,0 +1,214 @@
+package v3
+
+import (
+	envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_grpc_credential_v3 "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
+	envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
+	envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
+
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+)
+
+// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
+// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
+// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
+// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
+func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_tls_v3.DownstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.DownstreamTlsContext{
+		CommonTlsContext:         commonTlsContext,
+		RequireClientCertificate: &wrappers.BoolValue{Value: true},
+	}, nil
+}
+
+// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
+// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
+// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
+// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
+// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
+//
+// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
+func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_tls_v3.UpstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	var validationSANMatcher *envoy_type_matcher_v3.StringMatcher
+	if upstreamService == "*" {
+		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	} else {
+		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
+	}
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.UpstreamTlsContext{
+		CommonTlsContext: commonTlsContext,
+		Sni:              sni,
+	}, nil
+}
+
+func createCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher_v3.StringMatcher) (*envoy_tls_v3.CommonTlsContext, error) {
+	meshCaSecret, err := sdsSecretConfig(ctx, tls.MeshCaResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	identitySecret, err := sdsSecretConfig(ctx, tls.IdentityCertResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.CommonTlsContext{
+		ValidationContextType: &envoy_tls_v3.CommonTlsContext_CombinedValidationContext{
+			CombinedValidationContext: &envoy_tls_v3.CommonTlsContext_CombinedCertificateValidationContext{
+				DefaultValidationContext: &envoy_tls_v3.CertificateValidationContext{
+					MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{validationSANMatcher},
+				},
+				ValidationContextSdsSecretConfig: meshCaSecret,
+			},
+		},
+		TlsCertificateSdsSecretConfigs: []*envoy_tls_v3.SdsSecretConfig{
+			identitySecret,
+		},
+	}, nil
+}
+
+func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_tls_v3.SdsSecretConfig, error) {
+	withCallCredentials := func(grpc *envoy_core_v3.GrpcService_GoogleGrpc) (*envoy_core_v3.GrpcService_GoogleGrpc, error) {
+		if metadata.GetDataplaneTokenPath() == "" {
+			return grpc, nil
+		}
+
+		config := &envoy_grpc_credential_v3.FileBasedMetadataConfig{
+			SecretData: &envoy_core_v3.DataSource{
+				Specifier: &envoy_core_v3.DataSource_Filename{
+					Filename: metadata.GetDataplaneTokenPath(),
+				},
+			},
+		}
+		typedConfig, err := proto.MarshalAnyDeterministic(config)
+		if err != nil {
+			return nil, err
+		}
+
+		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials{
+			CredentialSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
+				FromPlugin: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
+					Name: "envoy.grpc_credentials.file_based_metadata",
+					ConfigType: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
+						TypedConfig: typedConfig,
+					},
+				},
+			},
+		})
+		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
+
+		return grpc, nil
+	}
+	googleGrpc, err := withCallCredentials(&envoy_core_v3.GrpcService_GoogleGrpc{
+		TargetUri:  context.SDSLocation(),
+		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
+		ChannelCredentials: &envoy_core_v3.GrpcService_GoogleGrpc_ChannelCredentials{
+			CredentialSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
+				SslCredentials: &envoy_core_v3.GrpcService_GoogleGrpc_SslCredentials{
+					RootCerts: &envoy_core_v3.DataSource{
+						Specifier: &envoy_core_v3.DataSource_InlineBytes{
+							InlineBytes: context.ControlPlane.SdsTlsCert,
+						},
+					},
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.SdsSecretConfig{
+		Name: name,
+		SdsConfig: &envoy_core_v3.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{
+				ApiConfigSource: &envoy_core_v3.ApiConfigSource{
+					ApiType: envoy_core_v3.ApiConfigSource_GRPC,
+					GrpcServices: []*envoy_core_v3.GrpcService{
+						{
+							TargetSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_{
+								GoogleGrpc: googleGrpc,
+							},
+						},
+					},
+				},
+			},
+		},
+	}, nil
+}
+
+func UpstreamTlsContextOutsideMesh(ca, cert, key []byte, hostname string) (*envoy_tls_v3.UpstreamTlsContext, error) {
+	var tlsCertificates []*envoy_tls_v3.TlsCertificate
+	if cert != nil && key != nil {
+		tlsCertificates = []*envoy_tls_v3.TlsCertificate{
+			{
+				CertificateChain: dataSourceFromBytes(cert),
+				PrivateKey:       dataSourceFromBytes(key),
+			},
+		}
+	}
+
+	var validationContextType *envoy_tls_v3.CommonTlsContext_ValidationContext
+	if ca != nil {
+		validationContextType = &envoy_tls_v3.CommonTlsContext_ValidationContext{
+			ValidationContext: &envoy_tls_v3.CertificateValidationContext{
+				TrustedCa: dataSourceFromBytes(ca),
+				MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{
+					{
+						MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
+							Exact: hostname,
+						},
+					},
+				},
+			},
+		}
+	}
+
+	return &envoy_tls_v3.UpstreamTlsContext{
+		CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
+			TlsCertificates:       tlsCertificates,
+			ValidationContextType: validationContextType,
+		},
+	}, nil
+}
+
+func dataSourceFromBytes(bytes []byte) *envoy_core_v3.DataSource {
+	return &envoy_core_v3.DataSource{
+		Specifier: &envoy_core_v3.DataSource_InlineBytes{
+			InlineBytes: bytes,
+		},
+	}
+}
+
+func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher_v3.StringMatcher {
+	return &envoy_type_matcher_v3.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v3.StringMatcher_Prefix{
+			Prefix: tls.MeshSpiffeIDPrefix(mesh),
+		},
+	}
+}
+
+func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher_v3.StringMatcher {
+	return &envoy_type_matcher_v3.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
+			Exact: tls.ServiceSpiffeID(mesh, service),
+		},
+	}
+}
diff --git a/pkg/xds/envoy/types.go b/pkg/xds/envoy/types.go
index 7cb8f6d18..69e0de011 100644
--- a/pkg/xds/envoy/types.go
+++ b/pkg/xds/envoy/types.go
@@ -5,6 +5,7 @@ import (
 	"sort"
 	"strings"
 
+	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
 	"github.com/pkg/errors"
 )
 
@@ -138,3 +139,8 @@ func (c Clusters) Get(name string) *Cluster {
 func (c Clusters) Tags(name string) []Tags {
 	return c[name].Tags()
 }
+
+type NamedResource interface {
+	envoy_types.Resource
+	GetName() string
+}
diff --git a/pkg/xds/generator/direct_access_proxy_generator.go b/pkg/xds/generator/direct_access_proxy_generator.go
index 2508eb1fa..40171dec4 100644
--- a/pkg/xds/generator/direct_access_proxy_generator.go
+++ b/pkg/xds/generator/direct_access_proxy_generator.go
@@ -63,7 +63,7 @@ func (_ DirectAccessProxyGenerator) Generate(ctx xds_context.Context, proxy *cor
 		})
 	}
 
-	directAccessCluster, err := envoy_clusters.NewClusterBuilder().
+	directAccessCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 		Configure(envoy_clusters.PassThroughCluster("direct_access")).
 		Configure(envoy_clusters.UnknownDestinationClientSideMTLS(ctx, proxy.Metadata)).
 		Build()
@@ -71,7 +71,7 @@ func (_ DirectAccessProxyGenerator) Generate(ctx xds_context.Context, proxy *cor
 		return nil, errors.Wrapf(err, "could not generate cluster: direct_access")
 	}
 	resources.Add(&core_xds.Resource{
-		Name:     directAccessCluster.Name,
+		Name:     directAccessCluster.GetName(),
 		Origin:   OriginDirectAccess,
 		Resource: directAccessCluster,
 	})
diff --git a/pkg/xds/generator/inbound_proxy_generator.go b/pkg/xds/generator/inbound_proxy_generator.go
index 518e2eab6..0cfc7cf01 100644
--- a/pkg/xds/generator/inbound_proxy_generator.go
+++ b/pkg/xds/generator/inbound_proxy_generator.go
@@ -32,7 +32,7 @@ func (g InboundProxyGenerator) Generate(ctx xds_context.Context, proxy *model.Pr
 
 		// generate CDS resource
 		localClusterName := envoy_names.GetLocalClusterName(endpoint.WorkloadPort)
-		clusterBuilder := envoy_clusters.NewClusterBuilder().
+		clusterBuilder := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.StaticCluster(localClusterName, endpoint.WorkloadIP, endpoint.WorkloadPort))
 		switch protocol {
 		case mesh_core.ProtocolHTTP2, mesh_core.ProtocolGRPC:
diff --git a/pkg/xds/generator/ingress_generator.go b/pkg/xds/generator/ingress_generator.go
index 239a448f6..2403462e6 100644
--- a/pkg/xds/generator/ingress_generator.go
+++ b/pkg/xds/generator/ingress_generator.go
@@ -13,7 +13,7 @@ import (
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
 	envoy_clusters "github.com/kumahq/kuma/pkg/xds/envoy/clusters"
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
 	envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names"
 )
@@ -126,7 +126,7 @@ func (_ IngressGenerator) services(proxy *model.Proxy) []string {
 
 func (i IngressGenerator) generateCDS(services []string, destinationsPerService map[string][]envoy_common.Tags) (resources []*model.Resource, _ error) {
 	for _, service := range services {
-		edsCluster, err := envoy_clusters.NewClusterBuilder().
+		edsCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.EdsCluster(service)).
 			Configure(envoy_clusters.LbSubset(i.lbSubsets(service, destinationsPerService))).
 			Build()
diff --git a/pkg/xds/generator/outbound_proxy_generator.go b/pkg/xds/generator/outbound_proxy_generator.go
index 8e046ece7..5b4e5f2ab 100644
--- a/pkg/xds/generator/outbound_proxy_generator.go
+++ b/pkg/xds/generator/outbound_proxy_generator.go
@@ -132,7 +132,7 @@ func (o OutboundProxyGenerator) generateCDS(ctx xds_context.Context, proxy *mode
 		tags := clusters.Tags(clusterName)
 		healthCheck := proxy.HealthChecks[serviceName]
 		circuitBreaker := proxy.CircuitBreakers[serviceName]
-		edsClusterBuilder := envoy_clusters.NewClusterBuilder().
+		edsClusterBuilder := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.LbSubset(o.lbSubsets(tags))).
 			Configure(envoy_clusters.OutlierDetection(circuitBreaker)).
 			Configure(envoy_clusters.HealthCheck(healthCheck))
diff --git a/pkg/xds/generator/prometheus_endpoint_generator.go b/pkg/xds/generator/prometheus_endpoint_generator.go
index 75e41273e..dabfbf3b3 100644
--- a/pkg/xds/generator/prometheus_endpoint_generator.go
+++ b/pkg/xds/generator/prometheus_endpoint_generator.go
@@ -11,6 +11,7 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	envoy_clusters "github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
 	envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names"
@@ -100,7 +101,7 @@ func (g PrometheusEndpointGenerator) Generate(ctx xds_context.Context, proxy *co
 	if err != nil {
 		return nil, err
 	}
-	cluster, err := envoy_clusters.NewClusterBuilder().
+	cluster, err := envoy_clusters.NewClusterBuilder(envoy.APIV2).
 		Configure(envoy_clusters.StaticCluster(envoyAdminClusterName, adminAddress, adminPort)).
 		Build()
 	if err != nil {
@@ -108,7 +109,7 @@ func (g PrometheusEndpointGenerator) Generate(ctx xds_context.Context, proxy *co
 	}
 	resources := core_xds.NewResourceSet()
 	resources.Add(&core_xds.Resource{
-		Name:     cluster.Name,
+		Name:     cluster.GetName(),
 		Origin:   OriginPrometheus,
 		Resource: cluster,
 	})
diff --git a/pkg/xds/generator/tracing_proxy_generator.go b/pkg/xds/generator/tracing_proxy_generator.go
index 960cc74e7..87be65043 100644
--- a/pkg/xds/generator/tracing_proxy_generator.go
+++ b/pkg/xds/generator/tracing_proxy_generator.go
@@ -10,6 +10,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 	"github.com/kumahq/kuma/pkg/xds/envoy/names"
 )
@@ -53,7 +54,7 @@ func (t TracingProxyGenerator) zipkinCluster(backend *mesh_proto.TracingBackend)
 	}
 
 	clusterName := names.GetTracingClusterName(backend.Name)
-	cluster, err := clusters.NewClusterBuilder().
+	cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 		Configure(clusters.DNSCluster(clusterName, url.Hostname(), uint32(port))).
 		Build()
 	if err != nil {
diff --git a/pkg/xds/generator/transparent_proxy_generator.go b/pkg/xds/generator/transparent_proxy_generator.go
index 1f8c18da4..8f0cd2194 100644
--- a/pkg/xds/generator/transparent_proxy_generator.go
+++ b/pkg/xds/generator/transparent_proxy_generator.go
@@ -31,12 +31,12 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 	sourceService := proxy.Dataplane.Spec.GetIdentifyingService()
 	meshName := ctx.Mesh.Resource.GetMeta().GetName()
 
-	var outboundPassThroughCluster *envoy_api_v2.Cluster = nil
+	var outboundPassThroughCluster envoy_common.NamedResource = nil
 	var outboundListener *envoy_api_v2.Listener = nil
 	var err error
 
 	if ctx.Mesh.Resource.Spec.IsPassthrough() {
-		outboundPassThroughCluster, err = envoy_clusters.NewClusterBuilder().
+		outboundPassThroughCluster, err = envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.PassThroughCluster(outboundName)).
 			Build()
 		if err != nil {
@@ -57,7 +57,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 
 	redirectPortInbound := proxy.Dataplane.Spec.Networking.GetTransparentProxying().GetRedirectPortInbound()
 
-	inboundPassThroughCluster, err := envoy_clusters.NewClusterBuilder().
+	inboundPassThroughCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 		Configure(envoy_clusters.PassThroughCluster(inboundName)).
 		Configure(envoy_clusters.UpstreamBindConfig("127.0.0.6", 0)).
 		Build()
@@ -83,7 +83,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 
 	if ctx.Mesh.Resource.Spec.IsPassthrough() {
 		resources.Add(&model.Resource{
-			Name:     outboundPassThroughCluster.Name,
+			Name:     outboundPassThroughCluster.GetName(),
 			Origin:   OriginTransparent,
 			Resource: outboundPassThroughCluster,
 		})
@@ -94,7 +94,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 		Resource: inboundListener,
 	})
 	resources.Add(&model.Resource{
-		Name:     inboundPassThroughCluster.Name,
+		Name:     inboundPassThroughCluster.GetName(),
 		Origin:   OriginTransparent,
 		Resource: inboundPassThroughCluster,
 	})
diff --git a/pkg/xds/topology/outbound.go b/pkg/xds/topology/outbound.go
index 4a6950bd2..4865776b2 100644
--- a/pkg/xds/topology/outbound.go
+++ b/pkg/xds/topology/outbound.go
@@ -4,8 +4,6 @@ import (
 	"context"
 	"fmt"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-
 	"github.com/kumahq/kuma/api/system/v1alpha1"
 
 	"github.com/kumahq/kuma/pkg/core"
@@ -171,7 +169,7 @@ func buildExternalServiceEndpoint(externalService *mesh_core.ExternalServiceReso
 	}, nil
 }
 
-func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Loader) *envoy_core.DataSource {
+func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Loader) []byte {
 	if ds == nil {
 		return nil
 	}
@@ -182,11 +180,7 @@ func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Load
 		return nil
 	}
 
-	return &envoy_core.DataSource{
-		Specifier: &envoy_core.DataSource_InlineBytes{
-			InlineBytes: data,
-		},
-	}
+	return data
 }
 
 func localityFromTags(mesh *mesh_core.MeshResource, priority uint32, tags map[string]string) *core_xds.Locality {


Test patch:
diff --git a/pkg/util/proto/any_test.go b/pkg/util/proto/any_test.go
index 2a2f2bbde..0c1ec94d1 100644
--- a/pkg/util/proto/any_test.go
+++ b/pkg/util/proto/any_test.go
@@ -4,7 +4,7 @@ import (
 	"github.com/golang/protobuf/proto"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
-	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -17,7 +17,7 @@ var _ = Describe("MarshalAnyDeterministic", func() {
 			"version": "v1",
 			"cloud":   "aws",
 		}
-		metadata := envoy.EndpointMetadata(tags)
+		metadata := envoy_metadata.EndpointMetadata(tags)
 		for i := 0; i < 100; i++ {
 			any1, _ := util_proto.MarshalAnyDeterministic(metadata)
 			any2, _ := util_proto.MarshalAnyDeterministic(metadata)
diff --git a/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go
new file mode 100644
index 000000000..4cffc602f
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go
@@ -0,0 +1,248 @@
+package clusters_test
+
+import (
+	"github.com/golang/protobuf/ptypes/wrappers"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("OutlierDetectionConfigurer", func() {
+
+	type testCase struct {
+		clusterName    string
+		circuitBreaker *mesh_core.CircuitBreakerResource
+		expected       string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.OutlierDetection(given.circuitBreaker)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("CircuitBreaker with TotalError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							TotalErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 100
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with GatewayError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							GatewayErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 100
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with LocalError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							LocalErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 100
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with all error detectors, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							TotalErrors:   &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 21}},
+							GatewayErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 11}},
+							LocalErrors:   &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 6}},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              consecutive5xx: 21
+              consecutiveGatewayFailure: 11
+              consecutiveLocalOriginFailure: 6
+              enforcingConsecutive5xx: 100
+              enforcingConsecutiveGatewayFailure: 100
+              enforcingConsecutiveLocalOriginFailure: 100
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with StandardDeviation detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							StandardDeviation: &mesh_proto.CircuitBreaker_Conf_Detectors_StandardDeviation{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingLocalOriginSuccessRate: 100
+              enforcingSuccessRate: 100
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with StandardDeviation detector, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							StandardDeviation: &mesh_proto.CircuitBreaker_Conf_Detectors_StandardDeviation{
+								RequestVolume: &wrappers.UInt32Value{Value: 7},
+								MinimumHosts:  &wrappers.UInt32Value{Value: 8},
+								Factor:        &wrappers.DoubleValue{Value: 1.9},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingLocalOriginSuccessRate: 100
+              enforcingSuccessRate: 100
+              successRateMinimumHosts: 8
+              successRateRequestVolume: 7
+              successRateStdevFactor: 1900
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with Failure detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							Failure: &mesh_proto.CircuitBreaker_Conf_Detectors_Failure{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentageLocalOrigin: 100
+              enforcingFailurePercentage: 100
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with Failure detector, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							Failure: &mesh_proto.CircuitBreaker_Conf_Detectors_Failure{
+								RequestVolume: &wrappers.UInt32Value{Value: 7},
+								MinimumHosts:  &wrappers.UInt32Value{Value: 8},
+								Threshold:     &wrappers.UInt32Value{Value: 85},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentageLocalOrigin: 100
+              enforcingFailurePercentage: 100
+              enforcingSuccessRate: 0
+              failurePercentageMinimumHosts: 8
+              failurePercentageRequestVolume: 7
+              failurePercentageThreshold: 85
+            type: EDS`,
+		}),
+	)
+
+})
diff --git a/pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
similarity index 99%
rename from pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
index 432b75b6c..ae3e381f7 100644
--- a/pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
@@ -29,7 +29,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.ClientSideMTLS(given.ctx, given.metadata, given.clientService, given.tags)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/client_side_tls_configurer_test.go b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
similarity index 93%
rename from pkg/xds/envoy/clusters/client_side_tls_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
index 91b76aaec..3c6315a61 100644
--- a/pkg/xds/envoy/clusters/client_side_tls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
@@ -6,6 +6,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
@@ -22,7 +23,7 @@ var _ = Describe("ClientSideTLSConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.ClientSideTLS(given.endpoints)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/clusters_suite_test.go b/pkg/xds/envoy/clusters/v2/clusters_suite_test.go
similarity index 83%
rename from pkg/xds/envoy/clusters/clusters_suite_test.go
rename to pkg/xds/envoy/clusters/v2/clusters_suite_test.go
index c1d91dd19..86ed027bc 100644
--- a/pkg/xds/envoy/clusters/clusters_suite_test.go
+++ b/pkg/xds/envoy/clusters/v2/clusters_suite_test.go
@@ -9,5 +9,5 @@ import (
 
 func TestClusters(t *testing.T) {
 	RegisterFailHandler(Fail)
-	RunSpecs(t, "Clusters Suite")
+	RunSpecs(t, "Clusters V2 Suite")
 }
diff --git a/pkg/xds/envoy/clusters/dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
similarity index 91%
rename from pkg/xds/envoy/clusters/dns_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
index 14ebcedc0..5604cbd03 100644
--- a/pkg/xds/envoy/clusters/dns_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -31,7 +32,7 @@ var _ = Describe("DNSClusterConfigurer", func() {
         type: STRICT_DNS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.DNSCluster(clusterName, address, port)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
similarity index 88%
rename from pkg/xds/envoy/clusters/eds_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
index 828251da2..0f8544c10 100644
--- a/pkg/xds/envoy/clusters/eds_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -23,7 +24,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
         type: EDS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.EdsCluster(clusterName)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
similarity index 98%
rename from pkg/xds/envoy/clusters/health_check_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
index 106a6ba5c..5e67de59a 100644
--- a/pkg/xds/envoy/clusters/health_check_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
@@ -9,6 +9,7 @@ import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 
 	. "github.com/onsi/ginkgo"
@@ -27,7 +28,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.HealthCheck(given.healthCheck)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/http2_configurer_test.go b/pkg/xds/envoy/clusters/v2/http2_configurer_test.go
similarity index 85%
rename from pkg/xds/envoy/clusters/http2_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/http2_configurer_test.go
index e9902aaa7..7d9042bae 100644
--- a/pkg/xds/envoy/clusters/http2_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/http2_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -15,7 +16,7 @@ var _ = Describe("Http2Configurer", func() {
 		expected := `http2ProtocolOptions: {}`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.Http2()).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
similarity index 94%
rename from pkg/xds/envoy/clusters/lb_subset_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
index 7a59e5e77..b47305147 100644
--- a/pkg/xds/envoy/clusters/lb_subset_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
@@ -2,6 +2,7 @@ package clusters_test
 
 import (
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 
 	. "github.com/onsi/ginkgo"
@@ -20,7 +21,7 @@ var _ = Describe("LbSubset", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.LbSubset(given.tags)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
similarity index 88%
rename from pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
index 78c7abb23..b019440c1 100644
--- a/pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -21,7 +22,7 @@ var _ = Describe("PassThroughClusterConfigurer", func() {
         type: ORIGINAL_DST`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.PassThroughCluster(clusterName)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/static_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
similarity index 91%
rename from pkg/xds/envoy/clusters/static_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
index 69772512d..a706099fb 100644
--- a/pkg/xds/envoy/clusters/static_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -31,7 +32,7 @@ var _ = Describe("StaticClusterConfigurer", func() {
         type: STATIC`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.StaticCluster(clusterName, address, port)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
similarity index 92%
rename from pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
index a4f9d9fbb..e1d80e95f 100644
--- a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
@@ -34,7 +35,7 @@ var _ = Describe("StrictDNSClusterConfigurer", func() {
         type: STRICT_DNS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.StrictDNSCluster(clusterName, []xds.Endpoint{
 				{
 					Target: address,
diff --git a/pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
similarity index 97%
rename from pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go
rename to pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
index 7448621ab..de9323588 100644
--- a/pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
@@ -2,17 +2,18 @@ package clusters_test
 
 import (
 	"github.com/golang/protobuf/ptypes/wrappers"
-	"github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
-var _ = ginkgo.Describe("OutlierDetectionConfigurer", func() {
+var _ = Describe("OutlierDetectionConfigurer", func() {
 
 	type testCase struct {
 		clusterName    string
@@ -23,7 +24,7 @@ var _ = ginkgo.Describe("OutlierDetectionConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.OutlierDetection(given.circuitBreaker)).
 				Build()
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
new file mode 100644
index 000000000..e7dc1c563
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
@@ -0,0 +1,342 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("EdsClusterConfigurer", func() {
+
+	type testCase struct {
+		clusterName   string
+		clientService string
+		tags          []envoy.Tags
+		ctx           xds_context.Context
+		metadata      *core_xds.DataplaneMetadata
+		expected      string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.ClientSideMTLS(given.ctx, given.metadata, given.clientService, given.tags)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("cluster with mTLS", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			// no tags therefore SNI is empty
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocket:
+              name: envoy.transport_sockets.tls
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+            type: EDS`,
+		}),
+		Entry("cluster with many different tag sets", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			tags: []envoy.Tags{
+				map[string]string{
+					"kuma.io/service": "backend",
+					"cluster":         "1",
+				},
+				map[string]string{
+					"kuma.io/service": "backend",
+					"cluster":         "2",
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocketMatches:
+            - match:
+                cluster: "1"
+              name: backend{cluster=1,mesh=default}
+              transportSocket:
+                name: envoy.transport_sockets.tls
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                  commonTlsContext:
+                    combinedValidationContext:
+                      defaultValidationContext:
+                        matchSubjectAltNames:
+                        - exact: spiffe://default/backend
+                      validationContextSdsSecretConfig:
+                        name: mesh_ca
+                        sdsConfig:
+                          apiConfigSource:
+                            apiType: GRPC
+                            grpcServices:
+                            - googleGrpc:
+                                channelCredentials:
+                                  sslCredentials:
+                                    rootCerts:
+                                      inlineBytes: Q0VSVElGSUNBVEU=
+                                statPrefix: sds_mesh_ca
+                                targetUri: kuma-control-plane:5677
+                    tlsCertificateSdsSecretConfigs:
+                    - name: identity_cert
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_identity_cert
+                              targetUri: kuma-control-plane:5677
+                  sni: backend{cluster=1,mesh=default}
+            - match:
+                cluster: "2"
+              name: backend{cluster=2,mesh=default}
+              transportSocket:
+                name: envoy.transport_sockets.tls
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                  commonTlsContext:
+                    combinedValidationContext:
+                      defaultValidationContext:
+                        matchSubjectAltNames:
+                        - exact: spiffe://default/backend
+                      validationContextSdsSecretConfig:
+                        name: mesh_ca
+                        sdsConfig:
+                          apiConfigSource:
+                            apiType: GRPC
+                            grpcServices:
+                            - googleGrpc:
+                                channelCredentials:
+                                  sslCredentials:
+                                    rootCerts:
+                                      inlineBytes: Q0VSVElGSUNBVEU=
+                                statPrefix: sds_mesh_ca
+                                targetUri: kuma-control-plane:5677
+                    tlsCertificateSdsSecretConfigs:
+                    - name: identity_cert
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_identity_cert
+                              targetUri: kuma-control-plane:5677
+                  sni: backend{cluster=2,mesh=default}
+            type: EDS`,
+		}),
+		Entry("cluster with mTLS and credentials", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			metadata: &core_xds.DataplaneMetadata{
+				DataplaneTokenPath: "/var/secret/token",
+			},
+			tags: []envoy.Tags{
+				{
+					"kuma.io/service": "backend",
+					"version":         "v1",
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocket:
+              name: envoy.transport_sockets.tls
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /var/secret/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /var/secret/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                sni: backend{mesh=default,version=v1}
+            type: EDS`,
+		}),
+	)
+})
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
new file mode 100644
index 000000000..0380aa1f6
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
@@ -0,0 +1,69 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("ClientSideTLSConfigurer", func() {
+
+	type testCase struct {
+		clusterName string
+		endpoints   []xds.Endpoint
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.ClientSideTLS(given.endpoints)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("cluster with mTLS", testCase{
+			clusterName: "testCluster",
+			endpoints: []xds.Endpoint{
+				{
+					Target: "httpbin.org",
+					Port:   3000,
+					Tags:   nil,
+					Weight: 100,
+					ExternalService: &xds.ExternalService{
+						TLSEnabled: true,
+					},
+				},
+			},
+
+			expected: `
+        connectTimeout: 5s
+        edsClusterConfig:
+          edsConfig:
+            ads: {}
+        name: testCluster
+        transportSocketMatches:
+        - match: {}
+          name: httpbin.org
+          transportSocket:
+            name: envoy.transport_sockets.tls
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+              commonTlsContext: {}
+        type: EDS
+`}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/clusters_suite_test.go b/pkg/xds/envoy/clusters/v3/clusters_suite_test.go
new file mode 100644
index 000000000..0b1e17e78
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/clusters_suite_test.go
@@ -0,0 +1,13 @@
+package clusters_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestClusters(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Clusters V3 Suite")
+}
diff --git a/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go
new file mode 100644
index 000000000..56af100cd
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go
@@ -0,0 +1,46 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("DNSClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "google.com"
+		port := uint32(80)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: google.com
+                    portValue: 80
+        name: test:cluster
+        type: STRICT_DNS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.DNSCluster(clusterName, address, port)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
new file mode 100644
index 000000000..20a2e7c6f
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
@@ -0,0 +1,38 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("EdsClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        edsClusterConfig:
+          edsConfig:
+            ads: {}
+        name: test:cluster
+        type: EDS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.EdsCluster(clusterName)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
new file mode 100644
index 000000000..49510cf98
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
@@ -0,0 +1,239 @@
+package clusters_test
+
+import (
+	"time"
+
+	"github.com/golang/protobuf/ptypes"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("HealthCheckConfigurer", func() {
+
+	type testCase struct {
+		clusterName string
+		healthCheck *mesh_core.HealthCheckResource
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.HealthCheck(given.healthCheck)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("HealthCheck with neither active nor passive checks", testCase{
+			clusterName: "testCluster",
+			healthCheck: mesh_core.NewHealthCheckResource(),
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            type: EDS`,
+		}),
+		Entry("HealthCheck with active checks", testCase{
+			clusterName: "testCluster",
+			healthCheck: &mesh_core.HealthCheckResource{
+				Spec: &mesh_proto.HealthCheck{
+					Sources: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "backend"}},
+					},
+					Destinations: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "redis"}},
+					},
+					Conf: &mesh_proto.HealthCheck_Conf{
+						Interval:           ptypes.DurationProto(5 * time.Second),
+						Timeout:            ptypes.DurationProto(4 * time.Second),
+						UnhealthyThreshold: 3,
+						HealthyThreshold:   2,
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            healthChecks:
+            - healthyThreshold: 2
+              interval: 5s
+              tcpHealthCheck: {}
+              timeout: 4s
+              unhealthyThreshold: 3
+            name: testCluster
+            type: EDS`,
+		}),
+		Entry("HealthCheck with provided TCP Send/Receive properties", testCase{
+			clusterName: "testCluster",
+			healthCheck: &mesh_core.HealthCheckResource{
+				Spec: &mesh_proto.HealthCheck{
+					Sources: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "backend"}},
+					},
+					Destinations: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "frontend"}},
+					},
+					Conf: &mesh_proto.HealthCheck_Conf{
+						Interval:           ptypes.DurationProto(5 * time.Second),
+						Timeout:            ptypes.DurationProto(4 * time.Second),
+						UnhealthyThreshold: 3,
+						HealthyThreshold:   2,
+						Protocol: &mesh_proto.HealthCheck_Conf_Tcp_{
+							Tcp: &mesh_proto.HealthCheck_Conf_Tcp{
+								Send: &wrappers.BytesValue{
+									Value: []byte("foo"),
+								},
+								Receive: []*wrappers.BytesValue{
+									{Value: []byte("bar")},
+									{Value: []byte("baz")},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            healthChecks:
+            - healthyThreshold: 2
+              interval: 5s
+              tcpHealthCheck:
+                receive:
+                - text: "626172"
+                - text: 62617a
+                send:
+                  text: 666f6f
+              timeout: 4s
+              unhealthyThreshold: 3
+            name: testCluster
+            type: EDS`,
+		}),
+		Entry("HealthCheck with provided TCP Send only properties", testCase{
+			clusterName: "testCluster",
+			healthCheck: &mesh_core.HealthCheckResource{
+				Spec: &mesh_proto.HealthCheck{
+					Sources: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "backend"}},
+					},
+					Destinations: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "frontend"}},
+					},
+					Conf: &mesh_proto.HealthCheck_Conf{
+						Interval:           ptypes.DurationProto(5 * time.Second),
+						Timeout:            ptypes.DurationProto(4 * time.Second),
+						UnhealthyThreshold: 3,
+						HealthyThreshold:   2,
+						Protocol: &mesh_proto.HealthCheck_Conf_Tcp_{
+							Tcp: &mesh_proto.HealthCheck_Conf_Tcp{
+								Send: &wrappers.BytesValue{
+									Value: []byte("foo"),
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            healthChecks:
+            - healthyThreshold: 2
+              interval: 5s
+              tcpHealthCheck:
+                send:
+                  text: 666f6f
+              timeout: 4s
+              unhealthyThreshold: 3
+            name: testCluster
+            type: EDS`,
+		}),
+		Entry("HealthCheck with provided HTTP configuration", testCase{
+			clusterName: "testCluster",
+			healthCheck: &mesh_core.HealthCheckResource{
+				Spec: &mesh_proto.HealthCheck{
+					Sources: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "backend"}},
+					},
+					Destinations: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "frontend"}},
+					},
+					Conf: &mesh_proto.HealthCheck_Conf{
+						Interval:           ptypes.DurationProto(5 * time.Second),
+						Timeout:            ptypes.DurationProto(4 * time.Second),
+						UnhealthyThreshold: 3,
+						HealthyThreshold:   2,
+						Protocol: &mesh_proto.HealthCheck_Conf_Http_{
+							Http: &mesh_proto.HealthCheck_Conf_Http{
+								Path: "/foo",
+								RequestHeadersToAdd: []*mesh_proto.
+									HealthCheck_Conf_Http_HeaderValueOption{
+									{
+										Header: &mesh_proto.HealthCheck_Conf_Http_HeaderValue{
+											Key:   "foobar",
+											Value: "foobaz",
+										},
+										Append: &wrappers.BoolValue{Value: false},
+									},
+								},
+								ExpectedStatuses: []*wrappers.UInt32Value{
+									{Value: 200},
+									{Value: 201},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            healthChecks:
+            - healthyThreshold: 2
+              interval: 5s
+              httpHealthCheck:
+                codecClientType: HTTP2
+                expectedStatuses:
+                - end: "201"
+                  start: "200"
+                - end: "202"
+                  start: "201"
+                path: /foo
+                requestHeadersToAdd:
+                - append: false
+                  header:
+                    key: foobar
+                    value: foobaz
+              timeout: 4s
+              unhealthyThreshold: 3
+            name: testCluster
+            type: EDS`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/http2_configurer_test.go b/pkg/xds/envoy/clusters/v3/http2_configurer_test.go
new file mode 100644
index 000000000..39532bc1d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/http2_configurer_test.go
@@ -0,0 +1,35 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("Http2Configurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		expected := `
+        typedExtensionProtocolOptions:
+          envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+            '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+            explicitHttpConfig:
+              http2ProtocolOptions: {}`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.Http2()).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
new file mode 100644
index 000000000..08aaa8b69
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
@@ -0,0 +1,72 @@
+package clusters_test
+
+import (
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("LbSubset", func() {
+
+	type testCase struct {
+		clusterName string
+		tags        [][]string
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.LbSubset(given.tags)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("LbSubset is empty if there are no tags", testCase{
+			clusterName: "backend",
+			tags:        [][]string{},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: backend
+            type: EDS`,
+		}),
+		Entry("LbSubset is set when more than service tag is set", testCase{
+			clusterName: "backend",
+			tags: [][]string{
+				{"version"},
+				{"cluster", "version"},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            lbSubsetConfig:
+              fallbackPolicy: ANY_ENDPOINT
+              subsetSelectors:
+              - fallbackPolicy: NO_FALLBACK
+                keys:
+                - version
+              - fallbackPolicy: NO_FALLBACK
+                keys:
+                - cluster
+                - version
+            name: backend
+            type: EDS`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go
new file mode 100644
index 000000000..334e3a603
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go
@@ -0,0 +1,36 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("PassThroughClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        lbPolicy: CLUSTER_PROVIDED
+        name: test:cluster
+        type: ORIGINAL_DST`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.PassThroughCluster(clusterName)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go
new file mode 100644
index 000000000..fc88b0d1c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go
@@ -0,0 +1,46 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("StaticClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "192.168.0.1"
+		port := uint32(8080)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+        name: test:cluster
+        type: STATIC`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.StaticCluster(clusterName, address, port)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go
new file mode 100644
index 000000000..fd2509c08
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go
@@ -0,0 +1,59 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("StrictDNSClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "google.com"
+		port := uint32(80)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: google.com
+                    portValue: 80
+              loadBalancingWeight: 100
+        name: test:cluster
+        type: STRICT_DNS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.StrictDNSCluster(clusterName, []xds.Endpoint{
+				{
+					Target: address,
+					Port:   port,
+					Tags:   nil,
+					Weight: 100,
+					ExternalService: &xds.ExternalService{
+						TLSEnabled: true,
+					},
+				},
+			})).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/endpoints/endpoints_suite_test.go b/pkg/xds/envoy/endpoints/v2/endpoints_suite_test.go
similarity index 100%
rename from pkg/xds/envoy/endpoints/endpoints_suite_test.go
rename to pkg/xds/envoy/endpoints/v2/endpoints_suite_test.go
diff --git a/pkg/xds/envoy/endpoints/endpoints_test.go b/pkg/xds/envoy/endpoints/v2/endpoints_test.go
similarity index 99%
rename from pkg/xds/envoy/endpoints/endpoints_test.go
rename to pkg/xds/envoy/endpoints/v2/endpoints_test.go
index 1af754d6d..c7b344f64 100644
--- a/pkg/xds/envoy/endpoints/endpoints_test.go
+++ b/pkg/xds/envoy/endpoints/v2/endpoints_test.go
@@ -5,7 +5,7 @@ import (
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
-	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go b/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go
new file mode 100644
index 000000000..7eb53a0de
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go
@@ -0,0 +1,13 @@
+package endpoints_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestEndpoints(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Endpoints Suite")
+}
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints_test.go b/pkg/xds/envoy/endpoints/v3/endpoints_test.go
new file mode 100644
index 000000000..d67fc1154
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints_test.go
@@ -0,0 +1,220 @@
+package endpoints_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+var _ = Describe("Endpoints", func() {
+
+	Describe("CreateStaticEndpoint()", func() {
+		It("should generate 'static' Endpoints", func() {
+			// given
+			expected := `
+            clusterName: localhost:8080
+            endpoints:
+            - lbEndpoints:
+              - endpoint:
+                  address:
+                    socketAddress:
+                      address: 127.0.0.1
+                      portValue: 8080
+`
+			// when
+			resource := CreateStaticEndpoint("localhost:8080", "127.0.0.1", 8080)
+
+			// then
+			actual, err := util_proto.ToYAML(resource)
+
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(expected))
+		})
+	})
+
+	Describe("ClusterLoadAssignment()", func() {
+		type testCase struct {
+			cluster   string
+			endpoints []core_xds.Endpoint
+			expected  string
+		}
+		DescribeTable("should generate ClusterLoadAssignment",
+			func(given testCase) {
+				// when
+				resource := CreateClusterLoadAssignment(given.cluster, given.endpoints)
+
+				// then
+				actual, err := util_proto.ToYAML(resource)
+
+				Expect(err).ToNot(HaveOccurred())
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("without tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Weight: 2,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Weight: 1,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    loadBalancingWeight: 2
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    loadBalancingWeight: 1
+`,
+			}),
+			Entry("stable output", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{ // endpoints in different order, but result is the same as in previous test
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    loadBalancingWeight: 2
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    loadBalancingWeight: 1
+`,
+			}),
+			Entry("with tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Tags:   map[string]string{"kuma.io/service": "backend", "region": "us"},
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Tags:   map[string]string{"kuma.io/service": "backend", "region": "eu"},
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          region: us
+                        envoy.transport_socket_match:
+                          region: us
+                    loadBalancingWeight: 1
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          region: eu
+                        envoy.transport_socket_match:
+                          region: eu
+                    loadBalancingWeight: 2
+`,
+			}),
+			Entry("with locality tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Tags:   map[string]string{"kuma.io/service": "backend", "kuma.io/region": "us", "kuma.io/zone": "west"},
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Tags:   map[string]string{"kuma.io/service": "backend", "kuma.io/region": "eu", "kuma.io/zone": "west"},
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          kuma.io/region: us
+                          kuma.io/zone: west
+                        envoy.transport_socket_match:
+                          kuma.io/region: us
+                          kuma.io/zone: west
+                    loadBalancingWeight: 1
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          kuma.io/region: eu
+                          kuma.io/zone: west
+                        envoy.transport_socket_match:
+                          kuma.io/region: eu
+                          kuma.io/zone: west
+                    loadBalancingWeight: 2
+`,
+			}),
+		)
+	})
+})
diff --git a/pkg/xds/envoy/metadata/v2/metadata_suite_test.go b/pkg/xds/envoy/metadata/v2/metadata_suite_test.go
new file mode 100644
index 000000000..c63d1da5b
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v2/metadata_suite_test.go
@@ -0,0 +1,13 @@
+package envoy_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestMetadata(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Envoy Metadata V2 Suite")
+}
diff --git a/pkg/xds/envoy/metadata/v3/metadata_suite_test.go b/pkg/xds/envoy/metadata/v3/metadata_suite_test.go
new file mode 100644
index 000000000..51f307449
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata_suite_test.go
@@ -0,0 +1,13 @@
+package envoy_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestMetadata(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Envoy Metadata V3 Suite")
+}
diff --git a/pkg/xds/envoy/metadata/v3/metadata_test.go b/pkg/xds/envoy/metadata/v3/metadata_test.go
new file mode 100644
index 000000000..2c27fce8e
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata_test.go
@@ -0,0 +1,66 @@
+package envoy
+
+import (
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Metadata()", func() {
+
+	It("should handle `nil` map of tags", func() {
+		// when
+		metadata := EndpointMetadata(nil)
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	It("should handle empty map of tags", func() {
+		// when
+		metadata := EndpointMetadata(map[string]string{})
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	It("should skip service tag", func() {
+		// when
+		metadata := EndpointMetadata(map[string]string{
+			"kuma.io/service": "backend",
+		})
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	type testCase struct {
+		tags     map[string]string
+		expected string
+	}
+	DescribeTable("should generate Envoy metadata",
+		func(given testCase) {
+			// when
+			metadata := EndpointMetadata(given.tags)
+			// and
+			actual, err := util_proto.ToYAML(metadata)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("map with multiple tags", testCase{
+			tags: map[string]string{
+				"kuma.io/service": "redis",
+				"version":         "v1",
+				"region":          "eu",
+			},
+			expected: `
+            filterMetadata:
+              envoy.lb:
+                region: eu
+                version: v1
+              envoy.transport_socket_match:
+                region: eu
+                version: v1`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/tls/tls_suite_test.go b/pkg/xds/envoy/tls/v2/tls_suite_test.go
similarity index 94%
rename from pkg/xds/envoy/tls/tls_suite_test.go
rename to pkg/xds/envoy/tls/v2/tls_suite_test.go
index e175e8392..8f93808f3 100644
--- a/pkg/xds/envoy/tls/tls_suite_test.go
+++ b/pkg/xds/envoy/tls/v2/tls_suite_test.go
@@ -1,4 +1,4 @@
-package tls_test
+package v2_test
 
 import (
 	"testing"
diff --git a/pkg/xds/envoy/tls/tls_test.go b/pkg/xds/envoy/tls/v2/tls_test.go
similarity index 97%
rename from pkg/xds/envoy/tls/tls_test.go
rename to pkg/xds/envoy/tls/v2/tls_test.go
index 9db3f38bd..101039144 100644
--- a/pkg/xds/envoy/tls/tls_test.go
+++ b/pkg/xds/envoy/tls/v2/tls_test.go
@@ -1,17 +1,16 @@
-package tls_test
+package v2_test
 
 import (
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
-
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 )
@@ -30,7 +29,7 @@ var _ = Describe("CreateDownstreamTlsContext()", func() {
 			metadata := &core_xds.DataplaneMetadata{}
 
 			// when
-			snippet, err := tls.CreateDownstreamTlsContext(ctx, metadata)
+			snippet, err := v2.CreateDownstreamTlsContext(ctx, metadata)
 			// then
 			Expect(err).ToNot(HaveOccurred())
 			// and
@@ -76,7 +75,7 @@ var _ = Describe("CreateDownstreamTlsContext()", func() {
 				}
 
 				// when
-				snippet, err := tls.CreateDownstreamTlsContext(ctx, given.metadata)
+				snippet, err := v2.CreateDownstreamTlsContext(ctx, given.metadata)
 				// then
 				Expect(err).ToNot(HaveOccurred())
 				// when
@@ -234,7 +233,7 @@ var _ = Describe("CreateUpstreamTlsContext()", func() {
 			metadata := &core_xds.DataplaneMetadata{}
 
 			// when
-			snippet, err := tls.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
+			snippet, err := v2.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
 			// then
 			Expect(err).ToNot(HaveOccurred())
 			// and
@@ -281,7 +280,7 @@ var _ = Describe("CreateUpstreamTlsContext()", func() {
 				}
 
 				// when
-				snippet, err := tls.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
+				snippet, err := v2.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
 				// then
 				Expect(err).ToNot(HaveOccurred())
 				// when
diff --git a/pkg/xds/envoy/tls/v3/tls_suite_test.go b/pkg/xds/envoy/tls/v3/tls_suite_test.go
new file mode 100644
index 000000000..9308f9091
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls_suite_test.go
@@ -0,0 +1,18 @@
+package v3_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_test "github.com/kumahq/kuma/pkg/util/test"
+)
+
+func TestTLS(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Envoy TLS Suite",
+		[]Reporter{util_test.NewlineReporter{}})
+}
diff --git a/pkg/xds/envoy/tls/v3/tls_test.go b/pkg/xds/envoy/tls/v3/tls_test.go
new file mode 100644
index 000000000..37ff3a07a
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls_test.go
@@ -0,0 +1,424 @@
+package v3_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+var _ = Describe("CreateDownstreamTlsContext()", func() {
+
+	Context("when mTLS is disabled on a given Mesh", func() {
+
+		It("should return `nil`", func() {
+			// given
+			ctx := xds_context.Context{
+				Mesh: xds_context.MeshContext{
+					Resource: mesh_core.NewMeshResource(),
+				},
+			}
+			metadata := &core_xds.DataplaneMetadata{}
+
+			// when
+			snippet, err := v3.CreateDownstreamTlsContext(ctx, metadata)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(snippet).To(BeNil())
+		})
+	})
+
+	Context("when mTLS is enabled on a given Mesh", func() {
+
+		type testCase struct {
+			metadata *core_xds.DataplaneMetadata
+			expected string
+		}
+
+		DescribeTable("should generate proper Envoy config",
+			func(given testCase) {
+				// given
+				ctx := xds_context.Context{
+					ConnectionInfo: xds_context.ConnectionInfo{
+						Authority: "kuma-control-plane:5677",
+					},
+					ControlPlane: &xds_context.ControlPlaneContext{
+						SdsTlsCert: []byte("CERTIFICATE"),
+					},
+					Mesh: xds_context.MeshContext{
+						Resource: &mesh_core.MeshResource{
+							Meta: &test_model.ResourceMeta{
+								Name: "default",
+							},
+							Spec: &mesh_proto.Mesh{
+								Mtls: &mesh_proto.Mesh_Mtls{
+									EnabledBackend: "builtin",
+									Backends: []*mesh_proto.CertificateAuthorityBackend{
+										{
+											Name: "builtin",
+											Type: "builtin",
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+
+				// when
+				snippet, err := v3.CreateDownstreamTlsContext(ctx, given.metadata)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// when
+				actual, err := util_proto.ToYAML(snippet)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// and
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("metadata is `nil`", testCase{
+				metadata: nil,
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+			Entry("dataplane without a token", testCase{
+				metadata: &core_xds.DataplaneMetadata{},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+			Entry("dataplane with a token", testCase{
+
+				metadata: &core_xds.DataplaneMetadata{
+					DataplaneTokenPath: "/path/to/token",
+				},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /path/to/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /path/to/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+		)
+	})
+})
+
+var _ = Describe("CreateUpstreamTlsContext()", func() {
+
+	Context("when mTLS is disabled on a given Mesh", func() {
+
+		It("should return `nil`", func() {
+			// given
+			ctx := xds_context.Context{
+				Mesh: xds_context.MeshContext{
+					Resource: mesh_core.NewMeshResource(),
+				},
+			}
+			metadata := &core_xds.DataplaneMetadata{}
+
+			// when
+			snippet, err := v3.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(snippet).To(BeNil())
+		})
+	})
+
+	Context("when mTLS is enabled on a given Mesh", func() {
+
+		type testCase struct {
+			metadata        *core_xds.DataplaneMetadata
+			upstreamService string
+			expected        string
+		}
+
+		DescribeTable("should generate proper Envoy config",
+			func(given testCase) {
+				// given
+				ctx := xds_context.Context{
+					ConnectionInfo: xds_context.ConnectionInfo{
+						Authority: "kuma-control-plane:5677",
+					},
+					ControlPlane: &xds_context.ControlPlaneContext{
+						SdsTlsCert: []byte("CERTIFICATE"),
+					},
+					Mesh: xds_context.MeshContext{
+						Resource: &mesh_core.MeshResource{
+							Meta: &test_model.ResourceMeta{
+								Name: "default",
+							},
+							Spec: &mesh_proto.Mesh{
+								Mtls: &mesh_proto.Mesh_Mtls{
+									EnabledBackend: "builtin",
+									Backends: []*mesh_proto.CertificateAuthorityBackend{
+										{
+											Name: "builtin",
+											Type: "builtin",
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+
+				// when
+				snippet, err := v3.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// when
+				actual, err := util_proto.ToYAML(snippet)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// and
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("metadata is `nil`", testCase{
+				metadata:        nil,
+				upstreamService: "backend",
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+			Entry("dataplane without a token", testCase{
+				metadata:        &core_xds.DataplaneMetadata{},
+				upstreamService: "backend",
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+			Entry("dataplane with a token", testCase{
+				upstreamService: "backend",
+				metadata: &core_xds.DataplaneMetadata{
+					DataplaneTokenPath: "/path/to/token",
+				},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /path/to/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /path/to/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+		)
+	})
+})
diff --git a/pkg/xds/generator/proxy_template_profile_source_test.go b/pkg/xds/generator/proxy_template_profile_source_test.go
index 5d572856b..dd32be82c 100644
--- a/pkg/xds/generator/proxy_template_profile_source_test.go
+++ b/pkg/xds/generator/proxy_template_profile_source_test.go
@@ -8,7 +8,7 @@ import (
 
 	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 
 	"github.com/golang/protobuf/ptypes"
 	. "github.com/onsi/ginkgo"