diff --git a/rollout/trafficrouting/alb/alb.go b/rollout/trafficrouting/alb/alb.go index fda478090e..103e3f5e9a 100644 --- a/rollout/trafficrouting/alb/alb.go +++ b/rollout/trafficrouting/alb/alb.go @@ -142,6 +142,7 @@ func (r *Reconciler) SetHeaderRoute(headerRoute *v1alpha1.SetHeaderRoute) error if !hasRule && headerRoute.Match != nil { desiredIngress.CreateAnnotationBasedPath(action) } + desiredIngress.SortHttpPaths(rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes) patch, modified, err := ingressutil.BuildIngressPatch(ingress.Mode(), ingress, desiredIngress, ingressutil.WithAnnotations(), ingressutil.WithSpec()) if err != nil { return nil diff --git a/utils/ingress/wrapper.go b/utils/ingress/wrapper.go index e176d59903..d70b6a96ed 100644 --- a/utils/ingress/wrapper.go +++ b/utils/ingress/wrapper.go @@ -3,6 +3,7 @@ package ingress import ( "context" "errors" + "sort" "sync" corev1 "k8s.io/api/core/v1" @@ -16,6 +17,8 @@ import ( networkingv1 "k8s.io/client-go/informers/networking/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" ) // Ingress defines an Ingress resource abstraction used to allow Rollouts to @@ -234,6 +237,38 @@ func (i *Ingress) RemovePathByServiceName(actionName string) { } } +func (i *Ingress) SortHttpPaths(routes []v1alpha1.MangedRoutes) { + var routeWeight = make(map[string]int) // map of route name for ordering + for j, route := range routes { + routeWeight[route.Name] = j + } + + i.mux.Lock() + defer i.mux.Unlock() + switch i.mode { + case IngressModeNetworking: + for _, rule := range i.ingress.Spec.Rules { + sort.SliceStable(rule.HTTP.Paths, func(i, j int) bool { + return getKeyWeight(routeWeight, rule.HTTP.Paths[i].Backend.Service.Name) < getKeyWeight(routeWeight, rule.HTTP.Paths[j].Backend.Service.Name) + }) + } + case IngressModeExtensions: + for _, rule := range i.legacyIngress.Spec.Rules { + sort.SliceStable(rule.HTTP.Paths, func(i, j int) bool { + return getKeyWeight(routeWeight, rule.HTTP.Paths[i].Backend.ServiceName) < getKeyWeight(routeWeight, rule.HTTP.Paths[j].Backend.ServiceName) + }) + } + } +} + +func getKeyWeight(weight map[string]int, key string) int { + if val, ok := weight[key]; ok { + return val + } else { + return len(weight) + } +} + func indexPathByService(rule v1.IngressRule, name string) int { for i, path := range rule.HTTP.Paths { if path.Backend.Service.Name == name { diff --git a/utils/ingress/wrapper_test.go b/utils/ingress/wrapper_test.go index 14b8508dd3..9e8f511494 100644 --- a/utils/ingress/wrapper_test.go +++ b/utils/ingress/wrapper_test.go @@ -15,6 +15,7 @@ import ( k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/pointer" + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" "github.com/argoproj/argo-rollouts/utils/ingress" ) @@ -396,6 +397,32 @@ func TestRemoveAnnotationBasedPath(t *testing.T) { }) } +func TestSortHttpPaths(t *testing.T) { + managedRoutes := []v1alpha1.MangedRoutes{{Name: "route1"}, {Name: "route2"}, {Name: "route3"}} + t.Run("v1 ingress, sort path", func(t *testing.T) { + ing := networkingIngressWithPath("action1", "route3", "route1", "route2") + ing.SortHttpPaths(managedRoutes) + ni, _ := ing.GetNetworkingIngress() + + assert.Equal(t, 4, len(ni.Spec.Rules[0].HTTP.Paths)) + assert.Equal(t, "route1", ni.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name) + assert.Equal(t, "route2", ni.Spec.Rules[0].HTTP.Paths[1].Backend.Service.Name) + assert.Equal(t, "route3", ni.Spec.Rules[0].HTTP.Paths[2].Backend.Service.Name) + assert.Equal(t, "action1", ni.Spec.Rules[0].HTTP.Paths[3].Backend.Service.Name) + }) + t.Run("v1beta1 ingress, sort path", func(t *testing.T) { + ing := extensionsIngressWithPath("action1", "route3", "route1", "route2") + ing.SortHttpPaths(managedRoutes) + ni, _ := ing.GetExtensionsIngress() + + assert.Equal(t, 4, len(ni.Spec.Rules[0].HTTP.Paths)) + assert.Equal(t, "route1", ni.Spec.Rules[0].HTTP.Paths[0].Backend.ServiceName) + assert.Equal(t, "route2", ni.Spec.Rules[0].HTTP.Paths[1].Backend.ServiceName) + assert.Equal(t, "route3", ni.Spec.Rules[0].HTTP.Paths[2].Backend.ServiceName) + assert.Equal(t, "action1", ni.Spec.Rules[0].HTTP.Paths[3].Backend.ServiceName) + }) +} + func TestDeepCopy(t *testing.T) { t.Run("will deepcopy ingress wrapped with networking.Ingress", func(t *testing.T) { // given @@ -976,6 +1003,43 @@ func networkingIngress() *ingress.Ingress { return ingress.NewIngress(&res) } +func networkingIngressWithPath(paths ...string) *ingress.Ingress { + var ingressPaths []v1.HTTPIngressPath + for _, path := range paths { + ingressPaths = append(ingressPaths, v1IngressPath(path)) + } + res := v1.Ingress{ + Spec: v1.IngressSpec{ + IngressClassName: pointer.String("v1ingress"), + Rules: []v1.IngressRule{ + { + Host: "v1host", + IngressRuleValue: v1.IngressRuleValue{ + HTTP: &v1.HTTPIngressRuleValue{ + Paths: ingressPaths, + }, + }, + }, + }, + }, + } + return ingress.NewIngress(&res) +} + +func v1IngressPath(serviceName string) v1.HTTPIngressPath { + pathType := v1.PathTypeImplementationSpecific + return v1.HTTPIngressPath{ + Backend: v1.IngressBackend{ + Service: &v1.IngressServiceBackend{ + Name: serviceName, + Port: v1.ServiceBackendPort{Name: "use-annotation"}, + }, + }, + Path: "/*", + PathType: &pathType, + } +} + func extensionsIngress() *ingress.Ingress { pathType := v1beta1.PathTypeImplementationSpecific res := v1beta1.Ingress{ @@ -1004,3 +1068,38 @@ func extensionsIngress() *ingress.Ingress { } return ingress.NewLegacyIngress(&res) } + +func extensionsIngressWithPath(paths ...string) *ingress.Ingress { + var ingressPaths []v1beta1.HTTPIngressPath + for _, path := range paths { + ingressPaths = append(ingressPaths, extensionIngressPath(path)) + } + res := v1beta1.Ingress{ + Spec: v1beta1.IngressSpec{ + IngressClassName: pointer.String("v1beta1ingress"), + Rules: []v1beta1.IngressRule{ + { + Host: "v1beta1host", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: ingressPaths, + }, + }, + }, + }, + }, + } + return ingress.NewLegacyIngress(&res) +} + +func extensionIngressPath(serviceName string) v1beta1.HTTPIngressPath { + pathType := v1beta1.PathTypeImplementationSpecific + return v1beta1.HTTPIngressPath{ + Backend: v1beta1.IngressBackend{ + ServiceName: serviceName, + ServicePort: intstr.FromString("use-annotation"), + }, + Path: "/*", + PathType: &pathType, + } +}