Repository: kumahq/kuma
Pull request #909 (2020-07-16 13:12:08)

feat(kuma-cp) virtual host modifications
### Summary

Virtual Host modification

I think VirtualHost is enough for RDS modifications.

We can skip [RouteConfiguration](https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/route.proto#envoy-api-msg-routeconfiguration) because VirtualHost offers all headers to add.
Most of the operations on Routes are possible from VirtualHost (you can remove VirtualHost and add another one with a different list of routes, most settings on routes like retries are available on VirtualHost)

### Documentation

https://github.com/kumahq/kuma-website/pull/238


lobkovilya: Probably even makes sense to skip VirtualHost as well and give an ability to modify only Routes of `CommonVirtualHost` created by Kuma. Because if you add new VirtualHost then traffic won't go through `CommonVirtualHost` and functionality will be broken.
jakubdyszkiewicz: ```
Probably even makes sense to skip VirtualHost as well and give an ability to modify only Routes of CommonVirtualHost created by Kuma. Because if you add new VirtualHost then traffic won't go through CommonVirtualHost and functionality will be broken.
```
But you can remove default virtual hosts and add new ones.
lobkovilya: Yes, but in this case, you have to manually write all stuff that Kuma does automatically here:
```go
func (c RouteConfigurer) routeAction() *envoy_route.RouteAction {
	routeAction := envoy_route.RouteAction{}
	if len(c.subsets) == 1 {
		routeAction.ClusterSpecifier = &envoy_route.RouteAction_Cluster{
			Cluster: c.subsets[0].ClusterName,
		}
		routeAction.MetadataMatch = envoy_common.LbMetadata(c.subsets[0].Tags)
	} else {
		var weightedClusters []*envoy_route.WeightedCluster_ClusterWeight
		var totalWeight uint32
		for _, subset := range c.subsets {
			weightedClusters = append(weightedClusters, &envoy_route.WeightedCluster_ClusterWeight{
				Name:          subset.ClusterName,
				Weight:        &wrappers.UInt32Value{Value: subset.Weight},
				MetadataMatch: envoy_common.LbMetadata(subset.Tags),
			})
			totalWeight += subset.Weight
		}
		routeAction.ClusterSpecifier = &envoy_route.RouteAction_WeightedClusters{
			WeightedClusters: &envoy_route.WeightedCluster{
				Clusters:    weightedClusters,
				TotalWeight: &wrappers.UInt32Value{Value: totalWeight},
			},
		}
	}
	return &routeAction
}
```
Is it correct?
jakubdyszkiewicz: Sure, but if you are going with rewriting VirtualHosts you probably want that.

And if you want to patch routes fields - like I said in the description, most of the settings are available on VirtualHost object

Patch:
diff --git a/api/mesh/v1alpha1/proxy_template.pb.go b/api/mesh/v1alpha1/proxy_template.pb.go
index 6efb7e3ec..78eb2f069 100644
--- a/api/mesh/v1alpha1/proxy_template.pb.go
+++ b/api/mesh/v1alpha1/proxy_template.pb.go
@@ -138,6 +138,7 @@ type ProxyTemplate_Modifications struct {
 	//	*ProxyTemplate_Modifications_Listener_
 	//	*ProxyTemplate_Modifications_NetworkFilter_
 	//	*ProxyTemplate_Modifications_HttpFilter_
+	//	*ProxyTemplate_Modifications_VirtualHost_
 	Type                 isProxyTemplate_Modifications_Type `protobuf_oneof:"type"`
 	XXX_NoUnkeyedLiteral struct{}                           `json:"-"`
 	XXX_unrecognized     []byte                             `json:"-"`
@@ -189,6 +190,10 @@ type ProxyTemplate_Modifications_HttpFilter_ struct {
 	HttpFilter *ProxyTemplate_Modifications_HttpFilter `protobuf:"bytes,4,opt,name=httpFilter,proto3,oneof"`
 }
 
+type ProxyTemplate_Modifications_VirtualHost_ struct {
+	VirtualHost *ProxyTemplate_Modifications_VirtualHost `protobuf:"bytes,5,opt,name=virtualHost,proto3,oneof"`
+}
+
 func (*ProxyTemplate_Modifications_Cluster_) isProxyTemplate_Modifications_Type() {}
 
 func (*ProxyTemplate_Modifications_Listener_) isProxyTemplate_Modifications_Type() {}
@@ -197,6 +202,8 @@ func (*ProxyTemplate_Modifications_NetworkFilter_) isProxyTemplate_Modifications
 
 func (*ProxyTemplate_Modifications_HttpFilter_) isProxyTemplate_Modifications_Type() {}
 
+func (*ProxyTemplate_Modifications_VirtualHost_) isProxyTemplate_Modifications_Type() {}
+
 func (m *ProxyTemplate_Modifications) GetType() isProxyTemplate_Modifications_Type {
 	if m != nil {
 		return m.Type
@@ -232,6 +239,13 @@ func (m *ProxyTemplate_Modifications) GetHttpFilter() *ProxyTemplate_Modificatio
 	return nil
 }
 
+func (m *ProxyTemplate_Modifications) GetVirtualHost() *ProxyTemplate_Modifications_VirtualHost {
+	if x, ok := m.GetType().(*ProxyTemplate_Modifications_VirtualHost_); ok {
+		return x.VirtualHost
+	}
+	return nil
+}
+
 // XXX_OneofWrappers is for the internal use of the proto package.
 func (*ProxyTemplate_Modifications) XXX_OneofWrappers() []interface{} {
 	return []interface{}{
@@ -239,6 +253,7 @@ func (*ProxyTemplate_Modifications) XXX_OneofWrappers() []interface{} {
 		(*ProxyTemplate_Modifications_Listener_)(nil),
 		(*ProxyTemplate_Modifications_NetworkFilter_)(nil),
 		(*ProxyTemplate_Modifications_HttpFilter_)(nil),
+		(*ProxyTemplate_Modifications_VirtualHost_)(nil),
 	}
 }
 
@@ -726,6 +741,131 @@ func (m *ProxyTemplate_Modifications_HttpFilter_Match) GetListenerName() string
 	return ""
 }
 
+// VirtualHost defines modification to generated virtual hosts
+type ProxyTemplate_Modifications_VirtualHost struct {
+	// Only virtual hosts that match will be modified
+	Match *ProxyTemplate_Modifications_VirtualHost_Match `protobuf:"bytes,1,opt,name=match,proto3" json:"match,omitempty"`
+	// Operation to apply on a virtual hosts (add, remove, patch)
+	Operation string `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"`
+	// xDS virtual host
+	Value                string   `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost) Reset() {
+	*m = ProxyTemplate_Modifications_VirtualHost{}
+}
+func (m *ProxyTemplate_Modifications_VirtualHost) String() string { return proto.CompactTextString(m) }
+func (*ProxyTemplate_Modifications_VirtualHost) ProtoMessage()    {}
+func (*ProxyTemplate_Modifications_VirtualHost) Descriptor() ([]byte, []int) {
+	return fileDescriptor_129e53d675ac14f4, []int{0, 1, 4}
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost.Unmarshal(m, b)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost.Marshal(b, m, deterministic)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost.Merge(m, src)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost) XXX_Size() int {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost.Size(m)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost proto.InternalMessageInfo
+
+func (m *ProxyTemplate_Modifications_VirtualHost) GetMatch() *ProxyTemplate_Modifications_VirtualHost_Match {
+	if m != nil {
+		return m.Match
+	}
+	return nil
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost) GetOperation() string {
+	if m != nil {
+		return m.Operation
+	}
+	return ""
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost) GetValue() string {
+	if m != nil {
+		return m.Value
+	}
+	return ""
+}
+
+// Match defines match for virtual host
+type ProxyTemplate_Modifications_VirtualHost_Match struct {
+	// Origin of the resource generation. (inbound, outbound, prometheus,
+	// transparent, ingress)
+	Origin string `protobuf:"bytes,1,opt,name=origin,proto3" json:"origin,omitempty"`
+	// Name of the virtual host to match
+	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	// Name of the route configuration
+	RouteConfigurationName string   `protobuf:"bytes,3,opt,name=routeConfigurationName,proto3" json:"routeConfigurationName,omitempty"`
+	XXX_NoUnkeyedLiteral   struct{} `json:"-"`
+	XXX_unrecognized       []byte   `json:"-"`
+	XXX_sizecache          int32    `json:"-"`
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) Reset() {
+	*m = ProxyTemplate_Modifications_VirtualHost_Match{}
+}
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) String() string {
+	return proto.CompactTextString(m)
+}
+func (*ProxyTemplate_Modifications_VirtualHost_Match) ProtoMessage() {}
+func (*ProxyTemplate_Modifications_VirtualHost_Match) Descriptor() ([]byte, []int) {
+	return fileDescriptor_129e53d675ac14f4, []int{0, 1, 4, 0}
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match.Unmarshal(m, b)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match.Marshal(b, m, deterministic)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match.Merge(m, src)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) XXX_Size() int {
+	return xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match.Size(m)
+}
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ProxyTemplate_Modifications_VirtualHost_Match proto.InternalMessageInfo
+
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) GetOrigin() string {
+	if m != nil {
+		return m.Origin
+	}
+	return ""
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *ProxyTemplate_Modifications_VirtualHost_Match) GetRouteConfigurationName() string {
+	if m != nil {
+		return m.RouteConfigurationName
+	}
+	return ""
+}
+
 type ProxyTemplateSource struct {
 	// Name of a configuration source.
 	// +optional
@@ -979,6 +1119,8 @@ func init() {
 	proto.RegisterType((*ProxyTemplate_Modifications_NetworkFilter_Match)(nil), "kuma.mesh.v1alpha1.ProxyTemplate.Modifications.NetworkFilter.Match")
 	proto.RegisterType((*ProxyTemplate_Modifications_HttpFilter)(nil), "kuma.mesh.v1alpha1.ProxyTemplate.Modifications.HttpFilter")
 	proto.RegisterType((*ProxyTemplate_Modifications_HttpFilter_Match)(nil), "kuma.mesh.v1alpha1.ProxyTemplate.Modifications.HttpFilter.Match")
+	proto.RegisterType((*ProxyTemplate_Modifications_VirtualHost)(nil), "kuma.mesh.v1alpha1.ProxyTemplate.Modifications.VirtualHost")
+	proto.RegisterType((*ProxyTemplate_Modifications_VirtualHost_Match)(nil), "kuma.mesh.v1alpha1.ProxyTemplate.Modifications.VirtualHost.Match")
 	proto.RegisterType((*ProxyTemplateSource)(nil), "kuma.mesh.v1alpha1.ProxyTemplateSource")
 	proto.RegisterType((*ProxyTemplateProfileSource)(nil), "kuma.mesh.v1alpha1.ProxyTemplateProfileSource")
 	proto.RegisterMapType((map[string]string)(nil), "kuma.mesh.v1alpha1.ProxyTemplateProfileSource.ParamsEntry")
@@ -991,47 +1133,51 @@ func init() {
 }
 
 var fileDescriptor_129e53d675ac14f4 = []byte{
-	// 661 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x96, 0xd1, 0x6a, 0x13, 0x4d,
-	0x14, 0xc7, 0x33, 0x49, 0x9a, 0x64, 0x4f, 0x1b, 0xf8, 0x38, 0x9f, 0x94, 0x65, 0xe9, 0x45, 0xc9,
-	0x85, 0x14, 0x91, 0x2d, 0x6d, 0x2f, 0xac, 0x81, 0x16, 0x69, 0x51, 0x42, 0xb1, 0xa5, 0x4c, 0x6a,
-	0x45, 0x11, 0x64, 0x4c, 0x27, 0x66, 0xe9, 0xee, 0xce, 0x32, 0x3b, 0x69, 0xcd, 0x43, 0xf8, 0x48,
-	0xe2, 0x85, 0x17, 0xe2, 0x43, 0xf8, 0x10, 0x5e, 0x79, 0x2b, 0x3b, 0x3b, 0x9b, 0x64, 0x30, 0xa5,
-	0x26, 0xbd, 0xd0, 0xbb, 0x9d, 0xe9, 0xf9, 0xff, 0xce, 0xf9, 0x9f, 0x39, 0x9d, 0x09, 0xb4, 0x22,
-	0x9e, 0x0e, 0x36, 0xaf, 0xb6, 0x58, 0x98, 0x0c, 0xd8, 0xd6, 0x66, 0x22, 0xc5, 0x87, 0xd1, 0x5b,
-	0xc5, 0xa3, 0x24, 0x64, 0x8a, 0xfb, 0x89, 0x14, 0x4a, 0x20, 0x5e, 0x0e, 0x23, 0xe6, 0x67, 0x81,
-	0x7e, 0x11, 0xe8, 0xad, 0xd9, 0xba, 0x94, 0x87, 0xbc, 0xa7, 0x84, 0xcc, 0x15, 0xad, 0xef, 0x2b,
-	0xd0, 0x3c, 0xcd, 0x50, 0x67, 0x86, 0x84, 0x6d, 0x70, 0x8a, 0x98, 0xd4, 0x25, 0xeb, 0x95, 0x8d,
-	0xe5, 0xed, 0x35, 0xff, 0x77, 0xae, 0xdf, 0x35, 0x41, 0x74, 0x12, 0x8e, 0x6d, 0xa8, 0xf6, 0x44,
-	0xdc, 0x77, 0xcb, 0xeb, 0x64, 0x63, 0x79, 0xfb, 0xfe, 0x2c, 0x99, 0x95, 0xcc, 0x3f, 0x14, 0x71,
-	0x9f, 0x6a, 0x8d, 0xf7, 0x85, 0x40, 0x35, 0x5b, 0xa2, 0x0b, 0xf5, 0x20, 0x4a, 0x84, 0x54, 0x79,
-	0x7a, 0x87, 0x16, 0x4b, 0x3c, 0x02, 0x47, 0xf2, 0x54, 0x0c, 0x65, 0x8f, 0xa7, 0x6e, 0x59, 0x97,
-	0xf6, 0xf0, 0xd6, 0x1c, 0x94, 0x5d, 0x53, 0x23, 0xa2, 0x13, 0x39, 0xbe, 0x80, 0x66, 0x24, 0x2e,
-	0x82, 0x7e, 0xd0, 0x63, 0x2a, 0x10, 0x71, 0xea, 0x56, 0x34, 0x6f, 0xf3, 0xf6, 0x9a, 0x8f, 0xa7,
-	0x65, 0xd4, 0xa6, 0x78, 0x1f, 0x01, 0x9a, 0x56, 0x00, 0x76, 0xa1, 0xde, 0x0b, 0x87, 0xa9, 0xe2,
-	0xd2, 0x25, 0xba, 0x2d, 0x8f, 0xe6, 0x4c, 0xe1, 0x1f, 0xe6, 0xf2, 0x4e, 0x89, 0x16, 0x24, 0x3c,
-	0x87, 0x46, 0x18, 0xa4, 0x8a, 0xc7, 0x5c, 0x9a, 0x66, 0xef, 0xce, 0x4b, 0x7d, 0x6e, 0xf4, 0x9d,
-	0x12, 0x1d, 0xb3, 0x90, 0x43, 0x33, 0xe6, 0xea, 0x5a, 0xc8, 0xcb, 0x67, 0x41, 0x98, 0x95, 0x5c,
-	0xd1, 0xf0, 0xbd, 0x79, 0xe1, 0x27, 0xd3, 0x90, 0x4e, 0x89, 0xda, 0x54, 0x7c, 0x03, 0x30, 0x50,
-	0x2a, 0x31, 0x39, 0xaa, 0x3a, 0x47, 0x7b, 0xde, 0x1c, 0x9d, 0x31, 0xa1, 0x53, 0xa2, 0x53, 0x3c,
-	0xef, 0x2b, 0x81, 0xba, 0xe9, 0x19, 0x76, 0x61, 0x29, 0x62, 0xaa, 0x37, 0x30, 0xbd, 0xdf, 0x5b,
-	0xb0, 0xf7, 0xfe, 0x71, 0x06, 0xa1, 0x39, 0x0b, 0xd7, 0xc0, 0x11, 0x09, 0x97, 0x3a, 0x44, 0xb7,
-	0xdf, 0xa1, 0x93, 0x0d, 0xbc, 0x07, 0x4b, 0x57, 0x2c, 0x1c, 0x72, 0xdd, 0x3b, 0x87, 0xe6, 0x0b,
-	0x6f, 0x07, 0x96, 0x34, 0x03, 0x57, 0xa1, 0x26, 0x64, 0xf0, 0x3e, 0x88, 0x75, 0x49, 0x0e, 0x35,
-	0x2b, 0x44, 0xa8, 0xc6, 0x2c, 0xe2, 0x86, 0xa7, 0xbf, 0xbd, 0x6f, 0x04, 0x1a, 0xc5, 0x39, 0xe1,
-	0x99, 0x6d, 0x65, 0x7f, 0xd1, 0x03, 0xff, 0x4b, 0x5e, 0x7e, 0x12, 0x68, 0x5a, 0x63, 0x81, 0xaf,
-	0x6c, 0x43, 0x87, 0x77, 0x1a, 0xb2, 0xbb, 0xbb, 0x7a, 0xb9, 0x80, 0x2b, 0x6c, 0xc1, 0x4a, 0xf1,
-	0xcf, 0x73, 0x92, 0xfd, 0x2d, 0x27, 0x5a, 0x7b, 0xde, 0x0f, 0x02, 0x30, 0x19, 0x56, 0x3c, 0xb7,
-	0x6d, 0x3f, 0x59, 0x7c, 0xee, 0xff, 0x5d, 0xcf, 0x07, 0x35, 0xa8, 0xaa, 0x51, 0xc2, 0x5b, 0x9f,
-	0x08, 0xfc, 0x6f, 0x99, 0xe9, 0xea, 0xfb, 0x77, 0xcc, 0x25, 0x53, 0xdc, 0x23, 0xa8, 0x27, 0x52,
-	0xf4, 0x83, 0x90, 0x9b, 0x3b, 0xcd, 0xbf, 0xb5, 0x35, 0xa7, 0x79, 0x7c, 0x0e, 0xcd, 0x2e, 0x48,
-	0x03, 0xc0, 0x7d, 0xa8, 0x48, 0x76, 0x6d, 0xae, 0xaf, 0x07, 0x7f, 0xf2, 0x48, 0x8c, 0x19, 0x99,
-	0x70, 0x5c, 0xff, 0x67, 0x02, 0xde, 0xcd, 0x19, 0x67, 0xda, 0xa0, 0x50, 0x4b, 0x98, 0x64, 0x51,
-	0xf1, 0x44, 0xb5, 0xe7, 0x73, 0xe1, 0x9f, 0x6a, 0xf1, 0xd3, 0x58, 0xc9, 0x11, 0x35, 0x24, 0xef,
-	0x31, 0x2c, 0x4f, 0x6d, 0xe3, 0x7f, 0x50, 0xb9, 0xe4, 0x23, 0x93, 0x35, 0xfb, 0x9c, 0x1c, 0x6f,
-	0x79, 0xea, 0x78, 0xdb, 0xe5, 0x5d, 0xd2, 0xba, 0x80, 0xd5, 0xd9, 0x56, 0xed, 0xe7, 0x94, 0xdc,
-	0xe9, 0x39, 0x6d, 0x5d, 0x80, 0x7b, 0x53, 0xd8, 0xcc, 0x26, 0xb9, 0x50, 0xbf, 0xe2, 0x32, 0x9d,
-	0x8c, 0x6a, 0xb1, 0x44, 0x0f, 0x1a, 0x05, 0xd6, 0x4c, 0xd6, 0x78, 0x7d, 0x00, 0xaf, 0x1b, 0x45,
-	0x55, 0xef, 0x6a, 0xfa, 0x07, 0xcc, 0xce, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6c, 0xd6, 0xb7,
-	0xb9, 0x18, 0x09, 0x00, 0x00,
+	// 723 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x96, 0xdf, 0x6b, 0xdb, 0x3a,
+	0x14, 0xc7, 0xe3, 0xfc, 0xf6, 0x49, 0xc3, 0xbd, 0xe8, 0x5e, 0x8a, 0x31, 0x7d, 0x28, 0x79, 0xb8,
+	0x94, 0xcb, 0x70, 0x69, 0x0b, 0x5b, 0x97, 0xd1, 0xb2, 0xb5, 0x6c, 0x98, 0xb2, 0x96, 0xa2, 0x74,
+	0x2d, 0x1b, 0x83, 0xe2, 0xa5, 0x4a, 0x63, 0x62, 0x5b, 0x46, 0x96, 0xd3, 0xe5, 0x2f, 0xd8, 0x5f,
+	0x35, 0xf6, 0xb0, 0x87, 0x6d, 0x7f, 0xce, 0x9e, 0xb6, 0xc7, 0x61, 0x59, 0x8e, 0x2d, 0x96, 0xd2,
+	0x25, 0x79, 0xd8, 0xde, 0x2c, 0xf9, 0x9c, 0xcf, 0x57, 0x5f, 0x1d, 0x89, 0x23, 0xe8, 0xf8, 0x24,
+	0x1a, 0x6e, 0x8e, 0xb7, 0x1c, 0x2f, 0x1c, 0x3a, 0x5b, 0x9b, 0x21, 0xa3, 0x6f, 0x27, 0x97, 0x9c,
+	0xf8, 0xa1, 0xe7, 0x70, 0x62, 0x85, 0x8c, 0x72, 0x8a, 0xd0, 0x28, 0xf6, 0x1d, 0x2b, 0x09, 0xb4,
+	0xb2, 0x40, 0x73, 0x4d, 0xcd, 0x8b, 0x88, 0x47, 0xfa, 0x9c, 0xb2, 0x34, 0xa3, 0xf3, 0xf9, 0x2f,
+	0x68, 0x9f, 0x26, 0xa8, 0x33, 0x49, 0x42, 0x5d, 0xd0, 0xb3, 0x98, 0xc8, 0xd0, 0xd6, 0x2b, 0x1b,
+	0xad, 0xed, 0x35, 0xeb, 0x67, 0xae, 0xd5, 0x93, 0x41, 0x38, 0x0f, 0x47, 0x5d, 0xa8, 0xf6, 0x69,
+	0x30, 0x30, 0xca, 0xeb, 0xda, 0x46, 0x6b, 0xfb, 0xbf, 0x59, 0x69, 0x8a, 0x98, 0x75, 0x48, 0x83,
+	0x01, 0x16, 0x39, 0xe6, 0x47, 0x0d, 0xaa, 0xc9, 0x10, 0x19, 0xd0, 0x70, 0xfd, 0x90, 0x32, 0x9e,
+	0xca, 0xeb, 0x38, 0x1b, 0xa2, 0x23, 0xd0, 0x19, 0x89, 0x68, 0xcc, 0xfa, 0x24, 0x32, 0xca, 0x62,
+	0x69, 0xf7, 0xee, 0xd4, 0xc0, 0xce, 0x0d, 0x96, 0x49, 0x38, 0x4f, 0x47, 0x2f, 0xa0, 0xed, 0xd3,
+	0x2b, 0x77, 0xe0, 0xf6, 0x1d, 0xee, 0xd2, 0x20, 0x32, 0x2a, 0x82, 0xb7, 0x79, 0xf7, 0x9a, 0x8f,
+	0x8b, 0x69, 0x58, 0xa5, 0x98, 0xdf, 0x57, 0xa0, 0xad, 0x04, 0xa0, 0x1e, 0x34, 0xfa, 0x5e, 0x1c,
+	0x71, 0xc2, 0x0c, 0x4d, 0x6c, 0xcb, 0x83, 0x39, 0x25, 0xac, 0xc3, 0x34, 0xdd, 0x2e, 0xe1, 0x8c,
+	0x84, 0xce, 0xa1, 0xe9, 0xb9, 0x11, 0x27, 0x01, 0x61, 0x72, 0xb3, 0x77, 0xe7, 0xa5, 0x3e, 0x97,
+	0xf9, 0x76, 0x09, 0x4f, 0x59, 0x88, 0x40, 0x3b, 0x20, 0xfc, 0x86, 0xb2, 0xd1, 0x33, 0xd7, 0x4b,
+	0x96, 0x5c, 0x11, 0xf0, 0xbd, 0x79, 0xe1, 0x27, 0x45, 0x88, 0x5d, 0xc2, 0x2a, 0x15, 0xbd, 0x06,
+	0x18, 0x72, 0x1e, 0x4a, 0x8d, 0xaa, 0xd0, 0xe8, 0xce, 0xab, 0x61, 0x4f, 0x09, 0x76, 0x09, 0x17,
+	0x78, 0xe8, 0x12, 0x5a, 0x63, 0x97, 0xf1, 0xd8, 0xf1, 0x6c, 0x1a, 0x71, 0xa3, 0x26, 0xf0, 0x8f,
+	0xe6, 0xc5, 0x9f, 0xe7, 0x08, 0xbb, 0x84, 0x8b, 0x44, 0xf3, 0x93, 0x06, 0x0d, 0x59, 0x14, 0xd4,
+	0x83, 0x9a, 0xef, 0xf0, 0xfe, 0x50, 0x16, 0x77, 0x6f, 0xc1, 0xe2, 0x5a, 0xc7, 0x09, 0x04, 0xa7,
+	0x2c, 0xb4, 0x06, 0x3a, 0x0d, 0x09, 0x13, 0x21, 0xa2, 0xbe, 0x3a, 0xce, 0x27, 0xd0, 0xbf, 0x50,
+	0x1b, 0x3b, 0x5e, 0x4c, 0x44, 0x71, 0x74, 0x9c, 0x0e, 0xcc, 0x1d, 0xa8, 0x09, 0x06, 0x5a, 0x85,
+	0x3a, 0x65, 0xee, 0xb5, 0x1b, 0x88, 0x25, 0xe9, 0x58, 0x8e, 0x10, 0x82, 0x6a, 0xe0, 0xf8, 0x44,
+	0xf2, 0xc4, 0xb7, 0xf9, 0x45, 0x83, 0x66, 0x76, 0x10, 0xd0, 0x99, 0x6a, 0x65, 0x7f, 0xd1, 0x13,
+	0xf5, 0x9b, 0xbc, 0x7c, 0xd3, 0xa0, 0xad, 0x9c, 0x3b, 0xf4, 0x52, 0x35, 0x74, 0xb8, 0xd4, 0x29,
+	0x5e, 0xde, 0xd5, 0xc5, 0x02, 0xae, 0x50, 0x07, 0x56, 0xb2, 0xdb, 0x79, 0x92, 0xfc, 0x4b, 0x89,
+	0xca, 0x9c, 0xf9, 0x55, 0x03, 0xc8, 0x6f, 0x03, 0x3a, 0x57, 0x6d, 0x3f, 0x5e, 0xfc, 0x62, 0xfd,
+	0xc1, 0x9e, 0xdf, 0x95, 0xa1, 0x55, 0xb8, 0xa2, 0xe8, 0x42, 0x35, 0xfd, 0x64, 0x89, 0xeb, 0xbe,
+	0xbc, 0xeb, 0xd1, 0x22, 0xae, 0xef, 0xc3, 0x2a, 0xa3, 0x31, 0x27, 0x49, 0x13, 0x74, 0xaf, 0xe3,
+	0x54, 0xa0, 0xe0, 0xff, 0x96, 0xbf, 0x07, 0x75, 0xa8, 0xf2, 0x49, 0x48, 0x3a, 0xef, 0x35, 0xf8,
+	0x47, 0x71, 0xd8, 0x13, 0xad, 0x6e, 0xaa, 0xa5, 0x15, 0xb4, 0x8e, 0xa0, 0x11, 0x32, 0x3a, 0x70,
+	0x3d, 0x22, 0xdb, 0x87, 0x75, 0xe7, 0x7e, 0x9d, 0xa6, 0xf1, 0x29, 0x34, 0xe9, 0x45, 0x12, 0x80,
+	0xf6, 0xa1, 0xc2, 0x9c, 0x1b, 0xd9, 0x29, 0xfe, 0xff, 0x95, 0x7e, 0x3c, 0x65, 0x24, 0x89, 0xd3,
+	0xf5, 0x7f, 0xd0, 0xc0, 0xbc, 0x5d, 0x71, 0xa6, 0x0d, 0x0c, 0xf5, 0xd0, 0x61, 0x8e, 0x9f, 0xbd,
+	0x06, 0xba, 0xf3, 0xb9, 0xb0, 0x4e, 0x45, 0xf2, 0xd3, 0x80, 0xb3, 0x09, 0x96, 0x24, 0xf3, 0x21,
+	0xb4, 0x0a, 0xd3, 0xe8, 0x6f, 0xa8, 0x8c, 0xc8, 0x44, 0xaa, 0x26, 0x9f, 0x79, 0xc9, 0xcb, 0x85,
+	0x92, 0x77, 0xcb, 0xbb, 0x5a, 0xe7, 0x0a, 0x56, 0x67, 0x5b, 0x55, 0x5f, 0x2e, 0xda, 0x52, 0x2f,
+	0x97, 0xce, 0x15, 0x18, 0xb7, 0x85, 0xcd, 0xdc, 0x24, 0x03, 0x1a, 0x63, 0xc2, 0xa2, 0xfc, 0xf8,
+	0x66, 0x43, 0x64, 0x42, 0x33, 0xc3, 0xca, 0x33, 0x36, 0x1d, 0x1f, 0xc0, 0xab, 0x66, 0xb6, 0xaa,
+	0x37, 0x75, 0xf1, 0x56, 0xdc, 0xf9, 0x11, 0x00, 0x00, 0xff, 0xff, 0x51, 0x6c, 0x97, 0xea, 0x83,
+	0x0a, 0x00, 0x00,
 }
diff --git a/api/mesh/v1alpha1/proxy_template.proto b/api/mesh/v1alpha1/proxy_template.proto
index d9a4ed531..b143a2a88 100644
--- a/api/mesh/v1alpha1/proxy_template.proto
+++ b/api/mesh/v1alpha1/proxy_template.proto
@@ -40,6 +40,8 @@ message ProxyTemplate {
       NetworkFilter networkFilter = 3;
       // HTTP Filter modification
       HttpFilter httpFilter = 4;
+      // Virtual Host modifications
+      VirtualHost virtualHost = 5;
     }
 
     // Cluster defines modifications to generated clusters
@@ -125,6 +127,27 @@ message ProxyTemplate {
         string listenerName = 3;
       }
     }
+
+    // VirtualHost defines modification to generated virtual hosts
+    message VirtualHost {
+      // Only virtual hosts that match will be modified
+      Match match = 1;
+      // Operation to apply on a virtual hosts (add, remove, patch)
+      string operation = 2;
+      // xDS virtual host
+      string value = 3;
+
+      // Match defines match for virtual host
+      message Match {
+        // Origin of the resource generation. (inbound, outbound, prometheus,
+        // transparent, ingress)
+        string origin = 1;
+        // Name of the virtual host to match
+        string name = 2;
+        // Name of the route configuration
+        string routeConfigurationName = 3;
+      }
+    }
   }
 }
 
diff --git a/pkg/core/resources/apis/mesh/proxytemplate_validator.go b/pkg/core/resources/apis/mesh/proxytemplate_validator.go
index 999ce72f7..34ea49f50 100644
--- a/pkg/core/resources/apis/mesh/proxytemplate_validator.go
+++ b/pkg/core/resources/apis/mesh/proxytemplate_validator.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	envoy_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
 	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -55,6 +56,29 @@ func validateModification(modification *mesh_proto.ProxyTemplate_Modifications)
 		verr.AddError("networkFilter", validateNetworkFilterModification(modification.GetNetworkFilter()))
 	case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
 		verr.AddError("httpFilter", validateHTTPFilterModification(modification.GetHttpFilter()))
+	case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+		verr.AddError("virtualHost", validateVirtualHostModification(modification.GetVirtualHost()))
+	}
+	return verr
+}
+
+func validateVirtualHostModification(vHostMod *mesh_proto.ProxyTemplate_Modifications_VirtualHost) validators.ValidationError {
+	verr := validators.ValidationError{}
+	switch vHostMod.Operation {
+	case mesh_proto.OpAdd:
+		if vHostMod.GetMatch().GetName() != "" {
+			verr.AddViolation("match.name", "cannot be defined")
+		}
+		if err := ValidateResourceYAML(&envoy_route.VirtualHost{}, vHostMod.Value); err != nil {
+			verr.AddViolation("value", fmt.Sprintf("native Envoy resource is not valid: %s", err.Error()))
+		}
+	case mesh_proto.OpPatch:
+		if err := ValidateResourceYAMLPatch(&envoy_route.VirtualHost{}, vHostMod.Value); err != nil {
+			verr.AddViolation("value", fmt.Sprintf("native Envoy resource is not valid: %s", err.Error()))
+		}
+	case mesh_proto.OpRemove:
+	default:
+		verr.AddViolation("operation", fmt.Sprintf("invalid operation. Available operations: %q, %q, %q", mesh_proto.OpAdd, mesh_proto.OpPatch, mesh_proto.OpRemove))
 	}
 	return verr
 }
diff --git a/pkg/xds/generator/modifications/modifications.go b/pkg/xds/generator/modifications/modifications.go
index 269b51897..c9eead727 100644
--- a/pkg/xds/generator/modifications/modifications.go
+++ b/pkg/xds/generator/modifications/modifications.go
@@ -30,8 +30,11 @@ func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTem
 		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
 			mod := httpFilterModificator(*modification.GetHttpFilter())
 			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+			mod := virtualHostModificator(*modification.GetVirtualHost())
+			modificator = &mod
 		default:
-			return errors.Errorf("invalid modification type %q", modification.Type)
+			return errors.Errorf("invalid modification type %T", modification.Type)
 		}
 		if err := modificator.apply(resources); err != nil {
 			return errors.Wrapf(err, "could not apply %d modification", i)
diff --git a/pkg/xds/generator/modifications/virtual_host.go b/pkg/xds/generator/modifications/virtual_host.go
new file mode 100644
index 000000000..4e951aea3
--- /dev/null
+++ b/pkg/xds/generator/modifications/virtual_host.go
@@ -0,0 +1,78 @@
+package modifications
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v2"
+	"github.com/golang/protobuf/proto"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type virtualHostModificator mesh_proto.ProxyTemplate_Modifications_VirtualHost
+
+func (c *virtualHostModificator) apply(resources *core_xds.ResourceSet) error {
+	virtualHost := &envoy_route.VirtualHost{}
+	if err := util_proto.FromYAML([]byte(c.Value), virtualHost); err != nil {
+		return err
+	}
+
+	for _, resource := range resources.Resources(envoy_resource.RouteType) {
+		if c.routeConfigurationMatches(resource) {
+			routeCfg := resource.Resource.(*envoy_api.RouteConfiguration)
+			switch c.Operation {
+			case mesh_proto.OpAdd:
+				c.add(routeCfg, virtualHost)
+			case mesh_proto.OpRemove:
+				c.remove(routeCfg)
+			case mesh_proto.OpPatch:
+				c.patch(routeCfg, virtualHost)
+			default:
+				return errors.Errorf("invalid operation: %s", c.Operation)
+			}
+		}
+	}
+	return nil
+}
+
+func (c *virtualHostModificator) patch(routeCfg *envoy_api.RouteConfiguration, vHostPatch *envoy_route.VirtualHost) {
+	for _, vHost := range routeCfg.VirtualHosts {
+		if c.virtualHostMatches(vHost) {
+			proto.Merge(vHost, vHostPatch)
+		}
+	}
+}
+
+func (c *virtualHostModificator) remove(routeCfg *envoy_api.RouteConfiguration) {
+	var vHosts []*envoy_route.VirtualHost
+	for _, vHost := range routeCfg.VirtualHosts {
+		if !c.virtualHostMatches(vHost) {
+			vHosts = append(vHosts, vHost)
+		}
+	}
+	routeCfg.VirtualHosts = vHosts
+}
+
+func (c *virtualHostModificator) add(routeCfg *envoy_api.RouteConfiguration, vHost *envoy_route.VirtualHost) {
+	routeCfg.VirtualHosts = append(routeCfg.VirtualHosts, vHost)
+}
+
+func (c *virtualHostModificator) virtualHostMatches(vHost *envoy_route.VirtualHost) bool {
+	if c.Match.GetName() != "" && c.Match.GetName() != vHost.Name {
+		return false
+	}
+	return true
+}
+
+func (c *virtualHostModificator) routeConfigurationMatches(routeCfg *core_xds.Resource) bool {
+	if c.Match.GetOrigin() != "" && c.Match.GetOrigin() != routeCfg.Origin {
+		return false
+	}
+	if c.Match.GetRouteConfigurationName() != "" && c.Match.GetRouteConfigurationName() != routeCfg.Name {
+		return false
+	}
+	return true
+}


Test patch:
diff --git a/pkg/core/resources/apis/mesh/proxytemplate_validator_test.go b/pkg/core/resources/apis/mesh/proxytemplate_validator_test.go
index f38e22eb0..2f4b41f89 100644
--- a/pkg/core/resources/apis/mesh/proxytemplate_validator_test.go
+++ b/pkg/core/resources/apis/mesh/proxytemplate_validator_test.go
@@ -82,7 +82,7 @@ var _ = Describe("ProxyTemplate", func() {
                   - cluster:
                       operation: patch
                       match:
-                        direction: inbound
+                        origin: inbound
                       value: |
                         connectTimeout: 5s
                   - cluster:
@@ -94,7 +94,7 @@ var _ = Describe("ProxyTemplate", func() {
                   - cluster:
                       operation: remove
                       match:
-                        direction: inbound
+                        origin: inbound
                   `,
 			),
 			Entry("listener modifications", `
@@ -128,7 +128,7 @@ var _ = Describe("ProxyTemplate", func() {
                   - listener:
                       operation: patch
                       match:
-                        direction: inbound
+                        origin: inbound
                       value: |
                         address:
                           socketAddress:
@@ -142,7 +142,7 @@ var _ = Describe("ProxyTemplate", func() {
                   - listener:
                       operation: remove
                       match:
-                        direction: inbound
+                        origin: inbound
                   `,
 			),
 			Entry("network filter modifications", `
@@ -243,6 +243,62 @@ var _ = Describe("ProxyTemplate", func() {
                           dynamicStats: false
                   - httpFilter:
                       operation: remove
+                  `,
+			),
+			Entry("virtual host modifications", `
+                selectors:
+                - match:
+                    kuma.io/service: backend
+                conf:
+                  modifications:
+                  - virtualHost:
+                      operation: add
+                      match:
+                        origin: outbound
+                        routeConfigurationName: outbound:backend
+                      value: |
+                        name: backend
+                        domains:
+                        - backend.com
+                        routes:
+                        - match:
+                            prefix: /
+                          route:
+                            cluster: backend
+                  - virtualHost:
+                      operation: patch
+                      value: |
+                        retryPolicy:
+                          retryOn: 5xx
+                          numRetries: 3
+                  - virtualHost:
+                      operation: patch
+                      match:
+                        origin: outbound
+                      value: |
+                        retryPolicy:
+                          retryOn: 5xx
+                          numRetries: 3
+                  - virtualHost:
+                      operation: patch
+                      match:
+                        routeConfigurationName: outbound:backend
+                        name: backend
+                      value: |
+                        retryPolicy:
+                          retryOn: 5xx
+                          numRetries: 3
+                  - cluster:
+                      operation: remove
+                  - cluster:
+                      operation: remove
+                      match:
+                        routeConfigurationName: outbound:backend
+                        name: backend
+                  - cluster:
+                      operation: remove
+                      match:
+                        origin: inbound
                   `,
 			),
 		)
@@ -529,6 +585,35 @@ var _ = Describe("ProxyTemplate", func() {
                 - field: conf.modifications[3].httpFilter.match.name
                   message: cannot be empty
                 - field: conf.modifications[3].httpFilter.value
+                  message: 'native Envoy resource is not valid: unexpected EOF'`,
+			}),
+			Entry("invalid virtual host operation", testCase{
+				proxyTemplate: `
+                selectors:
+                - match:
+                    kuma.io/service: backend
+                conf:
+                  modifications:
+                  - virtualHost:
+                      operation: add
+                      match:
+                        name: xyz
+                      value: '{'
+                  - virtualHost:
+                      operation: addFirst
+                  - virtualHost:
+                      operation: patch
+                      value: '{'
+`,
+				expected: `
+                violations:
+                - field: conf.modifications[0].virtualHost.match.name
+                  message: cannot be defined
+                - field: conf.modifications[0].virtualHost.value
+                  message: 'native Envoy resource is not valid: unexpected EOF'
+                - field: conf.modifications[1].virtualHost.operation
+                  message: 'invalid operation. Available operations: "add", "patch", "remove"'
+                - field: conf.modifications[2].virtualHost.value
                   message: 'native Envoy resource is not valid: unexpected EOF'`,
 			}),
 		)
diff --git a/pkg/xds/generator/modifications/virtual_host_test.go b/pkg/xds/generator/modifications/virtual_host_test.go
new file mode 100644
index 000000000..41e7ad1a2
--- /dev/null
+++ b/pkg/xds/generator/modifications/virtual_host_test.go
@@ -0,0 +1,249 @@
+package modifications_test
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Virtual Host modifications", func() {
+
+	type testCase struct {
+		routeCfgs     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, routeCfgYAML := range given.routeCfgs {
+				routeCfg := &envoy_api.RouteConfiguration{}
+				err := util_proto.FromYAML([]byte(routeCfgYAML), routeCfg)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     routeCfg.Name,
+					Origin:   generator.OriginOutbound,
+					Resource: routeCfg,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add virtual host", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:backend
+                virtualHosts:
+                - name: backend
+                  domains:
+                  - "*"
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: add
+                   value: |
+                     name: backend
+                     domains:
+                     - backend.com
+                     routes:
+                     - match:
+                         prefix: /
+                       route:
+                         cluster: backend`,
+			},
+			expected: `
+            resources:
+            - name: outbound:backend
+              resource:
+                '@type': type.googleapis.com/envoy.api.v2.RouteConfiguration
+                name: outbound:backend
+                virtualHosts:
+                - domains:
+                  - '*'
+                  name: backend
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend
+                - domains:
+                  - backend.com
+                  name: backend
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend`,
+		}),
+		Entry("should remove virtual host", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:backend
+                virtualHosts:
+                - name: backend
+                  domains:
+                  - "*"
+                  routes:
+                  - match:
+                      prefix: /backend
+                    route:
+                      cluster: backend
+                virtualHosts:
+                - name: web
+                  domains:
+                  - "*"
+                  routes:
+                  - match:
+                      prefix: /web
+                    route:
+                      cluster: web
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: remove
+                   match:
+                     name: backend`,
+			},
+			expected: `
+            resources:
+            - name: outbound:backend
+              resource:
+                '@type': type.googleapis.com/envoy.api.v2.RouteConfiguration
+                name: outbound:backend
+                virtualHosts:
+                - domains:
+                  - '*'
+                  name: web
+                  routes:
+                  - match:
+                      prefix: /web
+                    route:
+                      cluster: web`,
+		}),
+		Entry("should patch a virtual host", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:backend
+                virtualHosts:
+                - name: backend
+                  domains:
+                  - "*"
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     origin: outbound
+                   value: |
+                     retryPolicy:
+                       retryOn: 5xx
+                       numRetries: 3`,
+			},
+			expected: `
+            resources:
+            - name: outbound:backend
+              resource:
+                '@type': type.googleapis.com/envoy.api.v2.RouteConfiguration
+                name: outbound:backend
+                virtualHosts:
+                - domains:
+                  - '*'
+                  name: backend
+                  retryPolicy:
+                    numRetries: 3
+                    retryOn: 5xx
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend`,
+		}),
+		Entry("should patch a virtual host adding new route", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:backend
+                virtualHosts:
+                - name: backend
+                  domains:
+                  - "*"
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     routeConfigurationName: outbound:backend
+                   value: |
+                     routes:
+                     - match:
+                         prefix: /web
+                       route:
+                         cluster: web`,
+			},
+			expected: `
+            resources:
+            - name: outbound:backend
+              resource:
+                '@type': type.googleapis.com/envoy.api.v2.RouteConfiguration
+                name: outbound:backend
+                virtualHosts:
+                - domains:
+                  - '*'
+                  name: backend
+                  routes:
+                  - match:
+                      prefix: /
+                    route:
+                      cluster: backend
+                  - match:
+                      prefix: /web
+                    route:
+                      cluster: web`,
+		}),
+	)
+})