blob: 9fc91ad854c6dd64402c0baa643b4df0c5c29727 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package envoyfilter
import (
"strconv"
"strings"
)
import (
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"google.golang.org/protobuf/proto"
networking "istio.io/api/networking/v1alpha3"
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/util/runtime"
"github.com/apache/dubbo-go-pixiu/pkg/proto/merge"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
func ApplyRouteConfigurationPatches(
patchContext networking.EnvoyFilter_PatchContext,
proxy *model.Proxy,
efw *model.EnvoyFilterWrapper,
routeConfiguration *route.RouteConfiguration) (out *route.RouteConfiguration) {
defer runtime.HandleCrash(runtime.LogPanic, func(interface{}) {
IncrementEnvoyFilterErrorMetric(Route)
log.Errorf("route patch caused panic, so the patches did not take effect")
})
// In case the patches cause panic, use the route generated before to reduce the influence.
out = routeConfiguration
if efw == nil {
return out
}
var portMap model.GatewayPortMap
if proxy.MergedGateway != nil {
portMap = proxy.MergedGateway.PortMap
}
// only merge is applicable for route configuration.
for _, rp := range efw.Patches[networking.EnvoyFilter_ROUTE_CONFIGURATION] {
if rp.Operation != networking.EnvoyFilter_Patch_MERGE {
continue
}
if commonConditionMatch(patchContext, rp) &&
routeConfigurationMatch(patchContext, routeConfiguration, rp, portMap) {
merge.Merge(routeConfiguration, rp.Value)
IncrementEnvoyFilterMetric(rp.Key(), Route, true)
} else {
IncrementEnvoyFilterMetric(rp.Key(), Route, false)
}
}
patchVirtualHosts(patchContext, efw.Patches, routeConfiguration, portMap)
return routeConfiguration
}
func patchVirtualHosts(patchContext networking.EnvoyFilter_PatchContext,
patches map[networking.EnvoyFilter_ApplyTo][]*model.EnvoyFilterConfigPatchWrapper,
routeConfiguration *route.RouteConfiguration, portMap model.GatewayPortMap) {
removedVirtualHosts := sets.New()
// first do removes/merges/replaces
for i := range routeConfiguration.VirtualHosts {
if patchVirtualHost(patchContext, patches, routeConfiguration, routeConfiguration.VirtualHosts, i, portMap) {
removedVirtualHosts.Insert(routeConfiguration.VirtualHosts[i].Name)
}
}
// now for the adds
for _, rp := range patches[networking.EnvoyFilter_VIRTUAL_HOST] {
if rp.Operation != networking.EnvoyFilter_Patch_ADD {
continue
}
if commonConditionMatch(patchContext, rp) &&
routeConfigurationMatch(patchContext, routeConfiguration, rp, portMap) {
routeConfiguration.VirtualHosts = append(routeConfiguration.VirtualHosts, proto.Clone(rp.Value).(*route.VirtualHost))
IncrementEnvoyFilterMetric(rp.Key(), VirtualHost, true)
} else {
IncrementEnvoyFilterMetric(rp.Key(), VirtualHost, false)
}
}
if len(removedVirtualHosts) > 0 {
trimmedVirtualHosts := make([]*route.VirtualHost, 0, len(routeConfiguration.VirtualHosts))
for _, virtualHost := range routeConfiguration.VirtualHosts {
if removedVirtualHosts.Contains(virtualHost.Name) {
continue
}
trimmedVirtualHosts = append(trimmedVirtualHosts, virtualHost)
}
routeConfiguration.VirtualHosts = trimmedVirtualHosts
}
}
// patchVirtualHost patches passed in virtual host if it is MERGE operation.
// The return value indicates whether the virtual host has been removed for REMOVE operations.
func patchVirtualHost(patchContext networking.EnvoyFilter_PatchContext,
patches map[networking.EnvoyFilter_ApplyTo][]*model.EnvoyFilterConfigPatchWrapper,
routeConfiguration *route.RouteConfiguration, virtualHosts []*route.VirtualHost,
idx int, portMap model.GatewayPortMap) bool {
for _, rp := range patches[networking.EnvoyFilter_VIRTUAL_HOST] {
applied := false
if commonConditionMatch(patchContext, rp) &&
routeConfigurationMatch(patchContext, routeConfiguration, rp, portMap) &&
virtualHostMatch(virtualHosts[idx], rp) {
applied = true
if rp.Operation == networking.EnvoyFilter_Patch_REMOVE {
return true
} else if rp.Operation == networking.EnvoyFilter_Patch_MERGE {
merge.Merge(virtualHosts[idx], rp.Value)
} else if rp.Operation == networking.EnvoyFilter_Patch_REPLACE {
virtualHosts[idx] = proto.Clone(rp.Value).(*route.VirtualHost)
}
}
IncrementEnvoyFilterMetric(rp.Key(), VirtualHost, applied)
}
patchHTTPRoutes(patchContext, patches, routeConfiguration, virtualHosts[idx], portMap)
return false
}
func hasRouteMatch(rp *model.EnvoyFilterConfigPatchWrapper) bool {
cMatch := rp.Match.GetRouteConfiguration()
if cMatch == nil {
return false
}
vhMatch := cMatch.Vhost
if vhMatch == nil {
return false
}
return vhMatch.Route != nil
}
func patchHTTPRoutes(patchContext networking.EnvoyFilter_PatchContext,
patches map[networking.EnvoyFilter_ApplyTo][]*model.EnvoyFilterConfigPatchWrapper,
routeConfiguration *route.RouteConfiguration, virtualHost *route.VirtualHost, portMap model.GatewayPortMap) {
routesRemoved := false
// Apply the route level removes/merges if any.
for index := range virtualHost.Routes {
patchHTTPRoute(patchContext, patches, routeConfiguration, virtualHost, index, &routesRemoved, portMap)
}
// now for the adds
for _, rp := range patches[networking.EnvoyFilter_HTTP_ROUTE] {
applied := false
if !commonConditionMatch(patchContext, rp) ||
!routeConfigurationMatch(patchContext, routeConfiguration, rp, portMap) ||
!virtualHostMatch(virtualHost, rp) {
IncrementEnvoyFilterMetric(rp.Key(), Route, applied)
continue
}
if rp.Operation == networking.EnvoyFilter_Patch_ADD {
virtualHost.Routes = append(virtualHost.Routes, proto.Clone(rp.Value).(*route.Route))
applied = true
} else if rp.Operation == networking.EnvoyFilter_Patch_INSERT_AFTER {
// Insert after without a route match is same as ADD in the end
if !hasRouteMatch(rp) {
virtualHost.Routes = append(virtualHost.Routes, proto.Clone(rp.Value).(*route.Route))
continue
}
// find the matching route first
insertPosition := -1
for i := 0; i < len(virtualHost.Routes); i++ {
if routeMatch(virtualHost.Routes[i], rp) {
insertPosition = i + 1
break
}
}
if insertPosition == -1 {
continue
}
applied = true
clonedVal := proto.Clone(rp.Value).(*route.Route)
virtualHost.Routes = append(virtualHost.Routes, clonedVal)
if insertPosition < len(virtualHost.Routes)-1 {
copy(virtualHost.Routes[insertPosition+1:], virtualHost.Routes[insertPosition:])
virtualHost.Routes[insertPosition] = clonedVal
}
} else if rp.Operation == networking.EnvoyFilter_Patch_INSERT_BEFORE || rp.Operation == networking.EnvoyFilter_Patch_INSERT_FIRST {
// insert before/first without a route match is same as insert in the beginning
if !hasRouteMatch(rp) {
virtualHost.Routes = append([]*route.Route{proto.Clone(rp.Value).(*route.Route)}, virtualHost.Routes...)
continue
}
// find the matching route first
insertPosition := -1
for i := 0; i < len(virtualHost.Routes); i++ {
if routeMatch(virtualHost.Routes[i], rp) {
insertPosition = i
break
}
}
// If matching route is not found, then don't insert and continue.
if insertPosition == -1 {
continue
}
applied = true
// In case of INSERT_FIRST, if a match is found, still insert it at the top of the routes.
if rp.Operation == networking.EnvoyFilter_Patch_INSERT_FIRST {
insertPosition = 0
}
clonedVal := proto.Clone(rp.Value).(*route.Route)
virtualHost.Routes = append(virtualHost.Routes, clonedVal)
copy(virtualHost.Routes[insertPosition+1:], virtualHost.Routes[insertPosition:])
virtualHost.Routes[insertPosition] = clonedVal
}
IncrementEnvoyFilterMetric(rp.Key(), Route, applied)
}
if routesRemoved {
trimmedRoutes := make([]*route.Route, 0, len(virtualHost.Routes))
for i := range virtualHost.Routes {
if virtualHost.Routes[i] == nil {
continue
}
trimmedRoutes = append(trimmedRoutes, virtualHost.Routes[i])
}
virtualHost.Routes = trimmedRoutes
}
}
func patchHTTPRoute(patchContext networking.EnvoyFilter_PatchContext,
patches map[networking.EnvoyFilter_ApplyTo][]*model.EnvoyFilterConfigPatchWrapper,
routeConfiguration *route.RouteConfiguration, virtualHost *route.VirtualHost, routeIndex int, routesRemoved *bool, portMap model.GatewayPortMap) {
for _, rp := range patches[networking.EnvoyFilter_HTTP_ROUTE] {
applied := false
if commonConditionMatch(patchContext, rp) &&
routeConfigurationMatch(patchContext, routeConfiguration, rp, portMap) &&
virtualHostMatch(virtualHost, rp) &&
routeMatch(virtualHost.Routes[routeIndex], rp) {
// different virtualHosts may share same routes pointer
virtualHost.Routes = cloneVhostRoutes(virtualHost.Routes)
if rp.Operation == networking.EnvoyFilter_Patch_REMOVE {
virtualHost.Routes[routeIndex] = nil
*routesRemoved = true
return
} else if rp.Operation == networking.EnvoyFilter_Patch_MERGE {
merge.Merge(virtualHost.Routes[routeIndex], rp.Value)
}
applied = true
}
IncrementEnvoyFilterMetric(rp.Key(), Route, applied)
}
}
func routeConfigurationMatch(patchContext networking.EnvoyFilter_PatchContext, rc *route.RouteConfiguration,
rp *model.EnvoyFilterConfigPatchWrapper, portMap model.GatewayPortMap) bool {
rMatch := rp.Match.GetRouteConfiguration()
if rMatch == nil {
return true
}
// we match on the port number and virtual host for sidecars
// we match on port number, server port name, gateway name, plus virtual host for gateways
if patchContext != networking.EnvoyFilter_GATEWAY {
listenerPort := 0
if strings.HasPrefix(rc.Name, string(model.TrafficDirectionInbound)) {
_, _, _, listenerPort = model.ParseSubsetKey(rc.Name)
} else {
listenerPort, _ = strconv.Atoi(rc.Name)
}
// FIXME: Ports on a route can be 0. the API only takes uint32 for ports
// We should either make that field in API as a wrapper type or switch to int
if rMatch.PortNumber != 0 && int(rMatch.PortNumber) != listenerPort {
return false
}
if rMatch.Name != "" && rMatch.Name != rc.Name {
return false
}
return true
}
// This is a gateway. Get all the fields in the gateway's RDS route name
routePortNumber, portName, gateway := model.ParseGatewayRDSRouteName(rc.Name)
if rMatch.PortNumber != 0 && !anyPortMatches(portMap, routePortNumber, int(rMatch.PortNumber)) {
return false
}
if rMatch.PortName != "" && rMatch.PortName != portName {
return false
}
if rMatch.Gateway != "" && rMatch.Gateway != gateway {
return false
}
if rMatch.Name != "" && rMatch.Name != rc.Name {
return false
}
return true
}
func anyPortMatches(m model.GatewayPortMap, number int, matchNumber int) bool {
if servicePorts, f := m[number]; f {
// We do have service ports mapping to this, see if we match those
for s := range servicePorts {
if s == matchNumber {
return true
}
}
return false
}
// Otherwise, check the port directly
return number == matchNumber
}
func virtualHostMatch(vh *route.VirtualHost, rp *model.EnvoyFilterConfigPatchWrapper) bool {
rMatch := rp.Match.GetRouteConfiguration()
if rMatch == nil {
return true
}
match := rMatch.Vhost
if match == nil {
// match any virtual host in the named route configuration
return true
}
if vh == nil {
// route configuration has a specific match for a virtual host but
// we do not have a virtual host to match.
return false
}
// check if virtual host names match
return match.Name == "" || match.Name == vh.Name
}
func routeMatch(httpRoute *route.Route, rp *model.EnvoyFilterConfigPatchWrapper) bool {
rMatch := rp.Match.GetRouteConfiguration()
if rMatch == nil {
return true
}
vMatch := rMatch.Vhost
if vMatch == nil {
// match any virtual host in the named httpRoute configuration
return true
}
match := vMatch.Route
if match == nil {
// match any httpRoute in the virtual host
return true
}
if httpRoute == nil {
// we have a specific match for particular httpRoute but
// we do not have a httpRoute to match.
return false
}
// check if httpRoute names match
if match.Name != "" && match.Name != httpRoute.Name {
return false
}
if match.Action != networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch_ANY {
switch httpRoute.Action.(type) {
case *route.Route_Route:
return match.Action == networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch_ROUTE
case *route.Route_Redirect:
return match.Action == networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch_REDIRECT
case *route.Route_DirectResponse:
return match.Action == networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch_DIRECT_RESPONSE
}
}
return true
}
func cloneVhostRoutes(routes []*route.Route) []*route.Route {
out := make([]*route.Route, len(routes))
for i := 0; i < len(routes); i++ {
clone := proto.Clone(routes[i]).(*route.Route)
out[i] = clone
}
return out
}