Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ping pong support for istio #3371

Merged
merged 12 commits into from
Mar 26, 2024
12 changes: 12 additions & 0 deletions docs/features/traffic-management/istio.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,18 @@ help address this problem. The proposed solution is to introduce an annotation t
indicates to Argo CD to respect and preserve the differences at a specified path, in order to allow
other controllers (e.g. Argo Rollouts) controller manage them instead.

## Ping Pong

!!! important

Available since v1.7

Argo Rollouts also supports ping pong when using Istio this was added to support configuring both ALB and
Istio traffic routers at the same time. When using an ALB, ping-pong is generally a best practice especially with ALB readiness
gates enabled. However, when we change the service selectors when a rollout is aborted back to stable pod hash it causes a blip
of traffic outage because the ALB controller will set the pod readiness gates to false for a short while due to the label changes.
If we configure both ALB and Istio with ping-pong this selector change does not happen and hence we do not see any outages.

## Alternatives Considered

### Rollout ownership over the Virtual Service
Expand Down
10 changes: 5 additions & 5 deletions pkg/apis/rollouts/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ const (
DuplicatedPingPongServicesMessage = "This rollout uses the same service for the ping and pong services, but two different services are required."
// MissedAlbRootServiceMessage indicates that the rollout with ALB TrafficRouting and ping pong feature enabled must have root service provided
MissedAlbRootServiceMessage = "Root service field is required for the configuration with ALB and ping-pong feature enabled"
// PingPongWithAlbOnlyMessage At this moment ping-pong feature works with the ALB traffic routing only
PingPongWithAlbOnlyMessage = "Ping-pong feature works with the ALB traffic routing only"
// PingPongWithRouterOnlyMessage At this moment ping-pong feature works with the ALB traffic routing only
PingPongWithRouterOnlyMessage = "Ping-pong feature works with the ALB and Istio traffic routers only"
// InvalideStepRouteNameNotFoundInManagedRoutes A step has been configured that requires managedRoutes and the route name
// is missing from managedRoutes
InvalideStepRouteNameNotFoundInManagedRoutes = "Steps define a route that does not exist in spec.strategy.canary.trafficRouting.managedRoutes"
Expand Down Expand Up @@ -241,7 +241,7 @@ func requireCanaryStableServices(rollout *v1alpha1.Rollout) bool {

switch {
case canary.TrafficRouting.ALB != nil && canary.PingPong == nil,
canary.TrafficRouting.Istio != nil && canary.TrafficRouting.Istio.DestinationRule == nil,
canary.TrafficRouting.Istio != nil && canary.TrafficRouting.Istio.DestinationRule == nil && canary.PingPong == nil,
canary.TrafficRouting.SMI != nil,
canary.TrafficRouting.Apisix != nil,
canary.TrafficRouting.Ambassador != nil,
Expand All @@ -262,8 +262,8 @@ func ValidateRolloutStrategyCanary(rollout *v1alpha1.Rollout, fldPath *field.Pat
allErrs = append(allErrs, field.Invalid(fldPath.Child("stableService"), canary.StableService, DuplicatedServicesCanaryMessage))
}
if canary.PingPong != nil {
if canary.TrafficRouting != nil && canary.TrafficRouting.ALB == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("trafficRouting").Child("alb"), canary.TrafficRouting.ALB, PingPongWithAlbOnlyMessage))
if canary.TrafficRouting != nil && canary.TrafficRouting.ALB == nil && canary.TrafficRouting.Istio == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("trafficRouting").Child("alb"), canary.TrafficRouting.ALB, PingPongWithRouterOnlyMessage))
}
if canary.PingPong.PingService == "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("pingPong").Child("pingService"), canary.PingPong.PingService, InvalidPingPongProvidedMessage))
Expand Down
17 changes: 16 additions & 1 deletion pkg/apis/rollouts/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,21 @@ func TestValidateRolloutStrategyCanary(t *testing.T) {
assert.Empty(t, allErrs)
})

t.Run("valid Istio with ping pong", func(t *testing.T) {
validRo := ro.DeepCopy()
validRo.Spec.Strategy.Canary.Steps[0].SetWeight = pointer.Int32(10)
validRo.Spec.Strategy.Canary.CanaryService = ""
validRo.Spec.Strategy.Canary.StableService = ""
validRo.Spec.Strategy.Canary.PingPong = &v1alpha1.PingPongSpec{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add a new testcase for istio?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added new test instead of piggy backing off one

PingService: "ping",
PongService: "pong",
}
validRo.Spec.Strategy.Canary.TrafficRouting.Istio = &v1alpha1.IstioTrafficRouting{DestinationRule: &v1alpha1.IstioDestinationRule{Name: "destination-rule"}}
validRo.Spec.Strategy.Canary.TrafficRouting.ALB = nil
allErrs := ValidateRolloutStrategyCanary(validRo, field.NewPath(""))
assert.Empty(t, allErrs)
})

t.Run("valid PingPong missing canary and stable service", func(t *testing.T) {
validRo := ro.DeepCopy()
validRo.Spec.Strategy.Canary.Steps[0].SetWeight = pointer.Int32(10)
Expand Down Expand Up @@ -368,7 +383,7 @@ func TestValidateRolloutStrategyCanary(t *testing.T) {
Nginx: &v1alpha1.NginxTrafficRouting{StableIngress: "stable-ingress"},
}
allErrs := ValidateRolloutStrategyCanary(invalidRo, field.NewPath(""))
assert.Equal(t, PingPongWithAlbOnlyMessage, allErrs[0].Detail)
assert.Equal(t, PingPongWithRouterOnlyMessage, allErrs[0].Detail)
})

t.Run("invalid traffic routing", func(t *testing.T) {
Expand Down
18 changes: 8 additions & 10 deletions rollout/trafficrouting/istio/istio.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"fmt"
"strings"

"github.com/argoproj/argo-rollouts/rollout/trafficrouting"

jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -123,8 +125,7 @@ func (patches virtualServicePatches) patchVirtualService(httpRoutes []any, tlsRo
}

func (r *Reconciler) generateVirtualServicePatches(rolloutVsvcRouteNames []string, httpRoutes []VirtualServiceHTTPRoute, rolloutVsvcTLSRoutes []v1alpha1.TLSRoute, tlsRoutes []VirtualServiceTLSRoute, rolloutVsvcTCPRoutes []v1alpha1.TCPRoute, tcpRoutes []VirtualServiceTCPRoute, desiredWeight int64, additionalDestinations ...v1alpha1.WeightDestination) virtualServicePatches {
canarySvc := r.rollout.Spec.Strategy.Canary.CanaryService
stableSvc := r.rollout.Spec.Strategy.Canary.StableService
stableSvc, canarySvc := trafficrouting.GetStableAndCanaryServices(r.rollout)
canarySubset := ""
stableSubset := ""
if r.rollout.Spec.Strategy.Canary.TrafficRouting.Istio.DestinationRule != nil {
Expand Down Expand Up @@ -717,7 +718,7 @@ func (r *Reconciler) reconcileVirtualServiceHeaderRoutes(virtualService v1alpha1
return err
}

canarySvc := r.rollout.Spec.Strategy.Canary.CanaryService
_, canarySvc := trafficrouting.GetStableAndCanaryServices(r.rollout)
if destRuleHost != "" {
canarySvc = destRuleHost
}
Expand Down Expand Up @@ -1022,8 +1023,7 @@ func searchTcpRoute(tcpRoute v1alpha1.TCPRoute, istioTcpRoutes []VirtualServiceT

// ValidateHTTPRoutes ensures that all the routes in the rollout exist
func ValidateHTTPRoutes(r *v1alpha1.Rollout, routeNames []string, httpRoutes []VirtualServiceHTTPRoute) error {
stableSvc := r.Spec.Strategy.Canary.StableService
canarySvc := r.Spec.Strategy.Canary.CanaryService
stableSvc, canarySvc := trafficrouting.GetStableAndCanaryServices(r)

routeIndexesToPatch, err := getHttpRouteIndexesToPatch(routeNames, httpRoutes)
if err != nil {
Expand Down Expand Up @@ -1060,8 +1060,7 @@ func ValidateHTTPRoutes(r *v1alpha1.Rollout, routeNames []string, httpRoutes []V

// ValidateTlsRoutes ensures that all the routes in the rollout exist and they only have two destinations
func ValidateTlsRoutes(r *v1alpha1.Rollout, vsvcTLSRoutes []v1alpha1.TLSRoute, tlsRoutes []VirtualServiceTLSRoute) error {
stableSvc := r.Spec.Strategy.Canary.StableService
canarySvc := r.Spec.Strategy.Canary.CanaryService
stableSvc, canarySvc := trafficrouting.GetStableAndCanaryServices(r)

routeIndexesToPatch, err := getTlsRouteIndexesToPatch(vsvcTLSRoutes, tlsRoutes)
if err != nil {
Expand All @@ -1082,8 +1081,7 @@ func ValidateTlsRoutes(r *v1alpha1.Rollout, vsvcTLSRoutes []v1alpha1.TLSRoute, t

// ValidateTcpRoutes ensures that all the routes in the rollout exist and they only have two destinations
func ValidateTcpRoutes(r *v1alpha1.Rollout, vsvcTCPRoutes []v1alpha1.TCPRoute, tcpRoutes []VirtualServiceTCPRoute) error {
stableSvc := r.Spec.Strategy.Canary.StableService
canarySvc := r.Spec.Strategy.Canary.CanaryService
stableSvc, canarySvc := trafficrouting.GetStableAndCanaryServices(r)

routeIndexesToPatch, err := getTcpRouteIndexesToPatch(vsvcTCPRoutes, tcpRoutes)
if err != nil {
Expand Down Expand Up @@ -1191,7 +1189,7 @@ func (r *Reconciler) reconcileVirtualServiceMirrorRoutes(virtualService v1alpha1
if err != nil {
return fmt.Errorf("[reconcileVirtualServiceMirrorRoutes] failed to get destination rule host: %w", err)
}
canarySvc := r.rollout.Spec.Strategy.Canary.CanaryService
_, canarySvc := trafficrouting.GetStableAndCanaryServices(r.rollout)
if destRuleHost != "" {
canarySvc = destRuleHost
}
Expand Down
176 changes: 172 additions & 4 deletions rollout/trafficrouting/istio/istio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ func rollout(stableSvc, canarySvc string, istioVirtualService *v1alpha1.IstioVir
}
}

func rolloutPingPong(istioVirtualService *v1alpha1.IstioVirtualService) *v1alpha1.Rollout {
return &v1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "rollout",
Namespace: "default",
},
Spec: v1alpha1.RolloutSpec{
Strategy: v1alpha1.RolloutStrategy{
Canary: &v1alpha1.CanaryStrategy{
PingPong: &v1alpha1.PingPongSpec{
PingService: "ping",
PongService: "pong",
},
TrafficRouting: &v1alpha1.RolloutTrafficRouting{
Istio: &v1alpha1.IstioTrafficRouting{
VirtualService: istioVirtualService,
},
},
},
},
},
Status: v1alpha1.RolloutStatus{Canary: v1alpha1.CanaryStatus{StablePingPong: "ping"}},
}
}

func rolloutWithHttpRoutes(stableSvc, canarySvc, vsvc string, httpRoutes []string) *v1alpha1.Rollout {
istioVirtualService := &v1alpha1.IstioVirtualService{
Name: vsvc,
Expand Down Expand Up @@ -98,6 +123,16 @@ func rolloutWithHttpAndTlsAndTcpRoutes(stableSvc, canarySvc, vsvc string, httpRo
return rollout(stableSvc, canarySvc, istioVirtualService)
}

func rolloutWithHttpAndTlsAndTcpRoutesPingPong(vsvc string, httpRoutes []string, tlsRoutes []v1alpha1.TLSRoute, tcpRoutes []v1alpha1.TCPRoute) *v1alpha1.Rollout {
istioVirtualService := &v1alpha1.IstioVirtualService{
Name: vsvc,
Routes: httpRoutes,
TLSRoutes: tlsRoutes,
TCPRoutes: tcpRoutes,
}
return rolloutPingPong(istioVirtualService)
}

func checkDestination(t *testing.T, destinations []VirtualServiceRouteDestination, svc string, expectWeight int) {
for _, destination := range destinations {
if destination.Destination.Host == svc {
Expand Down Expand Up @@ -263,6 +298,72 @@ spec:
host: canary
weight: 0`

const regularMixedVsvcPingPong = `apiVersion: networking.istio.io/v1alpha3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer using go:embed directive to accomplish the same goal of having a large file instantiating its contents in a variable. This approach is cleaner/easier to maintain and doesn't require any additional tooling.

See the following PR as an example:
https://github.com/argoproj/argo-cd/pull/17404/files

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I kept it the way it is because it is consistent with all the other tests for istio

Copy link
Contributor

@leoluz leoluz Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.. but we have to start somewhere ¯\_(ツ)_/¯

kind: VirtualService
metadata:
name: vsvc
namespace: default
spec:
gateways:
- istio-rollout-gateway
hosts:
- istio-rollout.dev.argoproj.io
http:
- name: primary
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0
- name: secondary
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0
tls:
- match:
- port: 3000
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0
- match:
- port: 3001
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0
tcp:
- match:
- port: 3000
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0
- match:
- port: 3001
route:
- destination:
host: 'ping'
weight: 100
- destination:
host: pong
weight: 0`

const regularMixedVsvcTwoHttpRoutes = `apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
Expand Down Expand Up @@ -597,6 +698,14 @@ func extractTcpRoutes(t *testing.T, modifiedObj *unstructured.Unstructured) []Vi
}

func assertTcpRouteWeightChanges(t *testing.T, tcpRoute VirtualServiceTCPRoute, portNum, canaryWeight, stableWeight int) {
assertTcpRouteWeightChangesBase(t, tcpRoute, portNum, canaryWeight, stableWeight, "stable", "canary")
}

func assertTcpRouteWeightChangesPingPong(t *testing.T, tcpRoute VirtualServiceTCPRoute, portNum, canaryWeight, stableWeight int) {
assertTcpRouteWeightChangesBase(t, tcpRoute, portNum, canaryWeight, stableWeight, "ping", "pong")
}

func assertTcpRouteWeightChangesBase(t *testing.T, tcpRoute VirtualServiceTCPRoute, portNum, canaryWeight, stableWeight int, stableSvc, canarySvc string) {
portsMap := make(map[int64]bool)
for _, routeMatch := range tcpRoute.Match {
if routeMatch.Port != 0 {
Expand All @@ -610,8 +719,8 @@ func assertTcpRouteWeightChanges(t *testing.T, tcpRoute VirtualServiceTCPRoute,
if portNum != 0 {
assert.Equal(t, portNum, port)
}
checkDestination(t, tcpRoute.Route, "stable", stableWeight)
checkDestination(t, tcpRoute.Route, "canary", canaryWeight)
checkDestination(t, tcpRoute.Route, stableSvc, stableWeight)
checkDestination(t, tcpRoute.Route, canarySvc, canaryWeight)
}

func extractHttpRoutes(t *testing.T, modifiedObj *unstructured.Unstructured) []VirtualServiceHTTPRoute {
Expand Down Expand Up @@ -643,6 +752,14 @@ func extractTlsRoutes(t *testing.T, modifiedObj *unstructured.Unstructured) []Vi
}

func assertTlsRouteWeightChanges(t *testing.T, tlsRoute VirtualServiceTLSRoute, snis []string, portNum, canaryWeight, stableWeight int) {
assertTlsRouteWeightChangesBase(t, tlsRoute, snis, portNum, canaryWeight, stableWeight, "stable", "canary")
}

func assertTlsRouteWeightChangesPingPong(t *testing.T, tlsRoute VirtualServiceTLSRoute, snis []string, portNum, canaryWeight, stableWeight int) {
assertTlsRouteWeightChangesBase(t, tlsRoute, snis, portNum, canaryWeight, stableWeight, "ping", "pong")
}

func assertTlsRouteWeightChangesBase(t *testing.T, tlsRoute VirtualServiceTLSRoute, snis []string, portNum, canaryWeight, stableWeight int, stableSvc, canarySvc string) {
portsMap := make(map[int64]bool)
sniHostsMap := make(map[string]bool)
for _, routeMatch := range tlsRoute.Match {
Expand All @@ -667,8 +784,8 @@ func assertTlsRouteWeightChanges(t *testing.T, tlsRoute VirtualServiceTLSRoute,
if len(snis) != 0 {
assert.Equal(t, evalUtils.Equal(snis, sniHosts), true)
}
checkDestination(t, tlsRoute.Route, "stable", stableWeight)
checkDestination(t, tlsRoute.Route, "canary", canaryWeight)
checkDestination(t, tlsRoute.Route, stableSvc, stableWeight)
checkDestination(t, tlsRoute.Route, canarySvc, canaryWeight)
}

func TestHttpReconcileWeightsBaseCase(t *testing.T) {
Expand Down Expand Up @@ -1104,6 +1221,57 @@ func TestReconcileWeightsBaseCase(t *testing.T) {
assertTcpRouteWeightChanges(t, tcpRoutes[1], 3001, 0, 100)
}

func TestReconcileWeightsPingPongBaseCase(t *testing.T) {
r := &Reconciler{
rollout: rolloutWithHttpAndTlsAndTcpRoutesPingPong("vsvc", []string{"primary"},
[]v1alpha1.TLSRoute{
{
Port: 3000,
},
},
[]v1alpha1.TCPRoute{
{
Port: 3000,
},
},
),
}
obj := unstructuredutil.StrToUnstructuredUnsafe(regularMixedVsvcPingPong)
vsvcRoutes := r.rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualService.Routes
vsvcTLSRoutes := r.rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualService.TLSRoutes
vsvcTCPRoutes := r.rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualService.TCPRoutes
modifiedObj, _, err := r.reconcileVirtualService(obj, vsvcRoutes, vsvcTLSRoutes, vsvcTCPRoutes, 20)
assert.Nil(t, err)
assert.NotNil(t, modifiedObj)

// HTTP Routes
httpRoutes := extractHttpRoutes(t, modifiedObj)

// Assertions
assert.Equal(t, httpRoutes[0].Name, "primary")
checkDestination(t, httpRoutes[0].Route, "ping", 80)
checkDestination(t, httpRoutes[0].Route, "pong", 20)

//assertHttpRouteWeightChanges(t, httpRoutes[1], "secondary", 0, 100)
assert.Equal(t, httpRoutes[1].Name, "secondary")
checkDestination(t, httpRoutes[1].Route, "ping", 100)
checkDestination(t, httpRoutes[1].Route, "pong", 0)

// TLS Routes
tlsRoutes := extractTlsRoutes(t, modifiedObj)
//
// Assestions
assertTlsRouteWeightChangesPingPong(t, tlsRoutes[0], nil, 3000, 20, 80)
assertTlsRouteWeightChangesPingPong(t, tlsRoutes[1], nil, 3001, 0, 100)
//
// TCP Routes
tcpRoutes := extractTcpRoutes(t, modifiedObj)

// Assestions
assertTcpRouteWeightChangesPingPong(t, tcpRoutes[0], 3000, 20, 80)
assertTcpRouteWeightChangesPingPong(t, tcpRoutes[1], 3001, 0, 100)
}

func TestReconcileUpdateVirtualService(t *testing.T) {
ro := rolloutWithHttpRoutes("stable", "canary", "vsvc", []string{"primary"})
AssertReconcileUpdateVirtualService(t, regularVsvc, ro)
Expand Down
Loading
Loading