diff --git a/pkg/apis/flagger/v1beta1/metric.go b/pkg/apis/flagger/v1beta1/metric.go index 3d151c54..8fa01dbc 100644 --- a/pkg/apis/flagger/v1beta1/metric.go +++ b/pkg/apis/flagger/v1beta1/metric.go @@ -87,6 +87,7 @@ type MetricTemplateModel struct { Target string `json:"target"` Service string `json:"service"` Ingress string `json:"ingress"` + Route string `json:"route"` Interval string `json:"interval"` } @@ -98,6 +99,7 @@ func (mtm *MetricTemplateModel) TemplateFunctions() template.FuncMap { "target": func() string { return mtm.Target }, "service": func() string { return mtm.Service }, "ingress": func() string { return mtm.Ingress }, + "route": func() string { return mtm.Route }, "interval": func() string { return mtm.Interval }, } } diff --git a/pkg/controller/scheduler_metrics.go b/pkg/controller/scheduler_metrics.go index 245d52ef..3d018234 100644 --- a/pkg/controller/scheduler_metrics.go +++ b/pkg/controller/scheduler_metrics.go @@ -319,12 +319,17 @@ func toMetricModel(r *flaggerv1.Canary, interval string) flaggerv1.MetricTemplat if r.Spec.IngressRef != nil { ingress = r.Spec.IngressRef.Name } + route := r.Spec.TargetRef.Name + if r.Spec.RouteRef != nil { + route = r.Spec.RouteRef.Name + } return flaggerv1.MetricTemplateModel{ Name: r.Name, Namespace: r.Namespace, Target: r.Spec.TargetRef.Name, Service: service, Ingress: ingress, + Route: route, Interval: interval, } } diff --git a/pkg/metrics/observers/apisix.go b/pkg/metrics/observers/apisix.go index 80050310..a1358367 100644 --- a/pkg/metrics/observers/apisix.go +++ b/pkg/metrics/observers/apisix.go @@ -29,7 +29,7 @@ var apisixQueries = map[string]string{ sum( rate( apisix_http_status{ - route=~"{{ namespace }}_{{ target }}-canary_.+", + route=~"{{ namespace }}_{{ route }}-{{ target }}-canary_.+", code!~"5.." }[{{ interval }}] ) @@ -38,7 +38,7 @@ var apisixQueries = map[string]string{ sum( rate( apisix_http_status{ - route=~"{{ namespace }}_{{ target }}-canary_.+" + route=~"{{ namespace }}_{{ route }}-{{ target }}-canary_.+" }[{{ interval }}] ) ) * 100`, @@ -49,7 +49,7 @@ var apisixQueries = map[string]string{ rate( apisix_http_latency_bucket{ type=~"request", - route=~"{{ namespace }}_{{ target }}-canary_.+" + route=~"{{ namespace }}_{{ route }}-{{ target }}-canary_.+" }[{{ interval }}] ) ) by (le) @@ -61,7 +61,6 @@ type ApisixObserver struct { } func (ob *ApisixObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) { - query, err := RenderQuery(apisixQueries["request-success-rate"], model) if err != nil { return 0, fmt.Errorf("rendering query failed: %w", err) diff --git a/pkg/metrics/observers/apisix_test.go b/pkg/metrics/observers/apisix_test.go index 1e09c6f4..54fea774 100644 --- a/pkg/metrics/observers/apisix_test.go +++ b/pkg/metrics/observers/apisix_test.go @@ -31,7 +31,7 @@ import ( func TestApisixObserver_GetRequestSuccessRate(t *testing.T) { t.Run("ok", func(t *testing.T) { - expected := ` sum( rate( apisix_http_status{ route=~"default_podinfo-canary_.+", code!~"5.." }[1m] ) ) / sum( rate( apisix_http_status{ route=~"default_podinfo-canary_.+" }[1m] ) ) * 100` + expected := ` sum( rate( apisix_http_status{ route=~"default_podinfo-podinfo-canary_.+", code!~"5.." }[1m] ) ) / sum( rate( apisix_http_status{ route=~"default_podinfo-podinfo-canary_.+" }[1m] ) ) * 100` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { promql := r.URL.Query()["query"][0] @@ -56,6 +56,7 @@ func TestApisixObserver_GetRequestSuccessRate(t *testing.T) { Namespace: "default", Target: "podinfo", Service: "podinfo", + Route: "podinfo", Interval: "1m", }) require.NoError(t, err) @@ -84,7 +85,7 @@ func TestApisixObserver_GetRequestSuccessRate(t *testing.T) { } func TestApisixObserver_GetRequestDuration(t *testing.T) { - expected := ` histogram_quantile( 0.99, sum( rate( apisix_http_latency_bucket{ type=~"request", route=~"default_podinfo-canary_.+" }[1m] ) ) by (le) )` + expected := ` histogram_quantile( 0.99, sum( rate( apisix_http_latency_bucket{ type=~"request", route=~"default_podinfo-podinfo-canary_.+" }[1m] ) ) by (le) )` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { promql := r.URL.Query()["query"][0] @@ -109,6 +110,7 @@ func TestApisixObserver_GetRequestDuration(t *testing.T) { Namespace: "default", Target: "podinfo", Service: "podinfo", + Route: "podinfo", Interval: "1m", }) require.NoError(t, err) diff --git a/pkg/router/apisix.go b/pkg/router/apisix.go index ac38a501..fc3bee57 100644 --- a/pkg/router/apisix.go +++ b/pkg/router/apisix.go @@ -49,24 +49,46 @@ func (ar *ApisixRouter) Reconcile(canary *flaggerv1.Canary) error { apisixRoute, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Get(context.TODO(), canary.Spec.RouteRef.Name, metav1.GetOptions{}) if err != nil { - return fmt.Errorf("apisix route %s.%s get query error: %w", canary.Spec.RouteRef.Name, canary.Namespace, err) + return fmt.Errorf("APISIX route %s.%s get query error: %w", + canary.Spec.RouteRef.Name, canary.Namespace, err) } apisixRouteClone := apisixRoute.DeepCopy() - if len(apisixRouteClone.Spec.HTTP) != 1 && len(apisixRouteClone.Spec.HTTP[0].Backends) != 1 { - return fmt.Errorf("apisix route %s.%s only one http backend is supported", canary.Spec.RouteRef.Name, canary.Namespace) + if len(apisixRouteClone.Spec.HTTP) == 0 { + return fmt.Errorf("APISIX route %s.%s's spec.http is empty", + canary.Spec.RouteRef.Name, canary.Namespace) } - httpBackend := apisixRouteClone.Spec.HTTP[0] - httpBackend.Priority = maxPriority + apexName, primaryName, canaryName := canary.GetServiceNames() + var targetHttpRoute *a6v2.ApisixRouteHTTP + var targetIndex int + for index, item := range apisixRouteClone.Spec.HTTP { + for _, backend := range item.Backends { + if backend.ServiceName == apexName { + targetHttpRoute = &item + targetIndex = index + goto found + } + } + } - _, primaryName, canaryName := canary.GetServiceNames() +found: + if targetHttpRoute == nil { + return fmt.Errorf("Can not find %s backend on apisix route %s.%s ", + primaryName, canary.Spec.RouteRef.Name, canary.Namespace) + } + if len(targetHttpRoute.Backends) != 1 { + return fmt.Errorf("APISIX route %s.%s's http route %s only one http backend is supported", + canary.Spec.RouteRef.Name, canary.Namespace, targetHttpRoute.Name) + } - primaryBackend := httpBackend.Backends[0] + targetHttpRoute.Priority = maxPriority + + primaryBackend := targetHttpRoute.Backends[0] primaryBackend.ServiceName = primaryName primaryWeight := 100 primaryBackend.Weight = &primaryWeight - httpBackend.Backends[0] = primaryBackend + targetHttpRoute.Backends[0] = primaryBackend canaryWeight := 0 canaryBackend := a6v2.ApisixRouteHTTPBackend{ @@ -76,10 +98,11 @@ func (ar *ApisixRouter) Reconcile(canary *flaggerv1.Canary) error { Weight: &canaryWeight, Subset: primaryBackend.Subset, } - httpBackend.Backends = append(httpBackend.Backends, canaryBackend) - apisixRouteClone.Spec.HTTP[0] = httpBackend - canaryApisixRouteName := fmt.Sprintf("%s-canary", canary.Spec.RouteRef.Name) + targetHttpRoute.Backends = append(targetHttpRoute.Backends, canaryBackend) + apisixRouteClone.Spec.HTTP[targetIndex] = *targetHttpRoute + + canaryApisixRouteName := fmt.Sprintf("%s-%s-canary", canary.Spec.RouteRef.Name, apexName) canaryApisixRoute, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Get(context.TODO(), canaryApisixRouteName, metav1.GetOptions{}) if errors.IsNotFound(err) { @@ -105,41 +128,24 @@ func (ar *ApisixRouter) Reconcile(canary *flaggerv1.Canary) error { _, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Create(context.TODO(), route, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("apisix route %s.%s create error: %w", route.Name, route.Namespace, err) + return fmt.Errorf("APISIX route %s.%s create error: %w", route.Name, route.Namespace, err) } ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). - Infof("apisix route %s.%s created", route.GetName(), canary.Namespace) + Infof("APISIX route %s.%s created", route.GetName(), canary.Namespace) return nil } else if err != nil { - return fmt.Errorf("apisix route %s.%s query error: %w", canaryApisixRouteName, canary.Namespace, err) + return fmt.Errorf("APISIX route %s.%s query error: %w", canaryApisixRouteName, canary.Namespace, err) } - diffHttpSpec := "placeholder" - diffBackend := "placeholder" - diffCanaryBackend := "placeholder" - if len(canaryApisixRoute.Spec.HTTP) == 1 && - len(canaryApisixRoute.Spec.HTTP[0].Backends) == 2 { - diffHttpSpec = cmp.Diff(apisixRouteClone.Spec.HTTP[0], - canaryApisixRoute.Spec.HTTP[0], - cmpopts.IgnoreFields(a6v2.ApisixRouteHTTP{}, "Backends")) - - diffBackend = cmp.Diff(apisixRouteClone.Spec.HTTP[0].Backends[0], - canaryApisixRoute.Spec.HTTP[0].Backends[0], - cmpopts.IgnoreFields(a6v2.ApisixRouteHTTPBackend{}, "Weight")) - - diffCanaryBackend = cmp.Diff(apisixRouteClone.Spec.HTTP[0].Backends[1], - canaryApisixRoute.Spec.HTTP[0].Backends[1], - cmpopts.IgnoreFields(a6v2.ApisixRouteHTTPBackend{}, "Weight")) - } - - if diffHttpSpec != "" || diffBackend != "" || diffCanaryBackend != "" { + if diff := cmp.Diff(canaryApisixRoute.Spec, apisixRouteClone.Spec, + cmpopts.IgnoreFields(a6v2.ApisixRouteHTTPBackend{}, "Weight")); diff != "" { iClone := canaryApisixRoute.DeepCopy() iClone.Spec = apisixRouteClone.Spec _, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Update(context.TODO(), iClone, metav1.UpdateOptions{}) if err != nil { - return fmt.Errorf("apisix route %s.%s update error: %w", canaryApisixRouteName, iClone.Namespace, err) + return fmt.Errorf("APISIX route %s.%s update error: %w", canaryApisixRouteName, iClone.Namespace, err) } ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("Apisix route %s updated", canaryApisixRouteName) @@ -155,13 +161,13 @@ func (ar *ApisixRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - canaryApisixRouteName := fmt.Sprintf("%s-canary", canary.Spec.RouteRef.Name) + apexName, primaryName, _ := canary.GetServiceNames() + canaryApisixRouteName := fmt.Sprintf("%s-%s-canary", canary.Spec.RouteRef.Name, apexName) apisixRoute, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Get(context.TODO(), canaryApisixRouteName, metav1.GetOptions{}) if err != nil { err = fmt.Errorf("apisix route %s.%s query error: %w", canaryApisixRouteName, canary.Namespace, err) return } - _, primaryName, _ := canary.GetServiceNames() for _, backend := range apisixRoute.Spec.HTTP[0].Backends { if backend.ServiceName == primaryName { @@ -187,7 +193,7 @@ func (ar *ApisixRouter) SetRoutes( return fmt.Errorf("RoutingRule %s.%s update failed: no valid weights", apexName, canary.Namespace) } - canaryApisixRouteName := fmt.Sprintf("%s-canary", canary.Spec.RouteRef.Name) + canaryApisixRouteName := fmt.Sprintf("%s-%s-canary", canary.Spec.RouteRef.Name, apexName) apisixRoute, err := ar.apisixClient.ApisixV2().ApisixRoutes(canary.Namespace).Get(context.TODO(), canaryApisixRouteName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("apisix route %s.%s query error: %w", canaryApisixRouteName, canary.Namespace, err) diff --git a/pkg/router/apisix_test.go b/pkg/router/apisix_test.go index 9e6ba290..02ea9b45 100644 --- a/pkg/router/apisix_test.go +++ b/pkg/router/apisix_test.go @@ -38,9 +38,10 @@ func TestApisixRouter_Reconcile(t *testing.T) { apisixClient: mocks.flaggerClient, logger: mocks.logger, } + apexName, _, _ := mocks.canary.GetServiceNames() err := router.Reconcile(mocks.canary) require.NoError(t, err) - canaryName := fmt.Sprintf("%s-canary", mocks.canary.Spec.RouteRef.Name) + canaryName := fmt.Sprintf("%s-%s-canary", mocks.canary.Spec.RouteRef.Name, apexName) arCanary, err := router.apisixClient.ApisixV2().ApisixRoutes("default").Get(context.TODO(), canaryName, metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, 2, len(arCanary.Spec.HTTP[0].Backends)) @@ -77,7 +78,8 @@ func TestApisixRouter_GetSetRoutes(t *testing.T) { assert.Equal(t, 50, c) assert.False(t, m) - canaryName := fmt.Sprintf("%s-canary", mocks.canary.Spec.RouteRef.Name) + apexName, _, _ := mocks.canary.GetServiceNames() + canaryName := fmt.Sprintf("%s-%s-canary", mocks.canary.Spec.RouteRef.Name, apexName) arRouter, err := router.apisixClient.ApisixV2().ApisixRoutes("default").Get(context.TODO(), canaryName, metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, 2, len(arRouter.Spec.HTTP[0].Backends))