blob: 11dcf1209455b4eeae08c11d81268fb91a7c5f16 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 grpcgen
import (
"fmt"
"strconv"
"strings"
"github.com/apache/dubbo-kubernetes/dubbod/planet/pkg/util/protoconv"
discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
"github.com/apache/dubbo-kubernetes/dubbod/planet/pkg/model"
"github.com/apache/dubbo-kubernetes/pkg/config/host"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"google.golang.org/protobuf/types/known/wrapperspb"
networking "istio.io/api/networking/v1alpha3"
)
func (g *GrpcConfigGenerator) BuildHTTPRoutes(node *model.Proxy, push *model.PushContext, routeNames []string) model.Resources {
resp := model.Resources{}
for _, routeName := range routeNames {
if rc := buildHTTPRoute(node, push, routeName); rc != nil {
resp = append(resp, &discovery.Resource{
Name: routeName,
Resource: protoconv.MessageToAny(rc),
})
}
}
return resp
}
func buildHTTPRoute(node *model.Proxy, push *model.PushContext, routeName string) *route.RouteConfiguration {
// For proxyless gRPC inbound routes, routeName is just the port number (e.g., "17070")
// For outbound routes, routeName is cluster format (outbound|port||hostname)
_, err := strconv.Atoi(routeName)
if err != nil {
// Try to parse as cluster naming format (outbound|port||hostname)
_, _, hostname, parsedPort := model.ParseSubsetKey(routeName)
if hostname == "" || parsedPort == 0 {
log.Warnf("failed to parse route name %v", routeName)
return nil
}
// Build outbound route configuration for gRPC proxyless
// This is used by ApiListener to route traffic to the correct cluster
svc := push.ServiceForHostname(node, hostname)
if svc == nil {
log.Warnf("buildHTTPRoute: service not found for hostname %s", hostname)
return nil
}
// Build VirtualHost Domains for outbound route
// CRITICAL: Domains must match the xDS URL hostname for gRPC xDS client
hostStr := string(hostname)
domains := []string{
fmt.Sprintf("%s:%d", hostStr, parsedPort), // FQDN with port - MOST SPECIFIC
hostStr, // Full FQDN
}
// Add short name if different from FQDN
hostParts := strings.Split(hostStr, ".")
if len(hostParts) > 0 && hostParts[0] != hostStr {
shortName := hostParts[0]
domains = append(domains, fmt.Sprintf("%s:%d", shortName, parsedPort)) // Short name with port
domains = append(domains, shortName) // Short name
}
domains = append(domains, "*") // Wildcard for any domain - LEAST SPECIFIC
outboundRoutes := []*route.Route{
defaultSingleClusterRoute(routeName),
}
if vs := push.ServiceRouteForHost(host.Name(hostStr)); vs != nil {
log.Infof("buildHTTPRoute: found ServiceRoute for host %s with %d HTTP routes", hostStr, len(vs.Http))
if routes := buildRoutesFromServiceRoute(vs, host.Name(hostStr), parsedPort); len(routes) > 0 {
log.Infof("buildHTTPRoute: built %d weighted routes from ServiceRoute for host %s", len(routes), hostStr)
outboundRoutes = routes
} else {
log.Warnf("buildHTTPRoute: ServiceRoute found but no routes built for host %s", hostStr)
}
} else {
log.Debugf("buildHTTPRoute: no ServiceRoute found for host %s, using default route", hostStr)
}
return &route.RouteConfiguration{
Name: routeName,
VirtualHosts: []*route.VirtualHost{
{
Name: fmt.Sprintf("%s|http|%d", hostStr, parsedPort),
Domains: domains,
Routes: outboundRoutes,
},
},
}
}
// Build minimal route configuration for proxyless gRPC inbound listener
// NonForwardingAction indicates this is an inbound listener that should handle requests directly
return &route.RouteConfiguration{
Name: routeName,
VirtualHosts: []*route.VirtualHost{
{
Name: "inbound|http|" + routeName,
Domains: []string{"*"},
Routes: []*route.Route{
{
Match: &route.RouteMatch{
PathSpecifier: &route.RouteMatch_Prefix{
Prefix: "/",
},
},
Action: &route.Route_NonForwardingAction{},
},
},
},
},
}
}
func defaultSingleClusterRoute(clusterName string) *route.Route {
return &route.Route{
Match: &route.RouteMatch{
PathSpecifier: &route.RouteMatch_Prefix{
Prefix: "/",
},
},
Action: &route.Route_Route{
Route: &route.RouteAction{
ClusterSpecifier: &route.RouteAction_Cluster{
Cluster: clusterName,
},
},
},
}
}
func buildRoutesFromServiceRoute(vs *networking.VirtualService, hostName host.Name, defaultPort int) []*route.Route {
if vs == nil || len(vs.Http) == 0 {
return nil
}
var routes []*route.Route
for _, httpRoute := range vs.Http {
if httpRoute == nil {
continue
}
if built := buildRouteFromHTTPRoute(httpRoute, hostName, defaultPort); built != nil {
routes = append(routes, built)
}
}
return routes
}
func buildRouteFromHTTPRoute(httpRoute *networking.HTTPRoute, hostName host.Name, defaultPort int) *route.Route {
if httpRoute == nil || len(httpRoute.Route) == 0 {
log.Warnf("buildRouteFromHTTPRoute: httpRoute is nil or has no routes")
return nil
}
log.Infof("buildRouteFromHTTPRoute: processing HTTPRoute with %d route destinations", len(httpRoute.Route))
weights := make([]*route.WeightedCluster_ClusterWeight, 0, len(httpRoute.Route))
var totalWeight uint32
for i, dest := range httpRoute.Route {
if dest == nil {
log.Warnf("buildRouteFromHTTPRoute: route[%d] is nil", i)
continue
}
destination := dest.Destination
if destination == nil {
log.Warnf("buildRouteFromHTTPRoute: route[%d] has nil Destination (weight=%d), creating default destination with host=%s",
i, dest.Weight, hostName)
destination = &networking.Destination{
Host: string(hostName),
}
} else {
log.Debugf("buildRouteFromHTTPRoute: route[%d] Destination: host=%s, subset=%s, port=%v, weight=%d",
i, destination.Host, destination.Subset, destination.Port, dest.Weight)
}
targetHost := destination.Host
if targetHost == "" {
targetHost = string(hostName)
}
targetPort := defaultPort
if destination.Port != nil && destination.Port.Number != 0 {
targetPort = int(destination.Port.Number)
}
subsetName := destination.Subset
clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, subsetName, host.Name(targetHost), targetPort)
weight := dest.Weight
if weight <= 0 {
weight = 1
}
totalWeight += uint32(weight)
log.Infof("buildRouteFromHTTPRoute: route[%d] -> cluster=%s, subset=%s, weight=%d, host=%s, port=%d",
i, clusterName, subsetName, weight, targetHost, targetPort)
weights = append(weights, &route.WeightedCluster_ClusterWeight{
Name: clusterName,
Weight: wrapperspb.UInt32(uint32(weight)),
})
}
if len(weights) == 0 {
log.Warnf("buildRouteFromHTTPRoute: no valid weights generated")
return nil
}
weightedClusters := &route.WeightedCluster{
Clusters: weights,
}
if totalWeight > 0 {
weightedClusters.TotalWeight = wrapperspb.UInt32(totalWeight)
}
log.Infof("buildRouteFromHTTPRoute: built WeightedCluster with %d clusters, totalWeight=%d", len(weights), totalWeight)
return &route.Route{
Match: &route.RouteMatch{
PathSpecifier: &route.RouteMatch_Prefix{
Prefix: "/",
},
},
Action: &route.Route_Route{
Route: &route.RouteAction{
ClusterSpecifier: &route.RouteAction_WeightedClusters{
WeightedClusters: weightedClusters,
},
},
},
}
}