Skip to content

Commit af1e210

Browse files
authored
Merge pull request #1564 from kubroid/istio-tcp-canary
Istio Canary TCP service support
2 parents f946e0e + 4932527 commit af1e210

File tree

6 files changed

+333
-16
lines changed

6 files changed

+333
-16
lines changed

Makefile

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ test-codegen:
2929
test: test-fmt test-codegen
3030
go test ./...
3131

32+
test-coverage: test-fmt test-codegen
33+
go test -coverprofile cover.out ./...
34+
go tool cover -html=cover.out
35+
rm cover.out
36+
3237
crd:
3338
cat artifacts/flagger/crd.yaml > charts/flagger/crds/crd.yaml
3439
cat artifacts/flagger/crd.yaml > kustomize/base/flagger/crd.yaml

docs/gitbook/tutorials/istio-progressive-delivery.md

+60
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,63 @@ With the above configuration, Flagger will run a canary release with the followi
480480

481481
The above procedure can be extended with [custom metrics](../usage/metrics.md) checks, [webhooks](../usage/webhooks.md), [manual promotion](../usage/webhooks.md#manual-gating) approval and [Slack or MS Teams](../usage/alerting.md) notifications.
482482

483+
484+
## Canary Deployments for TCP Services
485+
486+
Performing a Canary deployment on a TCP (non HTTP) service is nearly identical to an HTTP Canary. Besides updating your `Gateway` document to support the `TCP` routing, the only difference is you have to set the `appProtocol` field to `TCP` inside of the `service` section of your `Canary` document.
487+
488+
#### Example:
489+
490+
```yaml
491+
apiVersion: networking.istio.io/v1alpha3
492+
kind: Gateway
493+
metadata:
494+
name: public-gateway
495+
namespace: istio-system
496+
spec:
497+
selector:
498+
istio: ingressgateway
499+
servers:
500+
- port:
501+
number: 7070
502+
name: tcp-service
503+
protocol: TCP # <== set the protocol to tcp here
504+
hosts:
505+
- "*"
506+
```
507+
508+
```yaml
509+
apiVersion: flagger.app/v1beta1
510+
kind: Canary
511+
...
512+
...
513+
service:
514+
port: 7070
515+
appProtocol: TCP # <== set the appProtocol here
516+
targetPort: 7070
517+
portName: "tcp-service-port"
518+
...
519+
...
520+
```
521+
522+
If the `appProtocol` equals `TCP` then Flagger will treat this as a Canary deployment for a `TCP` service. When it creates the `VirtualService` document it will add a `TCP` section to route requests between the `primary` and `canary` services. See Istio documentation for more information on this [spec](https://istio.io/latest/docs/reference/config/networking/virtual-service/#TCPRoute).
523+
524+
The resulting `VirtualService` will include a `tcp` section similar to what is shown below:
525+
```yaml
526+
tcp:
527+
- route:
528+
- destination:
529+
host: tcp-service-primary
530+
port:
531+
number: 7070
532+
weight: 100
533+
- destination:
534+
host: tcp-service-canary
535+
port:
536+
number: 7070
537+
weight: 0
538+
```
539+
540+
Once the Canary analysis begins, Flagger will be able to adjust the weights inside of this `tcp` section to advance the Canary deployment until it either runs into an error (and is halted) or it successfully reaches the end of the analysis and is Promoted.
541+
542+
It is also important to note that if you set `appProtocol` to anything other than `TCP`, for example if you set it to `HTTP`, it will perform the Canary and treat it as an `HTTP` service. The same remains true if you do not set `appProtocol` at all. It will __ONLY__ treat a Canary as a `TCP` service if `appProtocal` equals `TCP`.

pkg/apis/istio/v1alpha3/virtual_service.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ type TCPRoute struct {
597597
// Currently, only one destination is allowed for TCP services. When TCP
598598
// weighted routing support is introduced in Envoy, multiple destinations
599599
// with weights can be specified.
600-
Route HTTPRouteDestination `json:"route"`
600+
Route []HTTPRouteDestination `json:"route"`
601601
}
602602

603603
// L4 connection match attributes. Note that L4 connection matching support

pkg/apis/istio/v1alpha3/zz_generated.deepcopy.go

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/router/istio.go

+98-14
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ func (ir *IstioRouter) reconcileDestinationRule(canary *flaggerv1.Canary, name s
125125
return nil
126126
}
127127

128+
// return true if canary service has appProtocol == tcp
129+
func isTcp(canary *flaggerv1.Canary) bool {
130+
return strings.ToLower(canary.Spec.Service.AppProtocol) == "tcp"
131+
}
132+
133+
// map canary.spec.service.match into L4Match
134+
func canaryToL4Match(canary *flaggerv1.Canary) []istiov1alpha3.L4MatchAttributes {
135+
var match []istiov1alpha3.L4MatchAttributes
136+
for _, m := range canary.Spec.Service.Match {
137+
match = append(match, istiov1alpha3.L4MatchAttributes{
138+
Port: int(m.Port),
139+
})
140+
}
141+
142+
return match
143+
}
144+
128145
func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
129146
apexName, primaryName, canaryName := canary.GetServiceNames()
130147

@@ -175,20 +192,35 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
175192
gateways = []string{}
176193
}
177194

178-
newSpec := istiov1alpha3.VirtualServiceSpec{
179-
Hosts: hosts,
180-
Gateways: gateways,
181-
Http: []istiov1alpha3.HTTPRoute{
182-
{
183-
Match: canary.Spec.Service.Match,
184-
Rewrite: canary.Spec.Service.GetIstioRewrite(),
185-
Timeout: canary.Spec.Service.Timeout,
186-
Retries: canary.Spec.Service.Retries,
187-
CorsPolicy: canary.Spec.Service.CorsPolicy,
188-
Headers: canary.Spec.Service.Headers,
189-
Route: canaryRoute,
195+
var newSpec istiov1alpha3.VirtualServiceSpec
196+
197+
if isTcp(canary) {
198+
newSpec = istiov1alpha3.VirtualServiceSpec{
199+
Hosts: hosts,
200+
Gateways: gateways,
201+
Tcp: []istiov1alpha3.TCPRoute{
202+
{
203+
Match: canaryToL4Match(canary),
204+
Route: canaryRoute,
205+
},
190206
},
191-
},
207+
}
208+
} else {
209+
newSpec = istiov1alpha3.VirtualServiceSpec{
210+
Hosts: hosts,
211+
Gateways: gateways,
212+
Http: []istiov1alpha3.HTTPRoute{
213+
{
214+
Match: canary.Spec.Service.Match,
215+
Rewrite: canary.Spec.Service.GetIstioRewrite(),
216+
Timeout: canary.Spec.Service.Timeout,
217+
Retries: canary.Spec.Service.Retries,
218+
CorsPolicy: canary.Spec.Service.CorsPolicy,
219+
Headers: canary.Spec.Service.Headers,
220+
Route: canaryRoute,
221+
},
222+
},
223+
}
192224
}
193225

194226
newMetadata := canary.Spec.Service.Apex
@@ -203,7 +235,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
203235
}
204236
newMetadata.Annotations = filterMetadata(newMetadata.Annotations)
205237

206-
if len(canary.GetAnalysis().Match) > 0 {
238+
if !isTcp(canary) && len(canary.GetAnalysis().Match) > 0 {
207239
canaryMatch := mergeMatchConditions(canary.GetAnalysis().Match, canary.Spec.Service.Match)
208240
newSpec.Http = []istiov1alpha3.HTTPRoute{
209241
{
@@ -349,6 +381,38 @@ func (ir *IstioRouter) GetRoutes(canary *flaggerv1.Canary) (
349381
return
350382
}
351383

384+
if isTcp(canary) {
385+
ir.logger.Infof("Canary %s.%s uses TCP service", canary.Name, canary.Namespace)
386+
var tcpRoute istiov1alpha3.TCPRoute
387+
for _, tcp := range vs.Spec.Tcp {
388+
for _, r := range tcp.Route {
389+
if r.Destination.Host == canaryName {
390+
tcpRoute = tcp
391+
break
392+
}
393+
}
394+
}
395+
for _, route := range tcpRoute.Route {
396+
if route.Destination.Host == primaryName {
397+
primaryWeight = route.Weight
398+
}
399+
if route.Destination.Host == canaryName {
400+
canaryWeight = route.Weight
401+
}
402+
}
403+
404+
mirrored = false
405+
406+
if primaryWeight == 0 && canaryWeight == 0 {
407+
err = fmt.Errorf("VirtualService %s.%s does not contain routes for %s-primary and %s-canary",
408+
apexName, canary.Namespace, apexName, apexName)
409+
}
410+
411+
return
412+
}
413+
414+
ir.logger.Infof("Canary %s.%s uses HTTP service", canary.Name, canary.Namespace)
415+
352416
var httpRoute istiov1alpha3.HTTPRoute
353417
for _, http := range vs.Spec.Http {
354418
for _, r := range http.Route {
@@ -412,6 +476,26 @@ func (ir *IstioRouter) SetRoutes(
412476

413477
vsCopy := vs.DeepCopy()
414478

479+
if isTcp(canary) {
480+
// weighted routing (progressive canary)
481+
weightedRoute := istiov1alpha3.TCPRoute{
482+
Match: canaryToL4Match(canary),
483+
Route: []istiov1alpha3.HTTPRouteDestination{
484+
makeDestination(canary, primaryName, primaryWeight),
485+
makeDestination(canary, canaryName, canaryWeight),
486+
},
487+
}
488+
vsCopy.Spec.Tcp = []istiov1alpha3.TCPRoute{
489+
weightedRoute,
490+
}
491+
492+
vs, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Update(context.TODO(), vsCopy, metav1.UpdateOptions{})
493+
if err != nil {
494+
return fmt.Errorf("VirtualService %s.%s update failed: %w", apexName, canary.Namespace, err)
495+
}
496+
return nil
497+
}
498+
415499
// weighted routing (progressive canary)
416500
weightedRoute := istiov1alpha3.HTTPRoute{
417501
Match: canary.Spec.Service.Match,

0 commit comments

Comments
 (0)