diff --git a/README.md b/README.md index 8995e67d17..977a35eaa6 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ For these reasons, in large scale high-volume production environments, a rolling |-----------------------------------|------------------------------|-----------------------------|----------------------------|----------------------------| | ALB Ingress Controller | :white_check_mark: (stable) | :white_check_mark: (stable) | :x: | :white_check_mark: (alpha) | | Ambassador | :white_check_mark: (stable) | :x: | :x: | :x: | -| Apache APISIX Ingress Controller | :white_check_mark: (alpha) | :x: | :x: | :x: | +| Apache APISIX Ingress Controller | :white_check_mark: (alpha) | :x: | :x: | :white_check_mark: (alpha) | | Istio | :white_check_mark: (stable) | :white_check_mark: (stable) | :white_check_mark: (alpha) | :white_check_mark: (alpha) | | Nginx Ingress Controller | :white_check_mark: (stable) | :x: | :x: | :x: | | SMI | :white_check_mark: (stable) | :white_check_mark: (stable) | :x: | :x: | diff --git a/docs/features/traffic-management/apisix.md b/docs/features/traffic-management/apisix.md index ee741ba889..24bb734a17 100644 --- a/docs/features/traffic-management/apisix.md +++ b/docs/features/traffic-management/apisix.md @@ -66,12 +66,22 @@ spec: canaryService: rollout-apisix-canary-canary stableService: rollout-apisix-canary-stable trafficRouting: + managedRoutes: + - name: set-header apisix: route: name: rollouts-apisix-route rules: - rollouts-apisix steps: + - setCanaryScale: + replicas: 1 + setHeaderRoute: + match: + - headerName: trace + headerValue: + exact: debug + name: set-header - setWeight: 20 - pause: {} - setWeight: 40 @@ -182,5 +192,25 @@ Spec: Weight: 20 ...... ``` +The `rollout-apisix-canary-canary` service gets 20% traffic through the Apache APISIX. -The `rollout-apisix-canary-canary` service gets 20% traffic through the Apache APISIX. \ No newline at end of file +You can check SetHeader ApisixRoute's match by the following command +```bash +kubectl describe apisixroute set-header + +...... +Spec: + Http: + Backends: + Service Name: rollout-apisix-canary-canary + Service Port: 80 + Weight: 100 + Match: + Exprs: + Op: Equal + Subject: + Name: trace + Scope: Header + Value: debug +...... +``` diff --git a/examples/apisix/rollout.yaml b/examples/apisix/rollout.yaml index 9a8e923078..99de23751d 100644 --- a/examples/apisix/rollout.yaml +++ b/examples/apisix/rollout.yaml @@ -9,12 +9,23 @@ spec: canaryService: rollout-apisix-canary-canary stableService: rollout-apisix-canary-stable trafficRouting: + managedRoutes: + - name: set-header apisix: route: name: rollouts-apisix-route rules: - rollouts-apisix steps: + - setCanaryScale: + replicas: 1 + setHeaderRoute: + match: + - headerName: trace + headerValue: + exact: debug + name: set-header + - pause: { } - setWeight: 20 - pause: { } - setWeight: 40 diff --git a/pkg/apis/rollouts/validation/validation.go b/pkg/apis/rollouts/validation/validation.go index 37bf7eaa63..34c4e3cbf9 100644 --- a/pkg/apis/rollouts/validation/validation.go +++ b/pkg/apis/rollouts/validation/validation.go @@ -34,7 +34,7 @@ const ( // InvalidSetCanaryScaleTrafficPolicy indicates that TrafficRouting, required for SetCanaryScale, is missing InvalidSetCanaryScaleTrafficPolicy = "SetCanaryScale requires TrafficRouting to be set" // InvalidSetHeaderRouteTrafficPolicy indicates that TrafficRouting required for SetHeaderRoute is missing - InvalidSetHeaderRouteTrafficPolicy = "SetHeaderRoute requires TrafficRouting, supports Istio and ALB" + InvalidSetHeaderRouteTrafficPolicy = "SetHeaderRoute requires TrafficRouting, supports Istio and ALB and Apisix" // InvalidSetMirrorRouteTrafficPolicy indicates that TrafficRouting, required for SetCanaryScale, is missing InvalidSetMirrorRouteTrafficPolicy = "SetMirrorRoute requires TrafficRouting, supports Istio only" // InvalidStringMatchMultipleValuePolicy indicates that SetCanaryScale, has multiple values set @@ -305,7 +305,7 @@ func ValidateRolloutStrategyCanary(rollout *v1alpha1.Rollout, fldPath *field.Pat if step.SetHeaderRoute != nil { trafficRouting := rollout.Spec.Strategy.Canary.TrafficRouting - if trafficRouting == nil || (trafficRouting.Istio == nil && trafficRouting.ALB == nil) { + if trafficRouting == nil || (trafficRouting.Istio == nil && trafficRouting.ALB == nil && trafficRouting.Apisix == nil) { allErrs = append(allErrs, field.Invalid(stepFldPath.Child("setHeaderRoute"), step.SetHeaderRoute, InvalidSetHeaderRouteTrafficPolicy)) } else if step.SetHeaderRoute.Match != nil && len(step.SetHeaderRoute.Match) > 0 { for j, match := range step.SetHeaderRoute.Match { diff --git a/rollout/trafficrouting/apisix/apisix.go b/rollout/trafficrouting/apisix/apisix.go index 823a8b6102..1147721a39 100644 --- a/rollout/trafficrouting/apisix/apisix.go +++ b/rollout/trafficrouting/apisix/apisix.go @@ -3,19 +3,26 @@ package apisix import ( "context" "fmt" + "time" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" "github.com/argoproj/argo-rollouts/utils/record" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +var controllerKind = v1alpha1.SchemeGroupVersion.WithKind("Rollout") + // Type holds this controller type const Type = "Apisix" const apisixRouteUpdateError = "ApisixRouteUpdateError" +const apisixRouteCreateError = "ApisixRouteCreateError" +const apisixRouteDeleteError = "ApisixRouteDeleteError" +const failedToTypeAssertion = "Failed type assertion for Apisix http route" type ReconcilerConfig struct { Rollout *v1alpha1.Rollout @@ -40,6 +47,8 @@ func (r *Reconciler) sendEvent(eventType, id, msg string) { type ClientInterface interface { Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) + Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) + Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error } func NewReconciler(cfg *ReconcilerConfig) *Reconciler { @@ -64,12 +73,31 @@ func (r *Reconciler) SetWeight(desiredWeight int32, additionalDestinations ...v1 return err } - httpRoutes, isFound, err := unstructured.NestedSlice(apisixRoute.Object, "spec", "http") + httpRoutes, err := r.processSetWeightRoutes(desiredWeight, apisixRoute, rollout, apisixRouteName) + if err != nil { + return err + } + + err = unstructured.SetNestedSlice(apisixRoute.Object, httpRoutes, "spec", "http") if err != nil { return err } + _, err = r.Client.Update(ctx, apisixRoute, metav1.UpdateOptions{}) + if err != nil { + msg := fmt.Sprintf("Error updating apisix route %q: %s", apisixRoute.GetName(), err) + r.sendWarningEvent(apisixRouteUpdateError, msg) + } + + return err +} + +func (r *Reconciler) processSetWeightRoutes(desiredWeight int32, apisixRoute *unstructured.Unstructured, rollout *v1alpha1.Rollout, apisixRouteName string) ([]interface{}, error) { + httpRoutes, isFound, err := unstructured.NestedSlice(apisixRoute.Object, "spec", "http") + if err != nil { + return nil, err + } if !isFound { - return errors.New("spec.http was not found in Apisix Route manifest") + return nil, errors.New("spec.http was not found in Apisix Route manifest") } rules := rollout.Spec.Strategy.Canary.TrafficRouting.Apisix.Route.Rules if rules == nil { @@ -78,45 +106,34 @@ func (r *Reconciler) SetWeight(desiredWeight int32, additionalDestinations ...v1 for _, ruleName := range rules { httpRoute, err := GetHttpRoute(httpRoutes, ruleName) if err != nil { - return err + return nil, err } backends, err := GetBackends(httpRoute) if err != nil { - return err + return nil, err } canaryBackendName := rollout.Spec.Strategy.Canary.CanaryService err = setBackendWeight(canaryBackendName, backends, int64(desiredWeight)) if err != nil { - return err + return nil, err } stableBackendName := rollout.Spec.Strategy.Canary.StableService err = setBackendWeight(stableBackendName, backends, int64(100-desiredWeight)) if err != nil { - return err + return nil, err } } - - err = unstructured.SetNestedSlice(apisixRoute.Object, httpRoutes, "spec", "http") - if err != nil { - return err - } - _, err = r.Client.Update(ctx, apisixRoute, metav1.UpdateOptions{}) - if err != nil { - msg := fmt.Sprintf("Error updating apisix route %q: %s", apisixRoute.GetName(), err) - r.sendWarningEvent(apisixRouteUpdateError, msg) - } - - return err + return httpRoutes, nil } func GetHttpRoute(routes []interface{}, ref string) (interface{}, error) { for _, route := range routes { typedRoute, ok := route.(map[string]interface{}) if !ok { - return nil, errors.New("Failed type assertion for Apisix http route") + return nil, errors.New(failedToTypeAssertion) } rawName, ok := typedRoute["name"] if !ok { @@ -124,7 +141,7 @@ func GetHttpRoute(routes []interface{}, ref string) (interface{}, error) { } typedName, ok := rawName.(string) if !ok { - return nil, errors.New("Failed type assertion for Apisix http route rule name") + return nil, errors.New(fmt.Sprintf("%s rule name", failedToTypeAssertion)) } if typedName == ref { return route, nil @@ -137,7 +154,7 @@ func GetHttpRoute(routes []interface{}, ref string) (interface{}, error) { func GetBackends(httpRoute interface{}) ([]interface{}, error) { typedHttpRoute, ok := httpRoute.(map[string]interface{}) if !ok { - return nil, errors.New("Failed type assertion for Apisix http route") + return nil, errors.New(failedToTypeAssertion) } rawBackends, ok := typedHttpRoute["backends"] if !ok { @@ -145,7 +162,7 @@ func GetBackends(httpRoute interface{}) ([]interface{}, error) { } backends, ok := rawBackends.([]interface{}) if !ok { - return nil, errors.New("Failed type assertion for Apisix http route backends") + return nil, errors.New(fmt.Sprintf("%s backends", failedToTypeAssertion)) } return backends, nil } @@ -155,7 +172,7 @@ func setBackendWeight(backendName string, backends []interface{}, weight int64) for _, backend := range backends { typedBackend, ok := backend.(map[string]interface{}) if !ok { - return errors.New("Failed type assertion for Apisix http route backend") + return errors.New(fmt.Sprintf("%s backends", failedToTypeAssertion)) } nameOfCurrentBackend, isFound, err := unstructured.NestedString(typedBackend, "serviceName") if err != nil { @@ -178,9 +195,219 @@ func setBackendWeight(backendName string, backends []interface{}, weight int64) } func (r *Reconciler) SetHeaderRoute(headerRouting *v1alpha1.SetHeaderRoute) error { + ctx := context.TODO() + rollout := r.Rollout + apisixRouteName := rollout.Spec.Strategy.Canary.TrafficRouting.Apisix.Route.Name + apisixRoute, err := r.Client.Get(ctx, apisixRouteName, metav1.GetOptions{}) + if err != nil { + return err + } + + setHeaderApisixRoute, isNew, err := r.makeSetHeaderRoute(ctx, headerRouting, apisixRoute) + if !isNew && err != nil { + return err + } + + if headerRouting.Match == nil { + if isNew { + return nil + } else { + err = r.Client.Delete(ctx, headerRouting.Name, metav1.DeleteOptions{}) + if err != nil { + msg := fmt.Sprintf("Error delete apisix route %q: %s", setHeaderApisixRoute.GetName(), err) + r.sendWarningEvent(apisixRouteDeleteError, msg) + return err + } + return nil + } + } + + err = r.processSetHeaderApisixRoute(headerRouting, setHeaderApisixRoute, isNew) + if err != nil { + return err + } + if isNew { + _, err = r.Client.Create(ctx, setHeaderApisixRoute, metav1.CreateOptions{}) + } else { + _, err = r.Client.Update(ctx, setHeaderApisixRoute, metav1.UpdateOptions{}) + } + operate := "update" + if isNew { + operate = "create" + } + + if err != nil { + msg := fmt.Sprintf("Error %s apisix route %q: %s", operate, setHeaderApisixRoute.GetName(), err) + if isNew { + r.sendWarningEvent(apisixRouteCreateError, msg) + } else { + r.sendWarningEvent(apisixRouteUpdateError, msg) + } + + } + return err +} + +func (r *Reconciler) processSetHeaderApisixRoute(headerRouting *v1alpha1.SetHeaderRoute, setHeaderApisixRoute *unstructured.Unstructured, isNew bool) error { + httpRoutes, isFound, err := unstructured.NestedSlice(setHeaderApisixRoute.Object, "spec", "http") + if err != nil { + return err + } + if !isFound { + return errors.New("spec.http was not found in Apisix Route manifest") + } + rules := r.Rollout.Spec.Strategy.Canary.TrafficRouting.Apisix.Route.Rules + if rules == nil { + rules = append(rules, r.Rollout.Spec.Strategy.Canary.TrafficRouting.Apisix.Route.Name) + } + for _, ruleName := range rules { + httpRoute, err := GetHttpRoute(httpRoutes, ruleName) + if err != nil { + return err + } + + backends, err := GetBackends(httpRoute) + if err != nil { + return err + } + + canaryBackendName := r.Rollout.Spec.Strategy.Canary.CanaryService + err = setBackendWeight(canaryBackendName, backends, 100) + if err != nil { + return err + } + + stableBackendName := r.Rollout.Spec.Strategy.Canary.StableService + err = removeBackend(httpRoute, stableBackendName, backends) + if err != nil { + return err + } + if isNew { + if err = processRulePriority(httpRoute); err != nil { + return err + } + } + + if err = setApisixRuleMatch(httpRoute, headerRouting); err != nil { + return err + } + } + return unstructured.SetNestedSlice(setHeaderApisixRoute.Object, httpRoutes, "spec", "http") +} + +func (r *Reconciler) makeSetHeaderRoute(ctx context.Context, headerRouting *v1alpha1.SetHeaderRoute, apisixRoute *unstructured.Unstructured) (*unstructured.Unstructured, bool, error) { + setHeaderApisixRoute, err := r.Client.Get(ctx, headerRouting.Name, metav1.GetOptions{}) + isNew := false + + if err != nil { + // create new ApisixRoute CR + if k8serrors.IsNotFound(err) { + setHeaderApisixRoute = apisixRoute.DeepCopy() + setHeaderApisixRoute.SetName(headerRouting.Name) + setHeaderApisixRoute.SetResourceVersion("") + setHeaderApisixRoute.SetGeneration(0) + setHeaderApisixRoute.SetUID("") + setHeaderApisixRoute.SetCreationTimestamp(metav1.NewTime(time.Time{})) + setHeaderApisixRoute.SetOwnerReferences([]metav1.OwnerReference{ + *metav1.NewControllerRef(r.Rollout, controllerKind), + }) + isNew = true + } else { + return nil, false, err + } + } else { + if !metav1.IsControlledBy(setHeaderApisixRoute, r.Rollout) { + return nil, false, errors.New(fmt.Sprintf("duplicate ApisixRoute [%s] already exists", headerRouting.Name)) + } + } + return setHeaderApisixRoute, isNew, nil +} + +func removeBackend(route interface{}, backendName string, backends []interface{}) error { + typedRoute, ok := route.(map[string]interface{}) + if !ok { + return errors.New("Failed type assertion for Apisix http route") + } + result := []interface{}{} + for _, backend := range backends { + typedBackend, ok := backend.(map[string]interface{}) + if !ok { + return errors.New("Failed type assertion for Apisix http route backend") + } + nameOfCurrentBackend, isFound, err := unstructured.NestedString(typedBackend, "serviceName") + if err != nil { + return err + } + if !isFound { + return errors.New("serviceName field was not found in backend") + } + if nameOfCurrentBackend != backendName { + result = append(result, backend) + } + } + return unstructured.SetNestedSlice(typedRoute, result, "backends") +} + +func processRulePriority(route interface{}) error { + typedRoute, ok := route.(map[string]interface{}) + if !ok { + return errors.New("Failed type assertion for Apisix http route") + } + + priority, ok, err := unstructured.NestedInt64(typedRoute, "priority") + if err != nil { + return err + } + if !ok { + priority = 0 + } + priority++ + typedRoute["priority"] = priority return nil } +func setApisixRuleMatch(route interface{}, headerRouting *v1alpha1.SetHeaderRoute) error { + typedRoute, ok := route.(map[string]interface{}) + if !ok { + return errors.New("Failed type assertion for Apisix http route") + } + exprs := []interface{}{} + for _, match := range headerRouting.Match { + exprs = append(exprs, apisixExprs(match.HeaderName, match.HeaderValue.Exact, match.HeaderValue.Regex, match.HeaderValue.Prefix)...) + } + return unstructured.SetNestedSlice(typedRoute, exprs, "match", "exprs") +} + +func apisixExprs(header, exact, regex, prefix string) []interface{} { + subject := map[string]interface{}{ + "scope": "Header", + "name": header, + } + exprs := []interface{}{} + if exact != "" { + exprs = append(exprs, map[string]interface{}{ + "subject": subject, + "op": "Equal", + "value": exact, + }) + } + if regex != "" { + exprs = append(exprs, map[string]interface{}{ + "subject": subject, + "op": "RegexMatch", + "value": regex, + }) + } + if prefix != "" { + exprs = append(exprs, map[string]interface{}{ + "subject": subject, + "op": "RegexMatch", + "value": fmt.Sprintf("^%s.*", prefix), + }) + } + return exprs +} + func (r *Reconciler) VerifyWeight(desiredWeight int32, additionalDestinations ...v1alpha1.WeightDestination) (*bool, error) { return nil, nil } @@ -194,5 +421,31 @@ func (r *Reconciler) SetMirrorRoute(setMirrorRoute *v1alpha1.SetMirrorRoute) err } func (r *Reconciler) RemoveManagedRoutes() error { + + ctx := context.TODO() + + managedRoutes := r.Rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes + if len(managedRoutes) == 0 { + return nil + } + + for _, managedRoute := range managedRoutes { + apisixRoute, err := r.Client.Get(ctx, managedRoute.Name, metav1.GetOptions{}) + if err != nil { + if !k8serrors.IsNotFound(err) { + return err + } else { + continue + } + } + if metav1.IsControlledBy(apisixRoute, r.Rollout) { + err = r.Client.Delete(ctx, managedRoute.Name, metav1.DeleteOptions{}) + if err != nil { + msg := fmt.Sprintf("Error deleting apisix route %q: %s", apisixRoute.GetName(), err) + r.sendWarningEvent(apisixRouteDeleteError, msg) + return err + } + } + } return nil } diff --git a/rollout/trafficrouting/apisix/apisix_test.go b/rollout/trafficrouting/apisix/apisix_test.go index 6007484c76..9fcf4ff394 100644 --- a/rollout/trafficrouting/apisix/apisix_test.go +++ b/rollout/trafficrouting/apisix/apisix_test.go @@ -1,6 +1,8 @@ package apisix import ( + "errors" + "fmt" "testing" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" @@ -11,6 +13,9 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer/yaml" ) +const SetHeaderRouteName = "set-header" +const HeaderName = "header-name" + const apisixRoute = ` apiVersion: apisix.apache.org/v2 kind: ApisixRoute @@ -40,6 +45,77 @@ metadata: name: mocks-apisix-route ` +const apisixSetHeaderRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: set-header + ownerReferences: + - apiVersion: argoproj.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Rollout + name: rollout + uid: 1a2b2d82-50a4-4d83-9ff4-cdc6f5197d30 +spec: + http: + - backends: + - serviceName: canary-rollout + servicePort: 80 + weight: 100 + match: + exprs: + - op: Equal + subject: + name: trace + scope: Header + value: debug + hosts: + - rollouts-demo.apisix.local + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + paths: + - /* + name: mocks-apisix-route + priority: 2 +` + +const apisixSetHeaderDuplicateRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: set-header +spec: + http: + - backends: + - serviceName: canary-rollout + servicePort: 80 + weight: 100 + match: + exprs: + - op: Equal + subject: + name: trace + scope: Header + value: debug + hosts: + - rollouts-demo.apisix.local + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + paths: + - /* + name: mocks-apisix-route + priority: 2 +` + var ( client *mocks.FakeClient = &mocks.FakeClient{} ) @@ -50,6 +126,7 @@ const ( canaryServiceName string = "canary-rollout" fakeCanaryServiceName string = "fake-canary-rollout" apisixRouteName string = "mocks-apisix-route" + setHeaderName string = "mocks-set-header" ) func TestUpdateHash(t *testing.T) { @@ -298,9 +375,120 @@ func TestSetBackendWeightError(t *testing.T) { } func TestSetHeaderRoute(t *testing.T) { - t.Run("SetHeaderRoute", func(t *testing.T) { + mocks.ApisixRouteObj = toUnstructured(t, apisixRoute) + mocks.SetHeaderApisixRouteObj = toUnstructured(t, apisixSetHeaderRoute) + mocks.DuplicateSetHeaderApisixRouteObj = toUnstructured(t, apisixSetHeaderDuplicateRoute) + mocks.ErrorApisixRouteObj = toUnstructured(t, errorApisixRoute) + t.Run("SetHeaderGetRouteError", func(t *testing.T) { + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: &mocks.FakeClient{ + IsGetError: true, + }, + } + r := NewReconciler(&cfg) + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + }) + + // Then + assert.Error(t, err) + }) + t.Run("SetHeaderGetManagedRouteError", func(t *testing.T) { + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: &mocks.FakeClient{ + IsGetManagedRouteError: true, + }, + } + r := NewReconciler(&cfg) + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{{ + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }}, + }) + + // Then + assert.Error(t, err) + }) + t.Run("SetHeaderDuplicateManagedRouteError", func(t *testing.T) { + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: &mocks.FakeClient{ + IsDuplicateSetHeaderRouteError: true, + }, + } + r := NewReconciler(&cfg) + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{{ + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }}, + }) + + // Then + assert.ErrorContains(t, err, "duplicate ApisixRoute") + + }) + t.Run("SetHeaderRouteNilMatchWithNew", func(t *testing.T) { + // Given + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: &mocks.FakeClient{ + IsGetNotFoundError: true, + }, + } + r := NewReconciler(&cfg) + + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: nil, + }) + + // Then + assert.NoError(t, err) + }) + t.Run("SetHeaderRouteNilMatch", func(t *testing.T) { + client := &mocks.FakeClient{} + // Given + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: nil, + }) + + // Then + assert.NoError(t, err) + assert.Equal(t, SetHeaderRouteName, client.DeleteName) + }) + t.Run("SetHeaderRoutePriorityWithNew", func(t *testing.T) { // Given t.Parallel() + client := &mocks.FakeClient{ + IsGetNotFoundError: true, + } cfg := ReconcilerConfig{ Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), Client: client, @@ -309,9 +497,9 @@ func TestSetHeaderRoute(t *testing.T) { // When err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ - Name: "set-header", + Name: SetHeaderRouteName, Match: []v1alpha1.HeaderRoutingMatch{{ - HeaderName: "header-name", + HeaderName: HeaderName, HeaderValue: &v1alpha1.StringMatch{ Exact: "value", }, @@ -320,10 +508,289 @@ func TestSetHeaderRoute(t *testing.T) { // Then assert.NoError(t, err) + rules, ok, err := unstructured.NestedSlice(client.CreatedObj.Object, "spec", "http") + assert.NoError(t, err) + assert.Equal(t, true, ok) - err = r.RemoveManagedRoutes() - assert.Nil(t, err) + rule, ok := rules[0].(map[string]interface{}) + assert.Equal(t, true, ok) + priority, ok, err := unstructured.NestedInt64(rule, "priority") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, int64(1), priority) + }) + t.Run("SetHeaderRoutePriorityWithNew", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsGetNotFoundError: false, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{{ + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }}, + }) + + // Then + assert.NoError(t, err) + rules, ok, err := unstructured.NestedSlice(client.UpdatedObj.Object, "spec", "http") + assert.NoError(t, err) + assert.Equal(t, true, ok) + + rule, ok := rules[0].(map[string]interface{}) + assert.Equal(t, true, ok) + priority, ok, err := unstructured.NestedInt64(rule, "priority") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, int64(2), priority) + }) + + t.Run("SetHeaderRouteExprsWithNew", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsGetNotFoundError: true, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{ + { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }, + { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Regex: "value", + }, + }, { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Prefix: "value", + }, + }}, + }) + + // Then + assert.NoError(t, err) + rules, ok, err := unstructured.NestedSlice(client.CreatedObj.Object, "spec", "http") + assert.NoError(t, err) + assert.Equal(t, true, ok) + + rule, ok := rules[0].(map[string]interface{}) + assert.Equal(t, true, ok) + exprs, ok, err := unstructured.NestedSlice(rule, "match", "exprs") + assert.NoError(t, err) + assert.Equal(t, true, ok) + values := [][]string{ + {"Equal", HeaderName, "Header", "value"}, + {"RegexMatch", HeaderName, "Header", "value"}, + {"RegexMatch", HeaderName, "Header", fmt.Sprintf("^%s.*", "value")}, + } + for i, expr := range exprs { + assertExpr(t, expr, values[i][0], values[i][1], values[i][2], values[i][3]) + } + }) + t.Run("SetHeaderRouteExprs", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsGetNotFoundError: false, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + + // When + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{ + { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }, + { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Regex: "value", + }, + }, { + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Prefix: "value", + }, + }}, + }) + + // Then + assert.NoError(t, err) + rules, ok, err := unstructured.NestedSlice(client.UpdatedObj.Object, "spec", "http") + assert.NoError(t, err) + assert.Equal(t, true, ok) + + rule, ok := rules[0].(map[string]interface{}) + assert.Equal(t, true, ok) + exprs, ok, err := unstructured.NestedSlice(rule, "match", "exprs") + assert.NoError(t, err) + assert.Equal(t, true, ok) + values := [][]string{ + {"Equal", HeaderName, "Header", "value"}, + {"RegexMatch", HeaderName, "Header", "value"}, + {"RegexMatch", HeaderName, "Header", fmt.Sprintf("^%s.*", "value")}, + } + for i, expr := range exprs { + assertExpr(t, expr, values[i][0], values[i][1], values[i][2], values[i][3]) + } + }) + t.Run("SetHeaderDeleteError", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsDeleteError: true, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + Recorder: &mocks.FakeRecorder{}, + } + r := NewReconciler(&cfg) + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: nil, + }) + assert.Error(t, err) + }) + t.Run("SetHeaderCreateError", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsCreateError: true, + IsGetNotFoundError: true, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + Recorder: &mocks.FakeRecorder{}, + } + r := NewReconciler(&cfg) + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{{ + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }}, + }) + assert.Error(t, err) }) + t.Run("SetHeaderUpdateError", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + UpdateError: true, + IsGetNotFoundError: false, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + Recorder: &mocks.FakeRecorder{}, + } + r := NewReconciler(&cfg) + err := r.SetHeaderRoute(&v1alpha1.SetHeaderRoute{ + Name: SetHeaderRouteName, + Match: []v1alpha1.HeaderRoutingMatch{{ + HeaderName: HeaderName, + HeaderValue: &v1alpha1.StringMatch{ + Exact: "value", + }, + }}, + }) + assert.Error(t, err) + }) + t.Run("RemoveManagedRoutesDeleteError", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsDeleteError: true, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + Recorder: &mocks.FakeRecorder{}, + } + r := NewReconciler(&cfg) + err := r.RemoveManagedRoutes() + assert.Error(t, err) + }) + t.Run("RemoveManagedRoutesNilManagedRoutes", func(t *testing.T) { + // Given + t.Parallel() + client := &mocks.FakeClient{ + IsDeleteError: true, + } + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + Recorder: &mocks.FakeRecorder{}, + } + cfg.Rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes = nil + r := NewReconciler(&cfg) + err := r.RemoveManagedRoutes() + assert.NoError(t, err) + }) +} + +func assertExpr(t *testing.T, expr interface{}, op, name, scope, value string) { + if expr == nil { + assert.Error(t, errors.New("expr is nil")) + } + typedExpr, ok := expr.(map[string]interface{}) + assert.Equal(t, true, ok) + + opAct, ok, err := unstructured.NestedString(typedExpr, "op") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, op, opAct) + + nameAct, ok, err := unstructured.NestedString(typedExpr, "subject", "name") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, name, nameAct) + + scopeAct, ok, err := unstructured.NestedString(typedExpr, "subject", "scope") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, scope, scopeAct) + + valueAct, ok, err := unstructured.NestedString(typedExpr, "value") + assert.NoError(t, err) + assert.Equal(t, true, ok) + assert.Equal(t, value, valueAct) } func TestSetMirrorRoute(t *testing.T) { @@ -352,6 +819,58 @@ func TestSetMirrorRoute(t *testing.T) { }) } +func TestRemoveManagedRoutes(t *testing.T) { + mocks.SetHeaderApisixRouteObj = toUnstructured(t, apisixSetHeaderRoute) + mocks.ApisixRouteObj = toUnstructured(t, apisixRoute) + mocks.DuplicateSetHeaderApisixRouteObj = toUnstructured(t, apisixSetHeaderDuplicateRoute) + t.Run("RemoveManagedRoutes", func(t *testing.T) { + client := &mocks.FakeClient{} + // Given + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + err := r.RemoveManagedRoutes() + // Then + assert.NoError(t, err) + assert.Equal(t, SetHeaderRouteName, client.DeleteName) + }) + t.Run("RemoveManagedRoutesError", func(t *testing.T) { + client := &mocks.FakeClient{ + IsGetManagedRouteError: true, + } + // Given + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + err := r.RemoveManagedRoutes() + // Then + assert.Error(t, err) + assert.Equal(t, "", client.DeleteName) + }) + t.Run("RemoveManagedRoutesNotFound", func(t *testing.T) { + client := &mocks.FakeClient{ + IsGetNotFoundError: true, + } + // Given + t.Parallel() + cfg := ReconcilerConfig{ + Rollout: newRollout(stableServiceName, canaryServiceName, apisixRouteName), + Client: client, + } + r := NewReconciler(&cfg) + err := r.RemoveManagedRoutes() + // Then + assert.NoError(t, err) + assert.Equal(t, "", client.DeleteName) + }) +} + func toUnstructured(t *testing.T, manifest string) *unstructured.Unstructured { t.Helper() obj := &unstructured.Unstructured{} @@ -407,6 +926,7 @@ func newRollout(stableSvc, canarySvc, apisixRouteRef string) *v1alpha1.Rollout { ObjectMeta: metav1.ObjectMeta{ Name: "rollout", Namespace: "default", + UID: "1a2b2d82-50a4-4d83-9ff4-cdc6f5197d30", }, Spec: v1alpha1.RolloutSpec{ Strategy: v1alpha1.RolloutStrategy{ @@ -419,6 +939,11 @@ func newRollout(stableSvc, canarySvc, apisixRouteRef string) *v1alpha1.Rollout { Name: apisixRouteRef, }, }, + ManagedRoutes: []v1alpha1.MangedRoutes{ + { + Name: SetHeaderRouteName, + }, + }, }, }, }, diff --git a/rollout/trafficrouting/apisix/mocks/apisix.go b/rollout/trafficrouting/apisix/mocks/apisix.go index 77204364fd..5931fb456a 100644 --- a/rollout/trafficrouting/apisix/mocks/apisix.go +++ b/rollout/trafficrouting/apisix/mocks/apisix.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pkg/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" argoRecord "github.com/argoproj/argo-rollouts/utils/record" @@ -21,17 +22,27 @@ type FakeDynamicClient struct { } type FakeClient struct { - IsGetError bool - IsGetErrorManifest bool - UpdateError bool - IsListError bool + IsGetError bool + IsGetErrorManifest bool + UpdateError bool + IsListError bool + IsGetNotFoundError bool + IsGetManagedRouteError bool + IsDuplicateSetHeaderRouteError bool + IsDeleteError bool + IsCreateError bool + DeleteName string + UpdatedObj *unstructured.Unstructured + CreatedObj *unstructured.Unstructured } type FakeRecorder struct{} var ( - ApisixRouteObj *unstructured.Unstructured - ErrorApisixRouteObj *unstructured.Unstructured + ApisixRouteObj *unstructured.Unstructured + SetHeaderApisixRouteObj *unstructured.Unstructured + DuplicateSetHeaderApisixRouteObj *unstructured.Unstructured + ErrorApisixRouteObj *unstructured.Unstructured ) func (f *FakeRecorder) Eventf(object runtime.Object, opts argoRecord.EventOptions, messageFmt string, args ...interface{}) { @@ -45,20 +56,39 @@ func (f *FakeRecorder) K8sRecorder() record.EventRecorder { } func (f *FakeClient) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { + if f.IsCreateError { + return nil, errors.New("create apisix route error!") + } + f.CreatedObj = obj return nil, nil } func (f *FakeClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { - if f.IsGetError { - return ApisixRouteObj, errors.New("Apisix get error") - } - if f.IsGetErrorManifest { - return ErrorApisixRouteObj, nil + if name == "mocks-apisix-route" { + if f.IsGetError { + return ApisixRouteObj, errors.New("Apisix get error") + } + if f.IsGetErrorManifest { + return ErrorApisixRouteObj, nil + } + return ApisixRouteObj, nil + } else if name == "set-header" { + if f.IsGetNotFoundError { + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "set-header") + } + if f.IsGetManagedRouteError { + return nil, errors.New("") + } + if f.IsDuplicateSetHeaderRouteError { + return DuplicateSetHeaderApisixRouteObj, nil + } + return SetHeaderApisixRouteObj, nil } - return ApisixRouteObj, nil + return nil, nil } func (f *FakeClient) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { + f.UpdatedObj = obj if f.UpdateError { return obj, errors.New("Apisix update error") } @@ -70,6 +100,10 @@ func (f *FakeClient) UpdateStatus(ctx context.Context, obj *unstructured.Unstruc } func (f *FakeClient) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { + if f.IsDeleteError { + return errors.New("delete apisixroute error!") + } + f.DeleteName = name return nil } diff --git a/test/e2e/apisix/rollout-apisix-canary-set-header.yaml b/test/e2e/apisix/rollout-apisix-canary-set-header.yaml new file mode 100644 index 0000000000..d9d675c7f5 --- /dev/null +++ b/test/e2e/apisix/rollout-apisix-canary-set-header.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: Service +metadata: + name: rollout-apisix-canary-canary +spec: + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: rollout-apisix-canary + # This selector will be updated with the pod-template-hash of the canary ReplicaSet. e.g.: + # rollouts-pod-template-hash: 7bf84f9696 +--- +apiVersion: v1 +kind: Service +metadata: + name: rollout-apisix-canary-stable +spec: + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: rollout-apisix-canary + # This selector will be updated with the pod-template-hash of the stable ReplicaSet. e.g.: + # rollouts-pod-template-hash: 789746c88d +--- +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: rollouts-apisix-route +spec: + http: + - name: rollouts-apisix + match: + paths: + - /* + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + hosts: + - rollouts-demo.apisix.local + backends: + - serviceName: rollout-apisix-canary-stable + servicePort: 80 + weight: 100 + - serviceName: rollout-apisix-canary-canary + servicePort: 80 + weight: 0 +--- +apiVersion: argoproj.io/v1alpha1 +kind: Rollout +metadata: + name: rollout-apisix-canary + namespace: default +spec: + replicas: 1 + strategy: + canary: + canaryService: rollout-apisix-canary-canary + stableService: rollout-apisix-canary-stable + trafficRouting: + managedRoutes: + - name: set-header + apisix: + route: + name: rollouts-apisix-route + rules: + - rollouts-apisix + steps: + - setCanaryScale: + replicas: 1 + setHeaderRoute: + match: + - headerName: trace + headerValue: + exact: debug + name: set-header + pause: + duration: 15 + - pause: + duration: 15 + - setWeight: 5 + - pause: + duration: 15 + - setWeight: 50 + - pause: + duration: 15 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: rollout-apisix-canary + template: + metadata: + labels: + app: rollout-apisix-canary + spec: + containers: + - name: rollout-apisix-canary + image: nginx:1.19-alpine + ports: + - name: http + containerPort: 80 + protocol: TCP + resources: + requests: + memory: 16Mi + cpu: 5m diff --git a/test/e2e/apisix_test.go b/test/e2e/apisix_test.go index d9a344aee9..1c8d2de3a3 100644 --- a/test/e2e/apisix_test.go +++ b/test/e2e/apisix_test.go @@ -73,6 +73,53 @@ func (s *APISIXSuite) TestAPISIXCanaryStep() { ExpectRevisionPodCount("1", 1) // don't scale down old replicaset since it will be within scaleDownDelay } +func (s *APISIXSuite) TestAPISIXCanarySetHeaderStep() { + + s.Given(). + RolloutObjects("@apisix/rollout-apisix-canary-set-header.yaml"). + When(). + ApplyManifests(). + WaitForRolloutStatus("Healthy"). + Then(). + Assert(func(t *fixtures.Then) { + s.check(t, 100, 0) + }). + ExpectExperimentCount(0). + When(). + UpdateSpec(). + WaitForRolloutCanaryStepIndex(0). + Sleep(5*time.Second). + Then(). + Assert(func(t *fixtures.Then) { + s.checkSetHeader(t, 0, 0) + }). + ExpectExperimentCount(0). + When(). + WaitForRolloutCanaryStepIndex(3). + Sleep(5*time.Second). + Then(). + Assert(func(t *fixtures.Then) { + s.check(t, 95, 5) + }). + ExpectExperimentCount(0). + When(). + WaitForRolloutCanaryStepIndex(5). + Sleep(3*time.Second). + Then(). + Assert(func(t *fixtures.Then) { + s.check(t, 50, 50) + }). + When(). + PromoteRollout(). + WaitForRolloutStatus("Healthy"). + Sleep(1*time.Second). // stable is currently set first, and then changes made to VirtualServices/DestinationRules + Then(). + Assert(func(t *fixtures.Then) { + s.check(t, 100, 0) + }). + ExpectRevisionPodCount("1", 1) // don't scale down old replicaset since it will be within scaleDownDelay +} + func (s *APISIXSuite) check(t *fixtures.Then, stableWeight int64, canaryWeight int64) { ar := t.GetApisixRoute() assert.NotEmpty(s.T(), ar) @@ -106,3 +153,45 @@ func (s *APISIXSuite) check(t *fixtures.Then, stableWeight int64, canaryWeight i } } } + +func (s *APISIXSuite) checkSetHeader(t *fixtures.Then, stableWeight int64, canaryWeight int64) { + + ar := t.GetApisixSetHeaderRoute() + assert.NotEmpty(s.T(), ar) + apisixHttpRoutesObj, isFound, err := unstructured.NestedSlice(ar.Object, "spec", "http") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + assert.Equal(s.T(), "set-header", ar.GetName()) + apisixHttpRouteObj, err := a6.GetHttpRoute(apisixHttpRoutesObj, apisixRouteName) + assert.NoError(s.T(), err) + + exprs, isFound, err := unstructured.NestedSlice(apisixHttpRouteObj.(map[string]interface{}), "match", "exprs") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + + assert.Equal(s.T(), 1, len(exprs)) + expr := exprs[0] + + exprObj, ok := expr.(map[string]interface{}) + assert.Equal(s.T(), ok, true) + + op, isFound, err := unstructured.NestedString(exprObj, "op") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + assert.Equal(s.T(), "Equal", op) + + name, isFound, err := unstructured.NestedString(exprObj, "subject", "name") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + assert.Equal(s.T(), "trace", name) + + scope, isFound, err := unstructured.NestedString(exprObj, "subject", "scope") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + assert.Equal(s.T(), "Header", scope) + + value, isFound, err := unstructured.NestedString(exprObj, "value") + assert.NoError(s.T(), err) + assert.Equal(s.T(), isFound, true) + assert.Equal(s.T(), "debug", value) +} diff --git a/test/fixtures/common.go b/test/fixtures/common.go index c40715f7bc..6ca0619d59 100644 --- a/test/fixtures/common.go +++ b/test/fixtures/common.go @@ -582,6 +582,21 @@ func (c *Common) GetApisixRoute() *unstructured.Unstructured { return a6Route } +func (c *Common) GetApisixSetHeaderRoute() *unstructured.Unstructured { + ctx := context.TODO() + rollout, err := c.rolloutClient.ArgoprojV1alpha1().Rollouts(c.Rollout().GetNamespace()).Get(ctx, c.Rollout().GetName(), metav1.GetOptions{}) + c.CheckError(err) + dyClient := a6util.NewDynamicClient(c.dynamicClient, c.Rollout().GetNamespace()) + index := *rollout.Status.CurrentStepIndex + if step := rollout.Spec.Strategy.Canary.Steps[index]; step.SetHeaderRoute != nil { + name := step.SetHeaderRoute.Name + a6Route, err := dyClient.Get(ctx, name, metav1.GetOptions{}) + c.CheckError(err) + return a6Route + } + return nil +} + func (c *Common) GetAppMeshVirtualRouter() *unstructured.Unstructured { ro := c.Rollout() ctx := context.TODO()