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
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
6 changes: 5 additions & 1 deletion pkg/apis/rollouts/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ func TestValidateRolloutStrategyCanary(t *testing.T) {
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(""))
Expand Down Expand Up @@ -368,7 +372,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