-
Notifications
You must be signed in to change notification settings - Fork 4
/
plugin.go
416 lines (360 loc) · 13.4 KB
/
plugin.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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
package plugin
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"slices"
"strings"
"github.com/argoproj-labs/rollouts-plugin-trafficrouter-glooplatform/pkg/gloo"
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
pluginTypes "github.com/argoproj/argo-rollouts/utils/plugin/types"
"github.com/sirupsen/logrus"
solov2 "github.com/solo-io/solo-apis/client-go/common.gloo.solo.io/v2"
networkv2 "github.com/solo-io/solo-apis/client-go/networking.gloo.solo.io/v2"
"k8s.io/apimachinery/pkg/labels"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const (
Type = "GlooPlatformAPI"
GlooPlatformAPIUpdateError = "GlooPlatformAPIUpdateError"
PluginName = "solo-io/glooplatform"
)
type RpcPlugin struct {
IsTest bool // TODO: remove in favor of improved test clients and/or refactoring to allow enhanced test coverage which does not require test clients
// temporary hack until mock clienset is fixed (missing some interface methods)
// TestRouteTable *networkv2.RouteTable
LogCtx *logrus.Entry
Client gloo.NetworkV2ClientSet
}
type GlooPlatformAPITrafficRouting struct {
RouteTableSelector *SimpleObjectSelector `json:"routeTableSelector" protobuf:"bytes,1,name=routeTableSelector"`
RouteSelector *SimpleRouteSelector `json:"routeSelector" protobuf:"bytes,2,name=routeSelector"`
}
type SimpleObjectSelector struct {
Labels map[string]string `json:"labels" protobuf:"bytes,1,name=labels"`
Name string `json:"name" protobuf:"bytes,2,name=name"`
Namespace string `json:"namespace" protobuf:"bytes,3,name=namespace"`
}
type SimpleRouteSelector struct {
Labels map[string]string `json:"labels" protobuf:"bytes,1,name=labels"`
Name string `json:"name" protobuf:"bytes,2,name=name"`
}
type GlooDestinationMatcher struct {
// Regexp *GlooDestinationMatcherRegexp `json:"regexp" protobuf:"bytes,1,name=regexp"`
Ref *solov2.ObjectReference `json:"ref" protobuf:"bytes,2,name=ref"`
}
type GlooMatchedRouteTable struct {
// matched gloo platform route table
RouteTable *networkv2.RouteTable
// matched http routes within the routetable
HttpRoutes []*GlooMatchedHttpRoutes
// matched tcp routes within the routetable
TCPRoutes []*GlooMatchedTCPRoutes
// matched tls routes within the routetable
TLSRoutes []*GlooMatchedTLSRoutes
}
type GlooDestinations struct {
StableOrActiveDestination *solov2.DestinationReference
CanaryOrPreviewDestination *solov2.DestinationReference
}
type GlooMatchedHttpRoutes struct {
// matched HttpRoute
HttpRoute *networkv2.HTTPRoute
// matched destinations within the httpRoute
Destinations *GlooDestinations
}
type GlooMatchedTLSRoutes struct {
// matched HttpRoute
TLSRoute *networkv2.TLSRoute
// matched destinations within the httpRoute
Destinations []*GlooDestinations
}
type GlooMatchedTCPRoutes struct {
// matched HttpRoute
TCPRoute *networkv2.TCPRoute
// matched destinations within the httpRoute
Destinations []*GlooDestinations
}
func (r *RpcPlugin) InitPlugin() pluginTypes.RpcError {
if r.IsTest {
return pluginTypes.RpcError{}
}
client, err := gloo.NewNetworkV2ClientSet()
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
r.Client = client
return pluginTypes.RpcError{}
}
func (r *RpcPlugin) UpdateHash(rollout *v1alpha1.Rollout, canaryHash, stableHash string, additionalDestinations []v1alpha1.WeightDestination) pluginTypes.RpcError {
return pluginTypes.RpcError{}
}
func (r *RpcPlugin) SetWeight(rollout *v1alpha1.Rollout, desiredWeight int32, additionalDestinations []v1alpha1.WeightDestination) pluginTypes.RpcError {
ctx := context.TODO()
glooPluginConfig, err := getPluginConfig(rollout)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
// get the matched routetables
matchedRts, err := r.getRouteTables(ctx, rollout, glooPluginConfig)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
if rollout.Spec.Strategy.Canary != nil {
return r.handleCanary(ctx, rollout, desiredWeight, additionalDestinations, glooPluginConfig, matchedRts)
} else if rollout.Spec.Strategy.BlueGreen != nil {
return r.handleBlueGreen(rollout)
}
return pluginTypes.RpcError{}
}
func (r *RpcPlugin) SetHeaderRoute(rollout *v1alpha1.Rollout, headerRouting *v1alpha1.SetHeaderRoute) pluginTypes.RpcError {
r.LogCtx.Debugln("SetHeaderRoute")
ctx := context.TODO()
glooPluginConfig, err := getPluginConfig(rollout)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
// get the matched routetables
matchedRts, err := r.getRouteTables(ctx, rollout, glooPluginConfig)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
if len(matchedRts) == 0 {
// nothing to update, don't bother computing things
return pluginTypes.RpcError{
ErrorString: "unable to find qualifying RouteTables", // TODO: include the selection criteria which failed (may require update to getRouteTables to do nicely)
}
}
return r.handleHeaderRoute(ctx, matchedRts, buildGlooMatches(headerRouting), headerRouting.Name, rollout.Spec.Strategy.Canary.CanaryService)
}
func (r *RpcPlugin) SetMirrorRoute(rollout *v1alpha1.Rollout, setMirrorRoute *v1alpha1.SetMirrorRoute) pluginTypes.RpcError {
return pluginTypes.RpcError{}
}
func (r *RpcPlugin) VerifyWeight(rollout *v1alpha1.Rollout, desiredWeight int32, additionalDestinations []v1alpha1.WeightDestination) (pluginTypes.RpcVerified, pluginTypes.RpcError) {
return pluginTypes.Verified, pluginTypes.RpcError{}
}
func (r *RpcPlugin) RemoveManagedRoutes(rollout *v1alpha1.Rollout) pluginTypes.RpcError {
if !slices.ContainsFunc(rollout.Spec.Strategy.Canary.Steps, func(s v1alpha1.CanaryStep) bool {
return s.SetHeaderRoute != nil
}) {
// none of the steps have a SetHeaderRoute so nothing to clean up
return pluginTypes.RpcError{}
}
ctx := context.TODO()
glooPluginConfig, err := getPluginConfig(rollout)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
// get the matched routetables
matchedRts, err := r.getRouteTables(ctx, rollout, glooPluginConfig)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
if len(matchedRts) == 0 {
// nothing to update, don't bother computing things
return pluginTypes.RpcError{}
}
var combinedError error
for _, rt := range matchedRts {
originalRouteTable := &networkv2.RouteTable{}
rt.RouteTable.DeepCopyInto(originalRouteTable)
newRoutes := slices.DeleteFunc(rt.RouteTable.Spec.Http, func(r *networkv2.HTTPRoute) bool {
for _, managed := range rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes {
if strings.EqualFold(r.GetName(), managed.Name) {
return true
}
}
return false
})
rt.RouteTable.Spec.Http = newRoutes
if r.IsTest {
r.LogCtx.Debugf("test route table http routes: %v", rt.RouteTable.Spec.Http)
continue
}
if e := patchRouteTable(ctx, r.Client, rt.RouteTable, originalRouteTable); e != nil {
combinedError = errors.Join(combinedError, e)
continue
}
r.LogCtx.Debugf("patched route table %s.%s", rt.RouteTable.Namespace, rt.RouteTable.Name)
}
if combinedError != nil {
return pluginTypes.RpcError{
ErrorString: combinedError.Error(),
}
}
return pluginTypes.RpcError{}
}
func (r *RpcPlugin) Type() string {
return Type
}
func (r *RpcPlugin) getRouteTables(ctx context.Context, rollout *v1alpha1.Rollout, glooPluginConfig *GlooPlatformAPITrafficRouting) ([]*GlooMatchedRouteTable, error) {
if glooPluginConfig.RouteTableSelector == nil {
return nil, fmt.Errorf("routeTable selector is required")
}
if strings.EqualFold(glooPluginConfig.RouteTableSelector.Namespace, "") {
r.LogCtx.Debugf("defaulting routeTableSelector namespace to Rollout namespace %s for rollout %s", rollout.Namespace, rollout.Name)
glooPluginConfig.RouteTableSelector.Namespace = rollout.Namespace
}
var rts []*networkv2.RouteTable
if !strings.EqualFold(glooPluginConfig.RouteTableSelector.Name, "") {
r.LogCtx.Debugf("getRouteTables using ns:name ref %s:%s to get single table", glooPluginConfig.RouteTableSelector.Name, glooPluginConfig.RouteTableSelector.Namespace)
result, err := r.Client.RouteTables().GetRouteTable(ctx, glooPluginConfig.RouteTableSelector.Name, glooPluginConfig.RouteTableSelector.Namespace)
if err != nil {
return nil, err
}
r.LogCtx.Debugf("getRouteTables using ns:name ref %s:%s found 1 table", glooPluginConfig.RouteTableSelector.Name, glooPluginConfig.RouteTableSelector.Namespace)
rts = append(rts, result)
} else {
opts := &k8sclient.ListOptions{}
if glooPluginConfig.RouteTableSelector.Labels != nil {
opts.LabelSelector = labels.SelectorFromSet(glooPluginConfig.RouteTableSelector.Labels)
}
if !strings.EqualFold(glooPluginConfig.RouteTableSelector.Namespace, "") {
opts.Namespace = glooPluginConfig.RouteTableSelector.Namespace
}
r.LogCtx.Debugf("getRouteTables listing tables with opts %+v", opts)
var err error
rts, err = r.Client.RouteTables().ListRouteTable(ctx, opts)
if err != nil {
return nil, err
}
r.LogCtx.Debugf("getRouteTables listing tables with opts %+v; found %d routeTables", opts, len(rts))
}
matched := []*GlooMatchedRouteTable{}
for _, rt := range rts {
matchedRt := &GlooMatchedRouteTable{
RouteTable: rt,
}
// destination matching
if err := matchedRt.matchRoutes(r.LogCtx, rollout, glooPluginConfig); err != nil {
return nil, err // TODO: don't short circuit, potentially other RTs will match if we continue instead of immediately returning an error
}
matched = append(matched, matchedRt)
}
return matched, nil
}
func (g *GlooMatchedRouteTable) matchRoutes(logCtx *logrus.Entry, rollout *v1alpha1.Rollout, trafficConfig *GlooPlatformAPITrafficRouting) error {
if g.RouteTable == nil {
return fmt.Errorf("matchRoutes called for nil RouteTable")
}
// HTTP Routes
for _, httpRoute := range g.RouteTable.Spec.Http {
// find the destination that matches the stable svc
fw := httpRoute.GetForwardTo()
if fw == nil {
logCtx.Debugf("skipping route %s.%s because forwardTo is nil", g.RouteTable.Name, httpRoute.Name)
continue
}
// skip non-matching routes if RouteSelector provided
if trafficConfig.RouteSelector != nil {
// if name was provided, skip if route name doesn't match
if !strings.EqualFold(trafficConfig.RouteSelector.Name, "") && !strings.EqualFold(trafficConfig.RouteSelector.Name, httpRoute.Name) {
logCtx.Debugf("skipping route %s.%s because it doesn't match route name selector %s", g.RouteTable.Name, httpRoute.Name, trafficConfig.RouteSelector.Name)
continue
}
// if labels provided, skip if route labels do not contain all specified labels
if trafficConfig.RouteSelector.Labels != nil {
matchedLabels := func() bool {
for k, v := range trafficConfig.RouteSelector.Labels {
if vv, ok := httpRoute.Labels[k]; ok {
if !strings.EqualFold(v, vv) {
logCtx.Debugf("skipping route %s.%s because route labels do not contain %s=%s", g.RouteTable.Name, httpRoute.Name, k, v)
return false
}
}
}
return true
}()
if !matchedLabels {
continue
}
}
logCtx.Debugf("route %s.%s passed RouteSelector", g.RouteTable.Name, httpRoute.Name)
}
// find destinations
// var matchedDestinations []*GlooDestinations
var canary, stable *solov2.DestinationReference
for _, dest := range fw.Destinations {
ref := dest.GetRef()
if ref == nil {
logCtx.Debugf("skipping destination %s.%s because destination ref was nil; %+v", g.RouteTable.Name, httpRoute.Name, dest)
continue
}
if strings.EqualFold(ref.Name, rollout.Spec.Strategy.Canary.StableService) {
logCtx.Debugf("matched stable ref %s.%s.%s", g.RouteTable.Name, httpRoute.Name, ref.Name)
stable = dest
continue
}
if strings.EqualFold(ref.Name, rollout.Spec.Strategy.Canary.CanaryService) {
logCtx.Debugf("matched canary ref %s.%s.%s", g.RouteTable.Name, httpRoute.Name, ref.Name)
canary = dest
// bail if we found both stable and canary
if stable != nil {
break
}
continue
}
}
if stable != nil {
dest := &GlooMatchedHttpRoutes{
HttpRoute: httpRoute,
Destinations: &GlooDestinations{
StableOrActiveDestination: stable,
CanaryOrPreviewDestination: canary,
},
}
g.HttpRoutes = append(g.HttpRoutes, dest)
}
} // end range httpRoutes
return nil
}
func buildGlooMatches(headerRouting *v1alpha1.SetHeaderRoute) *solov2.HTTPRequestMatcher {
matcher := &solov2.HTTPRequestMatcher{
Name: headerRouting.Name + "-matcher",
Headers: []*solov2.HeaderMatcher{},
}
for _, m := range headerRouting.Match {
var isRegex bool
var matchValue string
if m.HeaderValue.Exact != "" {
matchValue = m.HeaderValue.Exact
} else if m.HeaderValue.Regex != "" {
matchValue = m.HeaderValue.Regex
isRegex = true
} else if m.HeaderValue.Prefix != "" {
matchValue = "^" + regexp.QuoteMeta(m.HeaderValue.Prefix)
isRegex = true
}
headerMatcher := &solov2.HeaderMatcher{
Name: m.HeaderName,
Value: matchValue,
Regex: isRegex,
}
matcher.Headers = append(matcher.Headers, headerMatcher)
}
return matcher
}
func getPluginConfig(rollout *v1alpha1.Rollout) (*GlooPlatformAPITrafficRouting, error) {
glooplatformConfig := GlooPlatformAPITrafficRouting{}
err := json.Unmarshal(rollout.Spec.Strategy.Canary.TrafficRouting.Plugins[PluginName], &glooplatformConfig)
if err != nil {
return nil, err
}
return &glooplatformConfig, nil
}