1 package ambex 2 3 import ( 4 // standard library 5 "context" 6 "fmt" 7 8 // third-party libraries 9 "google.golang.org/protobuf/proto" 10 "google.golang.org/protobuf/types/known/anypb" 11 12 // envoy api v3 13 v3cluster "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/cluster/v3" 14 v3core "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/core/v3" 15 v3endpoint "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/endpoint/v3" 16 v3listener "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/listener/v3" 17 v3route "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/route/v3" 18 v3httpman "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/network/http_connection_manager/v3" 19 20 // envoy control plane 21 ecp_cache_types "github.com/emissary-ingress/emissary/v3/pkg/envoy-control-plane/cache/types" 22 ecp_v3_resource "github.com/emissary-ingress/emissary/v3/pkg/envoy-control-plane/resource/v3" 23 ecp_wellknown "github.com/emissary-ingress/emissary/v3/pkg/envoy-control-plane/wellknown" 24 25 // first-party libraries 26 "github.com/datawire/dlib/dlog" 27 ) 28 29 // ListenerToRdsListener will take a listener definition and extract any inline RouteConfigurations 30 // replacing them with a reference to an RDS supplied route configuration. It does not modify the 31 // supplied listener, any configuration included in the result is copied from the input. 32 // 33 // If the input listener does not match the expected form it is simply copied, i.e. it is the 34 // identity transform for any inputs not matching the expected form. 35 // 36 // Example Input (that will get transformed in a non-identity way): 37 // - a listener configured with an http connection manager 38 // - that specifies an http router 39 // - that supplies its RouteConfiguration inline via the route_config field 40 // 41 // { 42 // "name": "...", 43 // ..., 44 // "filter_chains": [ 45 // { 46 // "filter_chain_match": {...}, 47 // "filters": [ 48 // { 49 // "name": "envoy.filters.network.http_connection_manager", 50 // "typed_config": { 51 // "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager", 52 // "http_filters": [...], 53 // "route_config": { 54 // "virtual_hosts": [ 55 // { 56 // "name": "ambassador-listener-8443-*", 57 // "domains": ["*"], 58 // "routes": [...], 59 // } 60 // ] 61 // } 62 // } 63 // } 64 // ] 65 // } 66 // ] 67 // } 68 // 69 // Example Output: 70 // - a duplicate listener that defines the "rds" field instead of the "route_config" field 71 // - and a list of route configurations 72 // - with route_config_name supplied in such a way as to correlate the two together 73 // 74 // lnr, routes, err := ListenerToRdsListener(...) 75 // 76 // lnr = { 77 // "name": "...", 78 // ..., 79 // "filter_chains": [ 80 // { 81 // "filter_chain_match": {...}, 82 // "filters": [ 83 // { 84 // "name": "envoy.filters.network.http_connection_manager", 85 // "typed_config": { 86 // "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager", 87 // "http_filters": [...], 88 // "rds": { 89 // "config_source": { 90 // "ads": {} 91 // }, 92 // "route_config_name": "ambassador-listener-8443-routeconfig-0" 93 // } 94 // } 95 // } 96 // ] 97 // } 98 // ] 99 // } 100 // 101 // routes = [ 102 // { 103 // "name": "ambassador-listener-8443-routeconfig-0", 104 // "virtual_hosts": [ 105 // { 106 // "name": "ambassador-listener-8443-*", 107 // "domains": ["*"], 108 // "routes": [...], 109 // } 110 // ] 111 // } 112 // ] 113 114 // V3ListenerToRdsListener is the v3 variety of ListnerToRdsListener 115 func V3ListenerToRdsListener(lnr *v3listener.Listener) (*v3listener.Listener, []*v3route.RouteConfiguration, error) { 116 l := proto.Clone(lnr).(*v3listener.Listener) 117 var routes []*v3route.RouteConfiguration 118 for _, fc := range l.FilterChains { 119 for _, f := range fc.Filters { 120 if f.Name != ecp_wellknown.HTTPConnectionManager { 121 // We only know how to create an rds listener for HttpConnectionManager 122 // listeners. We must ignore all other listeners. 123 continue 124 } 125 126 // Note that the hcm configuration is stored in a protobuf any, so √the 127 // GetHTTPConnectionManager is actually returning an unmarshalled copy. 128 hcm := ecp_v3_resource.GetHTTPConnectionManager(f) 129 if hcm != nil { 130 // RouteSpecifier is a protobuf oneof that corresponds to the rds, route_config, and 131 // scoped_routes fields. Only one of those may be set at a time. 132 rs, ok := hcm.RouteSpecifier.(*v3httpman.HttpConnectionManager_RouteConfig) 133 if ok { 134 rc := rs.RouteConfig 135 if rc.Name == "" { 136 // Generate a unique name for the RouteConfiguration that we can use to 137 // correlate the listener to the RDS record. We use the listener name plus 138 // an index because there can be more than one route configuration 139 // associated with a given listener. 140 rc.Name = fmt.Sprintf("%s-routeconfig-%d", l.Name, len(routes)) 141 } 142 routes = append(routes, rc) 143 // Now that we have extracted and named the RouteConfiguration, we change the 144 // RouteSpecifier from the inline RouteConfig variation to RDS via ADS. This 145 // will cause it to use whatever ADS source is defined in the bootstrap 146 // configuration. 147 hcm.RouteSpecifier = &v3httpman.HttpConnectionManager_Rds{ 148 Rds: &v3httpman.Rds{ 149 ConfigSource: &v3core.ConfigSource{ 150 ConfigSourceSpecifier: &v3core.ConfigSource_Ads{ 151 Ads: &v3core.AggregatedConfigSource{}, 152 }, 153 ResourceApiVersion: v3core.ApiVersion_V3, 154 }, 155 RouteConfigName: rc.Name, 156 }, 157 } 158 } 159 160 // Because the hcm is a protobuf any, we need to remarshal it, we can't simply 161 // expect the above modifications to take effect on our clone of the input. There is 162 // also a protobuf oneof that includes the deprecated config and typed_config 163 // fields. 164 any, err := anypb.New(hcm) 165 if err != nil { 166 return nil, nil, err 167 } 168 f.ConfigType = &v3listener.Filter_TypedConfig{TypedConfig: any} 169 } 170 } 171 } 172 173 return l, routes, nil 174 } 175 176 // JoinEdsClustersV3 will perform an outer join operation between the eds clusters in the supplied 177 // clusterlist and the eds endpoint data in the supplied map. It will return a slice of 178 // ClusterLoadAssignments (cast to []ecp_cache_types.Resource) with endpoint data for all the eds clusters in 179 // the supplied list. If there is no map entry for a given cluster, an empty ClusterLoadAssignment 180 // will be synthesized. The result is a set of endpoints that are consistent (by the 181 // go-control-plane's definition of consistent) with the input clusters. 182 func JoinEdsClustersV3(ctx context.Context, clusters []ecp_cache_types.Resource, edsEndpoints map[string]*v3endpoint.ClusterLoadAssignment, edsBypass bool) (endpoints []ecp_cache_types.Resource) { 183 for _, clu := range clusters { 184 c := clu.(*v3cluster.Cluster) 185 // Don't mess with non EDS clusters. 186 if c.EdsClusterConfig == nil { 187 continue 188 } 189 190 // By default, envoy will use the cluster name to lookup ClusterLoadAssignments unless the 191 // ServiceName is supplied in the EdsClusterConfig. 192 ref := c.EdsClusterConfig.ServiceName 193 if ref == "" { 194 ref = c.Name 195 } 196 197 // This change was introduced as a stop gap solution to mitigate the 503 issues when certificates are rotated. 198 // The issue is CDS gets updated and waits for EDS to send ClusterLoadAssignment. 199 // During this wait period calls that are coming through get hit with a 503 since the cluster is in a warming state. 200 // The solution is to "hijack" the cluster and insert all the endpoints instead of relying on EDS. 201 // Now there will be a discrepancy between envoy/envoy.json and the config envoy. 202 if edsBypass { 203 c.EdsClusterConfig = nil 204 // Type 0 is STATIC 205 c.ClusterDiscoveryType = &v3cluster.Cluster_Type{Type: 0} 206 207 if ep, ok := edsEndpoints[ref]; ok { 208 c.LoadAssignment = ep 209 } else { 210 c.LoadAssignment = &v3endpoint.ClusterLoadAssignment{ 211 ClusterName: ref, 212 Endpoints: []*v3endpoint.LocalityLbEndpoints{}, 213 } 214 } 215 } else { 216 var source string 217 ep, ok := edsEndpoints[ref] 218 if ok { 219 source = "found" 220 } else { 221 ep = &v3endpoint.ClusterLoadAssignment{ 222 ClusterName: ref, 223 Endpoints: []*v3endpoint.LocalityLbEndpoints{}, 224 } 225 source = "synthesized" 226 } 227 228 dlog.Debugf(ctx, "%s envoy v3 ClusterLoadAssignment for cluster %s: %v", source, c.Name, ep) 229 endpoints = append(endpoints, ep) 230 } 231 232 } 233 234 return 235 } 236