forked from kgateway-dev/kgateway
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbootstrap.go
338 lines (304 loc) · 12.7 KB
/
bootstrap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package bootstrap
import (
"context"
"errors"
envoy_config_bootstrap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_extensions_filters_network_http_connection_manager_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoycache "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/rotisserie/eris"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"github.com/kgateway-dev/kgateway/projects/gateway2/utils"
)
var (
// errNoHcm represents a situation where a filter chain does not have an HttpConnectionManager.
// Because this could occur for valid reasons, such as a TCP proxy, we use
// this as a sentinel error to inform us it's ok to ignore it and continue.
errNoHcm = eris.New("no HttpConnectionManager found")
)
func FromEnvoyResources(resources *EnvoyResources) (string, error) {
bootstrap := &envoy_config_bootstrap_v3.Bootstrap{
Node: &envoy_config_core_v3.Node{
Id: "validation-node-id",
Cluster: "validation-cluster",
},
StaticResources: &envoy_config_bootstrap_v3.Bootstrap_StaticResources{
Listeners: resources.Listeners,
Clusters: resources.Clusters,
Secrets: resources.Secrets,
},
}
marshaler := &protojson.MarshalOptions{
UseProtoNames: true,
}
j, err := marshaler.Marshal(bootstrap)
return string(j), err // returns a json, but json is valid yaml
}
// FromFilter accepts a filter name and typed config for that filter,
// contructs a static bootstrap config containing a single vhost with typed
// per-filter config matching the arguments, marshals it to json, and returns
// the stringified json or any error if it occurred.
func FromFilter(filterName string, msg proto.Message) (string, error) {
typedFilter, err := anypb.New(msg)
if err != nil {
return "", err
}
// Construct a vhost that contains our filter config as TypedPerFilterConfig.
vhosts := []*envoy_config_route_v3.VirtualHost{
{
Name: "placeholder_host",
Domains: []string{"*"},
TypedPerFilterConfig: map[string]*anypb.Any{
filterName: {
TypeUrl: typedFilter.GetTypeUrl(),
Value: typedFilter.GetValue(),
},
},
},
}
// Use our vhost with tpfc in an HttpConnectionManager to be placed in a
// FilterChain on our listener.
hcm := &envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager{
StatPrefix: "placeholder",
RouteSpecifier: &envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager_RouteConfig{
RouteConfig: &envoy_config_route_v3.RouteConfiguration{
VirtualHosts: vhosts,
},
},
}
hcmAny, err := anypb.New(hcm)
if err != nil {
return "", err
}
listener := &envoy_config_listener_v3.Listener{
Name: "placeholder_listener",
Address: &envoy_config_core_v3.Address{
Address: &envoy_config_core_v3.Address_SocketAddress{SocketAddress: &envoy_config_core_v3.SocketAddress{
Address: "0.0.0.0",
PortSpecifier: &envoy_config_core_v3.SocketAddress_PortValue{PortValue: 8081},
}},
},
FilterChains: []*envoy_config_listener_v3.FilterChain{
{
Name: "placeholder_filter_chain",
Filters: []*envoy_config_listener_v3.Filter{
{
Name: wellknown.HTTPConnectionManager,
ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{
TypedConfig: hcmAny,
},
},
},
},
},
}
return FromEnvoyResources(&EnvoyResources{Listeners: []*envoy_config_listener_v3.Listener{listener}})
}
// FromSnapshot accepts an xds Snapshot and converts it into valid bootstrap json.
func FromSnapshot(
ctx context.Context,
snap envoycache.ResourceSnapshot,
) (string, error) {
// Get the resources we're going to need as concrete types.
resources, err := resourcesFromSnapshot(snap)
if err != nil {
return "", err
}
// This map will hold the aggregate of all cluster names that are routed to
// by a FilterChain.
routedCluster := map[string]struct{}{}
// Gather up all of the clusters that we target with RouteConfigs that are associated with a FilterChain.
if err := extractRoutedClustersFromListeners(routedCluster, resources.Listeners, resources.routes); err != nil {
return "", err
}
// Next, we will look through our Snapshot's clusters and delete the ones which are
// already routed to.
convertToStaticClusters(routedCluster, resources.Clusters, resources.endpoints)
// We now need to find clusters which do not exist, even though they are targeted by
// a route. In static mode, envoy won't start without these. At this point in the
// processing, routedClusters holds this list, so we use it to create blackhole
// clusters for these routes to target. It is important to have unique clusters
// for the targets since some envoy functionality relies on such setup, like
// weighted destinations.
resources.Clusters = addBlackholeClusters(routedCluster, resources.Clusters)
return FromEnvoyResources(resources)
}
// extractRoutedClustersFromListeners accepts a hash set of strings containing the names of clusters
// to which routes point, a slice of pointers to Listener structs,
// and a slice of pointers to RouteConfiguration structs from the snapshot. It looks
// through the FilterChains on each Listener for an HttpConnectionManager, gets the
// routes on that hcm, and gets all of the clusters targeted by those routes. It then
// converts the hcm config to use static RouteConfiguration. routedCluster and elements
// of listeners are mutated in this function.
func extractRoutedClustersFromListeners(
routedCluster map[string]struct{},
listeners []*envoy_config_listener_v3.Listener,
routes []*envoy_config_route_v3.RouteConfiguration,
) error {
for _, l := range listeners {
for _, fc := range l.GetFilterChains() {
// Get the HttpConnectionManager for this FilterChain if it exists.
hcm, f, err := getHcmForFilterChain(fc)
if err != nil {
// If we just don't have an hcm on this filter chain, skip to the next one.
if errors.Is(err, errNoHcm) {
continue
}
// If we encountered any other error, fail loudly.
return err
}
// We use Route Discovery Service (RDS) in lieu of static route table config, so we
// need to get the RouteConfiguration name to lookup in our Snapshot-provided routes,
// which contain what we serve over RDS.
routeConfigName := hcm.GetRds().GetRouteConfigName()
if routeConfigName == "" {
continue
}
// Find matching route config from snapshot.
for _, r := range routes {
if r.GetName() != routeConfigName {
// These aren't the routes you're looking for.
continue
}
// Add clusters targeted by routes on this config to our aggregate list of all targeted clusters
findTargetedClusters(r, routedCluster)
// We need to add our route table as a static config to this hcm instead of
// relying on RDS, the we pack it back up and set it back on the filter chain.
if err = setStaticRouteConfig(f, hcm, r); err != nil {
return err
}
}
}
}
return nil
}
// convertToStaticClusters accepts a hash set of strings containing the names of clusters
// to which routes point, a slice of pointers to Cluster structs,
// and a slice of pointers to ClusterLoadAssignment structs from the snapshot. It
// deletes all clusters that exist from the routedCluster hash set, then converts
// the cluster's EDS config to static config using the endpoints from the snapshot.
// clusters is mutated in this function.
func convertToStaticClusters(
routedCluster map[string]struct{},
clusters []*envoy_config_cluster_v3.Cluster,
endpoints []*envoy_config_endpoint_v3.ClusterLoadAssignment,
) {
for _, c := range clusters {
delete(routedCluster, c.GetName())
// We use Endpoint Discovery Service (EDS) in lieu of static endpoint config, so we
// need to get the EDS ServiceName name to lookup in our Snapshot-provided endpoints,
// which contain what we serve over EDS.
if c.GetEdsClusterConfig() != nil {
clusterName := c.GetName()
if edsServiceName := c.GetEdsClusterConfig().GetServiceName(); edsServiceName != "" {
clusterName = edsServiceName
}
// Find endpoints matching our EDS config and convert the cluster to use
// static endpoint config matching that which would have been served over EDS.
for _, e := range endpoints {
if e.GetClusterName() == clusterName {
c.LoadAssignment = e
c.EdsClusterConfig = nil
c.ClusterDiscoveryType = &envoy_config_cluster_v3.Cluster_Type{
Type: envoy_config_cluster_v3.Cluster_STRICT_DNS,
}
}
}
}
}
}
// addBlackholeClusters accepts a hash set of strings containing the names of clusters
// to which routes point and a slice of pointers to Cluster structs from the snapshot. It
// adds an cluster to clusters for each entry in the routedCluster set. clusters is mutated
// by this function.
func addBlackholeClusters(
routedCluster map[string]struct{},
clusters []*envoy_config_cluster_v3.Cluster,
) []*envoy_config_cluster_v3.Cluster {
for c := range routedCluster {
clusters = append(clusters, &envoy_config_cluster_v3.Cluster{
Name: c,
ClusterDiscoveryType: &envoy_config_cluster_v3.Cluster_Type{
Type: envoy_config_cluster_v3.Cluster_STATIC,
},
LoadAssignment: &envoy_config_endpoint_v3.ClusterLoadAssignment{
ClusterName: c,
Endpoints: []*envoy_config_endpoint_v3.LocalityLbEndpoints{},
},
})
}
return clusters
}
// getHcmForFilterChain accepts a pointer to a FilterChain and looks for the HttpConnectionManager
// network filter if one exists. It returns a pointer to the HttpConnectionManager struct and
// a pointer to the filter that actually contained it. This function has no side effects.
func getHcmForFilterChain(fc *envoy_config_listener_v3.FilterChain) (
*envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager,
*envoy_config_listener_v3.Filter,
error,
) {
for _, f := range fc.GetFilters() {
if f.GetTypedConfig().GetTypeUrl() == "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager" {
hcmAny, err := utils.AnyToMessage(f.GetTypedConfig())
if err != nil {
return nil, nil, err
}
// This check can be unreliable if the proto *Any format can be successfully unmarshalled to this concrete type,
// which is surprisingly easy to do. This codepath is not tested as I was unable to force a failure, but we're
// leaving the check in to guard against NPE from the concrete cast.
if hcm, ok := hcmAny.(*envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager); ok {
return hcm, f, nil
} else {
return nil, nil, eris.Errorf("filter %v has hcm type url but casting to concrete failed", f.GetName())
}
}
}
return nil, nil, errNoHcm
}
// findTargetedClusters accepts a pointer to a RouteConfiguration and a hash set of strings. It
// finds all clusters and weighted clusters targeted by routes on the virtual hosts in the RouteConfiguration
// and adds their names to the routedCluster hash set. routedCluster is mutated in this function.
func findTargetedClusters(r *envoy_config_route_v3.RouteConfiguration, routedCluster map[string]struct{}) {
for _, v := range r.GetVirtualHosts() {
for _, r := range v.GetRoutes() {
if r.GetRoute() == nil {
continue
}
if c := r.GetRoute().GetCluster(); c != "" {
routedCluster[c] = struct{}{}
}
if wc := r.GetRoute().GetWeightedClusters().GetClusters(); len(wc) != 0 {
for _, c := range wc {
routedCluster[c.GetName()] = struct{}{}
}
}
}
}
}
// setStaticRouteConfig accepts pointers to each of a Filter, HttpConnectionManager, and RouteConfiguration.
// It adds the RouteConfiguration to the HttpConnectionManager as static, marshals the hcm, and sets the filter's
// TypedConfig. f and hcm are mutated in this function.
func setStaticRouteConfig(
f *envoy_config_listener_v3.Filter,
hcm *envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager,
r *envoy_config_route_v3.RouteConfiguration,
) error {
hcm.RouteSpecifier = &envoy_extensions_filters_network_http_connection_manager_v3.HttpConnectionManager_RouteConfig{
RouteConfig: r,
}
hcmAny, err := anypb.New(hcm)
if err != nil {
return err
}
f.ConfigType = &envoy_config_listener_v3.Filter_TypedConfig{
TypedConfig: hcmAny,
}
return nil
}