You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2025/07/25 01:05:16 UTC
(apisix-ingress-controller) branch master updated: fix: full sync during restart results in loss of dataplane traffic (#2489)
This is an automated email from the ASF dual-hosted git repository.
alinsran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 66c2b0ac fix: full sync during restart results in loss of dataplane traffic (#2489)
66c2b0ac is described below
commit 66c2b0acf14fc13f461e5f262753bdc0598f5d1a
Author: AlinsRan <al...@apache.org>
AuthorDate: Fri Jul 25 09:05:11 2025 +0800
fix: full sync during restart results in loss of dataplane traffic (#2489)
---
internal/controller/apisixconsumer_controller.go | 3 +
internal/controller/apisixglobalrule_controller.go | 4 +
internal/controller/apisixroute_controller.go | 3 +
internal/controller/apisixtls_controller.go | 3 +
internal/controller/consumer_controller.go | 31 +---
internal/controller/httproute_controller.go | 3 +
internal/controller/indexer/indexer.go | 22 ++-
internal/controller/ingress_controller.go | 3 +
internal/controller/status/updater.go | 4 +
internal/controller/utils.go | 35 ++++
internal/manager/controllers.go | 57 +++++-
internal/manager/readiness/manager.go | 196 +++++++++++++++++++++
internal/manager/run.go | 12 +-
internal/provider/adc/adc.go | 12 +-
internal/provider/provider.go | 1 +
internal/types/k8s.go | 86 ++++++++-
test/e2e/crds/v1alpha1/consumer.go | 86 +++++++++
test/e2e/crds/v2/route.go | 107 +++++++++++
test/e2e/gatewayapi/httproute.go | 81 +++++++++
19 files changed, 708 insertions(+), 41 deletions(-)
diff --git a/internal/controller/apisixconsumer_controller.go b/internal/controller/apisixconsumer_controller.go
index eb9b3c73..f9908b25 100644
--- a/internal/controller/apisixconsumer_controller.go
+++ b/internal/controller/apisixconsumer_controller.go
@@ -39,6 +39,7 @@ import (
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
@@ -51,10 +52,12 @@ type ApisixConsumerReconciler struct {
Provider provider.Provider
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting)
func (r *ApisixConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&apiv2.ApisixConsumer{}, req.NamespacedName)
r.Log.Info("reconcile", "request", req.NamespacedName)
ac := &apiv2.ApisixConsumer{}
diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go
index fc42b1fb..9431df67 100644
--- a/internal/controller/apisixglobalrule_controller.go
+++ b/internal/controller/apisixglobalrule_controller.go
@@ -38,6 +38,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/controller/config"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
@@ -49,10 +50,13 @@ type ApisixGlobalRuleReconciler struct {
Log logr.Logger
Provider provider.Provider
Updater status.Updater
+
+ Readier readiness.ReadinessManager
}
// Reconcile implements the reconciliation logic for ApisixGlobalRule
func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&apiv2.ApisixGlobalRule{}, req.NamespacedName)
var globalRule apiv2.ApisixGlobalRule
if err := r.Get(ctx, req.NamespacedName, &globalRule); err != nil {
if client.IgnoreNotFound(err) == nil {
diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go
index a07c6e76..c4dde0d5 100644
--- a/internal/controller/apisixroute_controller.go
+++ b/internal/controller/apisixroute_controller.go
@@ -44,6 +44,7 @@ import (
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/types"
"github.com/apache/apisix-ingress-controller/internal/utils"
@@ -57,6 +58,7 @@ type ApisixRouteReconciler struct {
Log logr.Logger
Provider provider.Provider
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// SetupWithManager sets up the controller with the Manager.
@@ -97,6 +99,7 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&apiv2.ApisixRoute{}, req.NamespacedName)
var ar apiv2.ApisixRoute
if err := r.Get(ctx, req.NamespacedName, &ar); err != nil {
if client.IgnoreNotFound(err) == nil {
diff --git a/internal/controller/apisixtls_controller.go b/internal/controller/apisixtls_controller.go
index 5cde26f6..7410fdb0 100644
--- a/internal/controller/apisixtls_controller.go
+++ b/internal/controller/apisixtls_controller.go
@@ -39,6 +39,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/controller/config"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
@@ -50,6 +51,7 @@ type ApisixTlsReconciler struct {
Log logr.Logger
Provider provider.Provider
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// SetupWithManager sets up the controller with the Manager.
@@ -85,6 +87,7 @@ func (r *ApisixTlsReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Reconcile implements the reconciliation logic for ApisixTls
func (r *ApisixTlsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&apiv2.ApisixTls{}, req.NamespacedName)
var tls apiv2.ApisixTls
if err := r.Get(ctx, req.NamespacedName, &tls); err != nil {
if client.IgnoreNotFound(err) == nil {
diff --git a/internal/controller/consumer_controller.go b/internal/controller/consumer_controller.go
index fd51f87a..7869ad6b 100644
--- a/internal/controller/consumer_controller.go
+++ b/internal/controller/consumer_controller.go
@@ -38,6 +38,7 @@ import (
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
@@ -51,6 +52,7 @@ type ConsumerReconciler struct { //nolint:revive
Provider provider.Provider
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// SetupWithManager sets up the controller with the Manager.
@@ -181,6 +183,7 @@ func (r *ConsumerReconciler) listConsumersForGatewayProxy(ctx context.Context, o
}
func (r *ConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&v1alpha1.Consumer{}, req.NamespacedName)
consumer := new(v1alpha1.Consumer)
if err := r.Get(ctx, req.NamespacedName, consumer); err != nil {
if client.IgnoreNotFound(err) == nil {
@@ -303,31 +306,5 @@ func (r *ConsumerReconciler) checkGatewayRef(object client.Object) bool {
if !ok {
return false
}
- if consumer.Spec.GatewayRef.Name == "" {
- return false
- }
- if consumer.Spec.GatewayRef.Kind != nil && *consumer.Spec.GatewayRef.Kind != KindGateway {
- return false
- }
- if consumer.Spec.GatewayRef.Group != nil && *consumer.Spec.GatewayRef.Group != gatewayv1.GroupName {
- return false
- }
- ns := consumer.GetNamespace()
- if consumer.Spec.GatewayRef.Namespace != nil {
- ns = *consumer.Spec.GatewayRef.Namespace
- }
- gateway := &gatewayv1.Gateway{}
- if err := r.Get(context.Background(), client.ObjectKey{
- Name: consumer.Spec.GatewayRef.Name,
- Namespace: ns,
- }, gateway); err != nil {
- r.Log.Error(err, "failed to get gateway", "gateway", consumer.Spec.GatewayRef.Name)
- return false
- }
- gatewayClass := &gatewayv1.GatewayClass{}
- if err := r.Get(context.Background(), client.ObjectKey{Name: string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil {
- r.Log.Error(err, "failed to get gateway class", "gateway", gateway.GetName(), "gatewayclass", gateway.Spec.GatewayClassName)
- return false
- }
- return matchesController(string(gatewayClass.Spec.ControllerName))
+ return MatchConsumerGatewayRef(context.Background(), r.Client, r.Log, consumer)
}
diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go
index 50b6e3b2..b6f39287 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -48,6 +48,7 @@ import (
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/types"
"github.com/apache/apisix-ingress-controller/internal/utils"
@@ -65,6 +66,7 @@ type HTTPRouteReconciler struct { //nolint:revive
genericEvent chan event.GenericEvent
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// SetupWithManager sets up the controller with the Manager.
@@ -130,6 +132,7 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&gatewayv1.HTTPRoute{}, req.NamespacedName)
hr := new(gatewayv1.HTTPRoute)
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
if client.IgnoreNotFound(err) == nil {
diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go
index 2f1b7574..e621ac10 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -47,6 +47,7 @@ const (
GatewayClassIndexRef = "gatewayClassRef"
ApisixUpstreamRef = "apisixUpstreamRef"
PluginConfigIndexRef = "pluginConfigRefs"
+ ControllerName = "controllerName"
)
func SetupIndexer(mgr ctrl.Manager) error {
@@ -59,11 +60,11 @@ func SetupIndexer(mgr ctrl.Manager) error {
setupIngressClassIndexer,
setupGatewayProxyIndexer,
setupGatewaySecretIndex,
- setupGatewayClassIndexer,
setupApisixRouteIndexer,
setupApisixPluginConfigIndexer,
setupApisixTlsIndexer,
setupApisixConsumerIndexer,
+ setupGatewayClassIndexer,
} {
if err := setup(mgr); err != nil {
return err
@@ -81,6 +82,17 @@ func setupGatewayIndexer(mgr ctrl.Manager) error {
); err != nil {
return err
}
+
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &gatewayv1.Gateway{},
+ GatewayClassIndexRef,
+ func(obj client.Object) (requests []string) {
+ return []string{string(obj.(*gatewayv1.Gateway).Spec.GatewayClassName)}
+ },
+ ); err != nil {
+ return err
+ }
return nil
}
@@ -273,10 +285,10 @@ func setupGatewaySecretIndex(mgr ctrl.Manager) error {
func setupGatewayClassIndexer(mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(
context.Background(),
- &gatewayv1.Gateway{},
- GatewayClassIndexRef,
- func(obj client.Object) (requests []string) {
- return []string{string(obj.(*gatewayv1.Gateway).Spec.GatewayClassName)}
+ &gatewayv1.GatewayClass{},
+ ControllerName,
+ func(obj client.Object) []string {
+ return []string{string(obj.(*gatewayv1.GatewayClass).Spec.ControllerName)}
},
)
}
diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go
index 86c4d7ff..25b52990 100644
--- a/internal/controller/ingress_controller.go
+++ b/internal/controller/ingress_controller.go
@@ -45,6 +45,7 @@ import (
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
@@ -59,6 +60,7 @@ type IngressReconciler struct { //nolint:revive
genericEvent chan event.GenericEvent
Updater status.Updater
+ Readier readiness.ReadinessManager
}
// SetupWithManager sets up the controller with the Manager.
@@ -117,6 +119,7 @@ func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Reconcile handles the reconciliation of Ingress resources
func (r *IngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ defer r.Readier.Done(&networkingv1.Ingress{}, req.NamespacedName)
ingress := new(networkingv1.Ingress)
if err := r.Get(ctx, req.NamespacedName, ingress); err != nil {
if client.IgnoreNotFound(err) == nil {
diff --git a/internal/controller/status/updater.go b/internal/controller/status/updater.go
index 019c1846..d00aec76 100644
--- a/internal/controller/status/updater.go
+++ b/internal/controller/status/updater.go
@@ -150,6 +150,10 @@ func (u *UpdateHandler) Start(ctx context.Context) error {
}
}
+func (u *UpdateHandler) NeedsLeaderElection() bool {
+ return true
+}
+
func (u *UpdateHandler) Writer() Updater {
return &UpdateWriter{
updateChannel: u.updateChannel,
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index 81a44843..0917cf81 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -35,6 +35,7 @@ import (
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
networkingv1 "k8s.io/api/networking/v1"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@@ -1448,3 +1449,37 @@ func TypePredicate[T client.Object]() func(obj client.Object) bool {
return ok
}
}
+
+func MatchConsumerGatewayRef(ctx context.Context, c client.Client, log logr.Logger, consumer *v1alpha1.Consumer) bool {
+ if consumer.Spec.GatewayRef.Name == "" {
+ return false
+ }
+ if consumer.Spec.GatewayRef.Kind != nil && *consumer.Spec.GatewayRef.Kind != KindGateway {
+ return false
+ }
+ if consumer.Spec.GatewayRef.Group != nil && *consumer.Spec.GatewayRef.Group != gatewayv1.GroupName {
+ return false
+ }
+ ns := consumer.GetNamespace()
+ if consumer.Spec.GatewayRef.Namespace != nil {
+ ns = *consumer.Spec.GatewayRef.Namespace
+ }
+ gateway := &gatewayv1.Gateway{}
+ if err := c.Get(context.Background(), client.ObjectKey{
+ Name: consumer.Spec.GatewayRef.Name,
+ Namespace: ns,
+ }, gateway); err != nil {
+ if !k8serrors.IsNotFound(err) {
+ log.Error(err, "failed to get gateway", "gateway", consumer.Spec.GatewayRef.Name)
+ }
+ return false
+ }
+ gatewayClass := &gatewayv1.GatewayClass{}
+ if err := c.Get(context.Background(), client.ObjectKey{Name: string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil {
+ if !k8serrors.IsNotFound(err) {
+ log.Error(err, "failed to get gateway class", "gateway", gateway.GetName(), "gatewayclass", gateway.Spec.GatewayClassName)
+ }
+ return false
+ }
+ return matchesController(string(gatewayClass.Spec.ControllerName))
+}
diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go
index 2af8cd6a..0688055d 100644
--- a/internal/manager/controllers.go
+++ b/internal/manager/controllers.go
@@ -20,13 +20,23 @@ package manager
import (
"context"
+ netv1 "k8s.io/api/networking/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
+ types "github.com/apache/apisix-ingress-controller/internal/types"
)
// K8s
@@ -83,7 +93,7 @@ type Controller interface {
SetupWithManager(mgr manager.Manager) error
}
-func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Provider, updater status.Updater) ([]Controller, error) {
+func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Provider, updater status.Updater, readier readiness.ReadinessManager) ([]Controller, error) {
if err := indexer.SetupIndexer(mgr); err != nil {
return nil, err
}
@@ -107,6 +117,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("HTTPRoute"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.IngressReconciler{
Client: mgr.GetClient(),
@@ -114,6 +125,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("Ingress"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.ConsumerReconciler{
Client: mgr.GetClient(),
@@ -121,6 +133,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("Consumer"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.IngressClassReconciler{
Client: mgr.GetClient(),
@@ -134,6 +147,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixGlobalRule"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.ApisixRouteReconciler{
Client: mgr.GetClient(),
@@ -141,6 +155,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.ApisixConsumerReconciler{
Client: mgr.GetClient(),
@@ -148,6 +163,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixConsumer"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.ApisixPluginConfigReconciler{
Client: mgr.GetClient(),
@@ -161,6 +177,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixTls"),
Provider: pro,
Updater: updater,
+ Readier: readier,
},
&controller.ApisixUpstreamReconciler{
Client: mgr.GetClient(),
@@ -176,3 +193,41 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro
},
}, nil
}
+
+func registerReadinessGVK(c client.Client, readier readiness.ReadinessManager) {
+ log := ctrl.LoggerFrom(context.Background()).WithName("readiness")
+ readier.RegisterGVK([]readiness.GVKConfig{
+ {
+ GVKs: []schema.GroupVersionKind{
+ types.GvkOf(&gatewayv1.HTTPRoute{}),
+ },
+ },
+ {
+ GVKs: []schema.GroupVersionKind{
+ types.GvkOf(&netv1.Ingress{}),
+ types.GvkOf(&apiv2.ApisixRoute{}),
+ types.GvkOf(&apiv2.ApisixGlobalRule{}),
+ types.GvkOf(&apiv2.ApisixPluginConfig{}),
+ types.GvkOf(&apiv2.ApisixTls{}),
+ types.GvkOf(&apiv2.ApisixConsumer{}),
+ },
+ Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool {
+ icName, _, _ := unstructured.NestedString(obj.Object, "spec", "ingressClassName")
+ ingressClass, _ := controller.GetIngressClass(context.Background(), c, log, icName)
+ return ingressClass != nil
+ }),
+ },
+ {
+ GVKs: []schema.GroupVersionKind{
+ types.GvkOf(&v1alpha1.Consumer{}),
+ },
+ Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool {
+ consumer := &v1alpha1.Consumer{}
+ if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, consumer); err != nil {
+ return false
+ }
+ return controller.MatchConsumerGatewayRef(context.Background(), c, log, consumer)
+ }),
+ },
+ }...)
+}
diff --git a/internal/manager/readiness/manager.go b/internal/manager/readiness/manager.go
new file mode 100644
index 00000000..7ba4b83b
--- /dev/null
+++ b/internal/manager/readiness/manager.go
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package readiness
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/api7/gopkg/pkg/log"
+ "go.uber.org/zap"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ k8stypes "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ types "github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+// Filter defines an interface to match unstructured Kubernetes objects.
+type Filter interface {
+ Match(obj *unstructured.Unstructured) bool
+}
+
+// GVKFilter is a functional implementation of Filter using a function type.
+type GVKFilter func(obj *unstructured.Unstructured) bool
+
+func (f GVKFilter) Match(obj *unstructured.Unstructured) bool {
+ return f(obj)
+}
+
+// GVKConfig defines a set of GVKs and an optional filter to match the objects.
+type GVKConfig struct {
+ GVKs []schema.GroupVersionKind
+ Filter Filter
+}
+
+// readinessManager prevents premature full sync to the data plane on controller startup.
+//
+// Background:
+// On startup, the controller watches CRDs and periodically performs full sync to the data plane.
+// If a sync occurs before all resources have been reconciled, it may push incomplete data,
+// causing traffic disruption.
+//
+// This manager tracks whether all relevant resources have been processed at least once.
+// It is used to delay full sync until initial reconciliation is complete.
+type ReadinessManager interface {
+ RegisterGVK(configs ...GVKConfig)
+ Start(ctx context.Context) error
+ IsReady() bool
+ WaitReady(ctx context.Context, timeout time.Duration) bool
+ Done(obj client.Object, namespacedName k8stypes.NamespacedName)
+}
+
+type readinessManager struct {
+ client client.Client
+ configs []GVKConfig
+ state map[schema.GroupVersionKind]map[k8stypes.NamespacedName]struct{}
+ mu sync.RWMutex
+ startOnce sync.Once
+ started chan struct{}
+ done chan struct{}
+
+ isReady atomic.Bool
+}
+
+// ReadinessManager tracks readiness of specific resources across the cluster.
+func NewReadinessManager(client client.Client) ReadinessManager {
+ return &readinessManager{
+ client: client,
+ state: make(map[schema.GroupVersionKind]map[k8stypes.NamespacedName]struct{}),
+ started: make(chan struct{}),
+ done: make(chan struct{}),
+ isReady: atomic.Bool{},
+ }
+}
+
+// RegisterGVK registers one or more GVKConfig objects for readiness tracking.
+func (r *readinessManager) RegisterGVK(configs ...GVKConfig) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.configs = append(r.configs, configs...)
+}
+
+// Start initializes the readiness state from the Kubernetes API.
+// Should be called only after informer cache has synced.
+func (r *readinessManager) Start(ctx context.Context) error {
+ var err error
+ r.startOnce.Do(func() {
+ for _, cfg := range r.configs {
+ for _, gvk := range cfg.GVKs {
+ uList := &unstructured.UnstructuredList{}
+ uList.SetGroupVersionKind(gvk)
+ if listErr := r.client.List(ctx, uList); listErr != nil {
+ err = fmt.Errorf("list %s failed: %w", gvk.String(), listErr)
+ return
+ }
+ var expected []k8stypes.NamespacedName
+ for _, item := range uList.Items {
+ if cfg.Filter != nil && !cfg.Filter.Match(&item) {
+ continue
+ }
+ expected = append(expected, k8stypes.NamespacedName{
+ Namespace: item.GetNamespace(),
+ Name: item.GetName(),
+ })
+ }
+ if len(expected) > 0 {
+ log.Debugw("registering readiness state", zap.Any("gvk", gvk), zap.Any("expected", expected))
+ r.registerState(gvk, expected)
+ }
+ }
+ }
+ close(r.started)
+ if len(r.state) == 0 && !r.isReady.Load() {
+ r.isReady.Store(true)
+ close(r.done)
+ }
+ })
+ return err
+}
+
+func (r *readinessManager) registerState(gvk schema.GroupVersionKind, list []k8stypes.NamespacedName) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if _, ok := r.state[gvk]; !ok {
+ r.state[gvk] = make(map[k8stypes.NamespacedName]struct{})
+ }
+ for _, name := range list {
+ r.state[gvk][name] = struct{}{}
+ }
+}
+
+// Done marks the resource as ready by removing it from the pending state.
+func (r *readinessManager) Done(obj client.Object, nn k8stypes.NamespacedName) {
+ if r.IsReady() {
+ return
+ }
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ gvk := types.GvkOf(obj)
+ if _, ok := r.state[gvk]; !ok {
+ return
+ }
+ delete(r.state[gvk], nn)
+ if len(r.state[gvk]) == 0 {
+ delete(r.state, gvk)
+ }
+ if len(r.state) == 0 && !r.isReady.Load() {
+ r.isReady.Store(true)
+ close(r.done)
+ }
+}
+
+func (r *readinessManager) IsReady() bool {
+ return r.isReady.Load()
+}
+
+// WaitReady blocks until readiness is achieved, a timeout occurs, or context is cancelled.
+func (r *readinessManager) WaitReady(ctx context.Context, timeout time.Duration) bool {
+ if r.IsReady() {
+ return true
+ }
+
+ select {
+ case <-r.started:
+ case <-ctx.Done():
+ return false
+ }
+
+ select {
+ case <-ctx.Done():
+ return false
+ case <-time.After(timeout):
+ return true
+ case <-r.done:
+ return true
+ }
+}
diff --git a/internal/manager/run.go b/internal/manager/run.go
index a27b2732..b75b383d 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -41,6 +41,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/controller"
"github.com/apache/apisix-ingress-controller/internal/controller/config"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider/adc"
_ "github.com/apache/apisix-ingress-controller/pkg/metrics"
)
@@ -150,13 +151,20 @@ func Run(ctx context.Context, logger logr.Logger) error {
return err
}
+ readier := readiness.NewReadinessManager(mgr.GetClient())
+ registerReadinessGVK(mgr.GetClient(), readier)
+
+ if err := mgr.Add(readier); err != nil {
+ setupLog.Error(err, "unable to add readiness manager")
+ }
+
updater := status.NewStatusUpdateHandler(ctrl.LoggerFrom(ctx).WithName("status").WithName("updater"), mgr.GetClient())
if err := mgr.Add(updater); err != nil {
setupLog.Error(err, "unable to add status updater")
return err
}
- provider, err := adc.New(updater.Writer(), &adc.Options{
+ provider, err := adc.New(updater.Writer(), readier, &adc.Options{
SyncTimeout: config.ControllerConfig.ExecADCTimeout.Duration,
SyncPeriod: config.ControllerConfig.ProviderConfig.SyncPeriod.Duration,
InitSyncDelay: config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration,
@@ -184,7 +192,7 @@ func Run(ctx context.Context, logger logr.Logger) error {
controller.SetEnableReferenceGrant(err == nil)
setupLog.Info("setting up controllers")
- controllers, err := setupControllers(ctx, mgr, provider, updater.Writer())
+ controllers, err := setupControllers(ctx, mgr, provider, updater.Writer(), readier)
if err != nil {
setupLog.Error(err, "unable to set up controllers")
return err
diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go
index a109dcbb..5ff00761 100644
--- a/internal/provider/adc/adc.go
+++ b/internal/provider/adc/adc.go
@@ -38,6 +38,7 @@ import (
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller/label"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/provider/adc/translator"
"github.com/apache/apisix-ingress-controller/internal/types"
@@ -93,6 +94,8 @@ type adcClient struct {
updater status.Updater
statusUpdateMap map[types.NamespacedNameKind][]string
+ readier readiness.ReadinessManager
+
syncCh chan struct{}
}
@@ -104,7 +107,7 @@ type Task struct {
configs []adcConfig
}
-func New(updater status.Updater, opts ...Option) (provider.Provider, error) {
+func New(updater status.Updater, readier readiness.ReadinessManager, opts ...Option) (provider.Provider, error) {
o := Options{}
o.ApplyOptions(opts)
@@ -116,6 +119,7 @@ func New(updater status.Updater, opts ...Option) (provider.Provider, error) {
store: NewStore(),
executor: &DefaultADCExecutor{},
updater: updater,
+ readier: readier,
syncCh: make(chan struct{}, 1),
}, nil
}
@@ -302,6 +306,8 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error {
}
func (d *adcClient) Start(ctx context.Context) error {
+ d.readier.WaitReady(ctx, 5*time.Minute)
+
initalSyncDelay := d.InitSyncDelay
if initalSyncDelay > 0 {
time.AfterFunc(initalSyncDelay, func() {
@@ -478,3 +484,7 @@ func (d *adcClient) handleADCExecutionErrors(statusesMap map[string]types.ADCExe
statusUpdateMap := d.resolveADCExecutionErrors(statusesMap)
d.handleStatusUpdate(statusUpdateMap)
}
+
+func (d *adcClient) NeedLeaderElection() bool {
+ return true
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index b06899fb..d9b6cbfa 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -37,6 +37,7 @@ type Provider interface {
Delete(context.Context, client.Object) error
Sync(context.Context) error
Start(context.Context) error
+ NeedLeaderElection() bool
}
type TranslateContext struct {
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index a115562e..d83158fe 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -18,11 +18,13 @@
package types
import (
- "github.com/apache/apisix-ingress-controller/api/v1alpha1"
- v2 "github.com/apache/apisix-ingress-controller/api/v2"
corev1 "k8s.io/api/core/v1"
- v1 "k8s.io/api/networking/v1"
+ netv1 "k8s.io/api/networking/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ v2 "github.com/apache/apisix-ingress-controller/api/v2"
)
const DefaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
@@ -56,9 +58,9 @@ func KindOf(obj any) string {
return KindHTTPRoute
case *gatewayv1.GatewayClass:
return KindGatewayClass
- case *v1.Ingress:
+ case *netv1.Ingress:
return KindIngress
- case *v1.IngressClass:
+ case *netv1.IngressClass:
return KindIngressClass
case *corev1.Secret:
return KindSecret
@@ -90,3 +92,77 @@ func KindOf(obj any) string {
return "Unknown"
}
}
+
+func GvkOf(obj any) schema.GroupVersionKind {
+ kind := KindOf(obj)
+ switch obj.(type) {
+ case *gatewayv1.Gateway, *gatewayv1.HTTPRoute, *gatewayv1.GatewayClass:
+ return gatewayv1.SchemeGroupVersion.WithKind(kind)
+ case *netv1.Ingress, *netv1.IngressClass:
+ return netv1.SchemeGroupVersion.WithKind(kind)
+ case *corev1.Secret, *corev1.Service:
+ return corev1.SchemeGroupVersion.WithKind(kind)
+ case *v2.ApisixRoute:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v2",
+ Kind: KindApisixRoute,
+ }
+ case *v2.ApisixGlobalRule:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v2",
+ Kind: KindApisixGlobalRule,
+ }
+ case *v2.ApisixPluginConfig:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v2",
+ Kind: KindApisixPluginConfig,
+ }
+ case *v2.ApisixTls:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v2",
+ Kind: KindApisixTls,
+ }
+ case *v2.ApisixConsumer:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v2",
+ Kind: KindApisixConsumer,
+ }
+ case *v1alpha1.HTTPRoutePolicy:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v1alpha1",
+ Kind: KindHTTPRoutePolicy,
+ }
+ case *v1alpha1.BackendTrafficPolicy:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v1alpha1",
+ Kind: KindBackendTrafficPolicy,
+ }
+ case *v1alpha1.GatewayProxy:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v1alpha1",
+ Kind: KindGatewayProxy,
+ }
+ case *v1alpha1.Consumer:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v1alpha1",
+ Kind: KindConsumer,
+ }
+ case *v1alpha1.PluginConfig:
+ return schema.GroupVersionKind{
+ Group: "apisix.apache.org",
+ Version: "v1alpha1",
+ Kind: KindPluginConfig,
+ }
+ default:
+ return schema.GroupVersionKind{}
+ }
+}
diff --git a/test/e2e/crds/v1alpha1/consumer.go b/test/e2e/crds/v1alpha1/consumer.go
index d2db2a5b..daa092cd 100644
--- a/test/e2e/crds/v1alpha1/consumer.go
+++ b/test/e2e/crds/v1alpha1/consumer.go
@@ -579,4 +579,90 @@ spec:
})
})
})
+
+ Context("Test Consumer sync during startup", func() {
+ var consumer1 = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+ name: consumer-sample
+spec:
+ gatewayRef:
+ name: apisix
+ credentials:
+ - type: key-auth
+ name: key-auth-sample
+ config:
+ key: sample-key
+`
+ var consumer2 = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+ name: consumer-unused
+spec:
+ gatewayRef:
+ name: apisix-non-existent
+ credentials:
+ - type: key-auth
+ name: key-auth-sample
+ config:
+ key: sample-key2
+`
+
+ BeforeEach(func() {
+ s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute)
+ })
+
+ It("Should sync Consumer during startup", func() {
+ Expect(s.CreateResourceFromString(consumer2)).NotTo(HaveOccurred(), "creating unused consumer")
+ s.ResourceApplied("Consumer", "consumer-sample", consumer1, 1)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Headers: map[string]string{
+ "apikey": "sample-key",
+ },
+ Check: scaffold.WithExpectedStatus(200),
+ })
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Headers: map[string]string{
+ "apikey": "sample-key2",
+ },
+ Check: scaffold.WithExpectedStatus(401),
+ })
+
+ By("restarting the controller and dataplane")
+ s.Deployer.ScaleIngress(0)
+ s.Deployer.ScaleDataplane(0)
+ s.Deployer.ScaleDataplane(1)
+ s.Deployer.ScaleIngress(1)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Headers: map[string]string{
+ "apikey": "sample-key",
+ },
+ Check: scaffold.WithExpectedStatus(200),
+ })
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Headers: map[string]string{
+ "apikey": "sample-key2",
+ },
+ Check: scaffold.WithExpectedStatus(401),
+ })
+ })
+ })
})
diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go
index ce1b9864..78d14636 100644
--- a/test/e2e/crds/v2/route.go
+++ b/test/e2e/crds/v2/route.go
@@ -650,4 +650,111 @@ spec:
Expect(err).ShouldNot(HaveOccurred(), "check apisixupstream is referenced")
})
})
+
+ Context("Test ApisixRoute sync during startup", func() {
+ const route = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: default
+spec:
+ ingressClassName: apisix
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin
+ paths:
+ - /get
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+`
+
+ const route2 = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: route2
+spec:
+ ingressClassName: apisix-nonexistent
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin2
+ paths:
+ - /get
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+`
+ const route3 = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: route3
+spec:
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin3
+ paths:
+ - /get
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+`
+ It("Should sync ApisixRoute during startup", func() {
+ By("apply ApisixRoute")
+ Expect(s.CreateResourceFromString(route2)).ShouldNot(HaveOccurred(), "apply ApisixRoute with nonexistent ingressClassName")
+ Expect(s.CreateResourceFromString(route3)).ShouldNot(HaveOccurred(), "apply ApisixRoute without ingressClassName")
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, route)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin2",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin3",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+
+ By("restart controller and dataplane")
+ s.Deployer.ScaleIngress(0)
+ s.Deployer.ScaleDataplane(0)
+ s.Deployer.ScaleDataplane(1)
+ s.Deployer.ScaleIngress(1)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin2",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin3",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+ })
+ })
})
diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go
index daba664e..f4012aa1 100644
--- a/test/e2e/gatewayapi/httproute.go
+++ b/test/e2e/gatewayapi/httproute.go
@@ -1900,4 +1900,85 @@ spec:
}
})
})
+
+ Context("Test HTTPRoute sync during startup", func() {
+ BeforeEach(beforeEachHTTP)
+ var route = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: httpbin
+spec:
+ parentRefs:
+ - name: apisix
+ hostnames:
+ - httpbin
+ rules:
+ - matches:
+ - path:
+ type: Exact
+ value: /get
+ backendRefs:
+ - name: httpbin-service-e2e-test
+ port: 80
+`
+
+ var route2 = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: httpbin2
+spec:
+ parentRefs:
+ - name: apisix-nonexistent
+ hostnames:
+ - httpbin2
+ rules:
+ - matches:
+ - path:
+ type: Exact
+ value: /get
+ backendRefs:
+ - name: httpbin-service-e2e-test
+ port: 80
+`
+ It("Should sync ApisixRoute during startup", func() {
+ By("apply ApisixRoute")
+ Expect(s.CreateResourceFromString(route2)).ShouldNot(HaveOccurred(), "applying HTTPRoute with non-existent parent")
+ s.ResourceApplied("HTTPRoute", "httpbin", route, 1)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin2",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+
+ By("restart controller and dataplane")
+ s.Deployer.ScaleIngress(0)
+ s.Deployer.ScaleDataplane(0)
+ s.Deployer.ScaleDataplane(1)
+ s.Deployer.ScaleIngress(1)
+
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin2",
+ Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+ })
+ })
+
+ })
})