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 b25bae86 fix: retract configuration when a referenced 
ApisixPluginConfig is gone (#2859)
b25bae86 is described below

commit b25bae86d5b380a149c658c50a9e0a114849a440
Author: AlinsRan <[email protected]>
AuthorDate: Thu Sep 10 14:29:53 2026 +0800

    fix: retract configuration when a referenced ApisixPluginConfig is gone 
(#2859)
---
 internal/controller/apisixroute_controller.go    |  26 ++-
 internal/controller/ingress_controller.go        |  18 ++
 internal/controller/pluginconfig_retract_test.go | 261 +++++++++++++++++++++++
 internal/types/error.go                          |  23 ++
 test/e2e/crds/v2/pluginconfig.go                 |  85 ++++++++
 test/e2e/ingress/annotations.go                  |  85 ++++++++
 6 files changed, 495 insertions(+), 3 deletions(-)

diff --git a/internal/controller/apisixroute_controller.go 
b/internal/controller/apisixroute_controller.go
index 8902802f..cd21eeb3 100644
--- a/internal/controller/apisixroute_controller.go
+++ b/internal/controller/apisixroute_controller.go
@@ -146,6 +146,21 @@ func (r *ApisixRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request)
                return ctrl.Result{}, client.IgnoreNotFound(err)
        }
        if err = r.processApisixRoute(tctx, &ar); err != nil {
+               // A reference the route needs is gone, so the route can no 
longer be
+               // translated. Retract what an earlier reconcile published: the 
store is what
+               // every sync pushes, so leaving it in place keeps the data 
plane serving the
+               // last good configuration while the status says the spec is 
invalid.
+               if types.IsDependencyMissing(err) {
+                       if derr := r.Provider.Delete(ctx, &ar); derr != nil {
+                               r.Log.Error(derr, "failed to delete 
apisixroute", "apisixroute", utils.NamespacedName(&ar))
+                               return ctrl.Result{}, derr
+                       }
+                       // err is the local variable the deferred updateStatus 
reads, so the
+                       // status still reports the reason. Returning it as 
well would requeue
+                       // forever with backoff: the reference does not come 
back on its own, and
+                       // the ApisixPluginConfig watch reconciles the route 
again when it does.
+                       return ctrl.Result{}, nil
+               }
                return ctrl.Result{}, err
        }
        if err = r.Provider.Update(ctx, tctx, &ar); err != nil {
@@ -274,10 +289,15 @@ func (r *ApisixRouteReconciler) validatePluginConfig(tctx 
*provider.TranslateCon
                pcNN = utils.NamespacedName(&pc)
        )
        if err := r.Get(tctx, pcNN, &pc); err != nil {
-               return types.ReasonError{
-                       Reason:  string(apiv2.ConditionReasonInvalidSpec),
-                       Message: fmt.Sprintf("failed to get ApisixPluginConfig: 
%s", pcNN),
+               if !k8serrors.IsNotFound(err) {
+                       // A read failure is transient: retry it rather than 
reporting the
+                       // reference as invalid and retracting the route.
+                       return err
                }
+               return types.DependencyMissingError{Err: types.ReasonError{
+                       Reason:  string(apiv2.ConditionReasonInvalidSpec),
+                       Message: fmt.Sprintf("ApisixPluginConfig not found: 
%s", pcNN),
+               }}
        }
 
        // Check if ApisixPluginConfig has IngressClassName and if it matches
diff --git a/internal/controller/ingress_controller.go 
b/internal/controller/ingress_controller.go
index f019076f..92eb6f7a 100644
--- a/internal/controller/ingress_controller.go
+++ b/internal/controller/ingress_controller.go
@@ -27,6 +27,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"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/types"
@@ -192,6 +193,20 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, 
req ctrl.Request) (ct
        // process plugin config annotation
        if err := r.processPluginConfig(tctx, ingress); err != nil {
                r.Log.Error(err, "failed to process PluginConfig annotation", 
"ingress", ingress.Name)
+               // The referenced ApisixPluginConfig is gone, so the Ingress 
can no longer be
+               // translated. Retract what an earlier reconcile published: the 
store is what
+               // every sync pushes, so leaving it in place keeps the data 
plane applying the
+               // deleted plugin configuration.
+               if internaltypes.IsDependencyMissing(err) {
+                       if derr := r.Provider.Delete(ctx, ingress); derr != nil 
{
+                               r.Log.Error(derr, "failed to delete ingress", 
"ingress", utils.NamespacedName(ingress))
+                               return ctrl.Result{}, derr
+                       }
+                       // Requeueing would retry forever with backoff for a 
reference that does
+                       // not come back on its own; the ApisixPluginConfig 
watch reconciles the
+                       // Ingress again when it does.
+                       return ctrl.Result{}, nil
+               }
                return ctrl.Result{}, err
        }
 
@@ -628,6 +643,9 @@ func (r *IngressReconciler) processPluginConfig(tctx 
*provider.TranslateContext,
 
        if err := r.Get(tctx, pcNN, &pc); err != nil {
                r.Log.Error(err, "failed to get ApisixPluginConfig", 
"pluginconfig", pcNN)
+               if k8serrors.IsNotFound(err) {
+                       return internaltypes.DependencyMissingError{Err: err}
+               }
                return err
        }
 
diff --git a/internal/controller/pluginconfig_retract_test.go 
b/internal/controller/pluginconfig_retract_test.go
new file mode 100644
index 00000000..5a9f959b
--- /dev/null
+++ b/internal/controller/pluginconfig_retract_test.go
@@ -0,0 +1,261 @@
+// 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 controller
+
+import (
+       "context"
+       "errors"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       networkingv1 "k8s.io/api/networking/v1"
+       k8serrors "k8s.io/apimachinery/pkg/api/errors"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       "k8s.io/apimachinery/pkg/util/intstr"
+       clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+       "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/manager/readiness"
+)
+
+const (
+       retractPluginConfigNamespace = "default"
+       retractPluginConfigName      = "shared"
+)
+
+func retractPluginConfigScheme(t *testing.T) *runtime.Scheme {
+       t.Helper()
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, apiv2.AddToScheme(scheme))
+       require.NoError(t, v1alpha1.AddToScheme(scheme))
+       return scheme
+}
+
+func retractIngressClass() *networkingv1.IngressClass {
+       return &networkingv1.IngressClass{
+               ObjectMeta: metav1.ObjectMeta{Name: "apisix"},
+               Spec:       networkingv1.IngressClassSpec{Controller: 
config.GetControllerName()},
+       }
+}
+
+func newRetractReadier(t *testing.T, cli client.Client) 
readiness.ReadinessManager {
+       t.Helper()
+       readier := readiness.NewReadinessManager(cli, logr.Discard())
+       require.NoError(t, readier.Start(context.Background()))
+       return readier
+}
+
+// failGetOn makes Get fail with a non-NotFound error for objects of type T, 
so a
+// transient read failure can be told apart from an absent reference.
+func failGetOn[T client.Object]() interceptor.Funcs {
+       return interceptor.Funcs{
+               Get: func(ctx context.Context, cli client.WithWatch, key 
client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
+                       if _, ok := obj.(T); ok {
+                               return 
k8serrors.NewInternalError(errors.New("boom"))
+                       }
+                       return cli.Get(ctx, key, obj, opts...)
+               },
+       }
+}
+
+func newApisixRoutePluginConfigFixture(
+       t *testing.T,
+       interceptorFuncs interceptor.Funcs,
+       extraObjects ...client.Object,
+) (*ApisixRouteReconciler, *recordingProvider, *recordingUpdater) {
+       t.Helper()
+
+       scheme := retractPluginConfigScheme(t)
+       route := &apiv2.ApisixRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: 
retractPluginConfigNamespace, Name: "route"},
+               Spec: apiv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
+                       HTTP: []apiv2.ApisixRouteHTTP{{
+                               Name:             "rule",
+                               PluginConfigName: retractPluginConfigName,
+                               Match:            
apiv2.ApisixRouteHTTPMatch{Hosts: []string{"repro.test"}, Paths: 
[]string{"/*"}},
+                               Backends: []apiv2.ApisixRouteHTTPBackend{{
+                                       ServiceName: "backend",
+                                       ServicePort: intstr.FromInt32(80),
+                               }},
+                       }},
+               },
+       }
+
+       objects := append([]client.Object{retractIngressClass(), route}, 
extraObjects...)
+       cli := fake.NewClientBuilder().WithScheme(scheme).
+               WithObjects(objects...).
+               WithStatusSubresource(route).
+               WithInterceptorFuncs(interceptorFuncs).
+               Build()
+
+       prov := &recordingProvider{}
+       updater := &recordingUpdater{}
+       return &ApisixRouteReconciler{
+               Client:   cli,
+               Scheme:   scheme,
+               Log:      logr.Discard(),
+               Provider: prov,
+               Updater:  updater,
+               Readier:  newRetractReadier(t, cli),
+       }, prov, updater
+}
+
+func newIngressPluginConfigFixture(
+       t *testing.T,
+       interceptorFuncs interceptor.Funcs,
+       extraObjects ...client.Object,
+) (*IngressReconciler, *recordingProvider) {
+       t.Helper()
+
+       scheme := retractPluginConfigScheme(t)
+       ingress := &networkingv1.Ingress{
+               ObjectMeta: metav1.ObjectMeta{
+                       Namespace:   retractPluginConfigNamespace,
+                       Name:        "ing",
+                       Annotations: 
map[string]string{annotations.AnnotationsPluginConfigName: 
retractPluginConfigName},
+               },
+               Spec: networkingv1.IngressSpec{IngressClassName: 
ptrTo("apisix")},
+       }
+
+       objects := append([]client.Object{retractIngressClass(), ingress}, 
extraObjects...)
+       cli := fake.NewClientBuilder().WithScheme(scheme).
+               WithObjects(objects...).
+               WithStatusSubresource(ingress).
+               WithInterceptorFuncs(interceptorFuncs).
+               // The reconcile lists HTTPRoutePolicies by target, which the 
fake client
+               // only serves once the index exists. No policy is under test 
here.
+               WithIndex(&v1alpha1.HTTPRoutePolicy{}, indexer.PolicyTargetRefs,
+                       func(client.Object) []string { return nil }).
+               Build()
+
+       prov := &recordingProvider{}
+       return &IngressReconciler{
+               Client:   cli,
+               Scheme:   scheme,
+               Log:      logr.Discard(),
+               Provider: prov,
+               Updater:  &recordingUpdater{},
+               Readier:  newRetractReadier(t, cli),
+       }, prov
+}
+
+func ptrTo[T any](v T) *T { return &v }
+
+var (
+       retractApisixRouteKey = k8stypes.NamespacedName{Namespace: 
retractPluginConfigNamespace, Name: "route"}
+       retractIngressKey     = k8stypes.NamespacedName{Namespace: 
retractPluginConfigNamespace, Name: "ing"}
+)
+
+// Deleting a shared ApisixPluginConfig leaves the referencing ApisixRoute in 
place
+// but untranslatable. Its published configuration must be retracted, 
otherwise the
+// data plane keeps applying the deleted plugins while the status reports the 
spec
+// as invalid, and only deleting the route itself clears it.
+func TestApisixRouteReconcile_RetractsWhenPluginConfigIsMissing(t *testing.T) {
+       r, prov, updater := newApisixRoutePluginConfigFixture(t, 
interceptor.Funcs{})
+
+       result, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractApisixRouteKey})
+
+       // No error and no requeue: the reference does not come back on its 
own, so
+       // retrying it forever with backoff only produces log noise. The
+       // ApisixPluginConfig watch reconciles the route again when it returns.
+       require.NoError(t, err)
+       assert.Equal(t, ctrl.Result{}, result)
+       assert.Equal(t, []k8stypes.NamespacedName{retractApisixRouteKey}, 
prov.deleted)
+       assert.Zero(t, prov.updated)
+
+       // Retracting without saying why would be the same disagreement the 
other way
+       // round. The reason travels on the error the deferred updateStatus 
reads, which
+       // returning nil must not disturb.
+       require.Len(t, updater.updates, 1)
+       mutated, ok := 
updater.updates[0].Mutator.Mutate(&apiv2.ApisixRoute{}).(*apiv2.ApisixRoute)
+       require.True(t, ok)
+       require.Len(t, mutated.Status.Conditions, 1)
+       accepted := mutated.Status.Conditions[0]
+       assert.Equal(t, string(apiv2.ConditionTypeAccepted), accepted.Type)
+       assert.Equal(t, metav1.ConditionFalse, accepted.Status)
+       assert.Equal(t, string(apiv2.ConditionReasonInvalidSpec), 
accepted.Reason)
+       assert.Contains(t, accepted.Message, "ApisixPluginConfig not found")
+}
+
+// A read failure that is not NotFound is transient. Retracting on it would 
drop a
+// working route because the API server hiccuped.
+func TestApisixRouteReconcile_KeepsRouteWhenPluginConfigReadFails(t 
*testing.T) {
+       r, prov, _ := newApisixRoutePluginConfigFixture(t, 
failGetOn[*apiv2.ApisixPluginConfig]())
+
+       _, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractApisixRouteKey})
+
+       require.Error(t, err)
+       assert.True(t, k8serrors.IsInternalError(err), "want the transient 
error to surface, got %v", err)
+       assert.Empty(t, prov.deleted, "a transient read failure must not 
retract the route")
+}
+
+// The same applies to an Ingress that names the plugin config through its
+// annotation.
+func TestIngressReconcile_RetractsWhenPluginConfigIsMissing(t *testing.T) {
+       r, prov := newIngressPluginConfigFixture(t, interceptor.Funcs{})
+
+       result, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractIngressKey})
+
+       require.NoError(t, err)
+       assert.Equal(t, ctrl.Result{}, result)
+       assert.Equal(t, []k8stypes.NamespacedName{retractIngressKey}, 
prov.deleted)
+       assert.Zero(t, prov.updated)
+}
+
+func TestIngressReconcile_KeepsIngressWhenPluginConfigReadFails(t *testing.T) {
+       r, prov := newIngressPluginConfigFixture(t, 
failGetOn[*apiv2.ApisixPluginConfig]())
+
+       _, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractIngressKey})
+
+       require.Error(t, err)
+       assert.True(t, k8serrors.IsInternalError(err), "want the transient 
error to surface, got %v", err)
+       assert.Empty(t, prov.deleted, "a transient read failure must not 
retract the Ingress")
+}
+
+// With the plugin config present the resources must still be published.
+func TestReconcile_PublishesWhenPluginConfigExists(t *testing.T) {
+       pc := &apiv2.ApisixPluginConfig{
+               ObjectMeta: metav1.ObjectMeta{Namespace: 
retractPluginConfigNamespace, Name: retractPluginConfigName},
+       }
+
+       ar, arProv, _ := newApisixRoutePluginConfigFixture(t, 
interceptor.Funcs{}, pc.DeepCopy())
+       _, err := ar.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractApisixRouteKey})
+       require.NoError(t, err)
+       assert.Empty(t, arProv.deleted)
+       assert.Equal(t, 1, arProv.updated)
+
+       ing, ingProv := newIngressPluginConfigFixture(t, interceptor.Funcs{}, 
pc.DeepCopy())
+       _, err = ing.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: retractIngressKey})
+       require.NoError(t, err)
+       assert.Empty(t, ingProv.deleted)
+       assert.Equal(t, 1, ingProv.updated)
+}
diff --git a/internal/types/error.go b/internal/types/error.go
index ace6992b..38f9ddba 100644
--- a/internal/types/error.go
+++ b/internal/types/error.go
@@ -37,6 +37,29 @@ func (e ReasonError) Error() string {
        return e.Message
 }
 
+// DependencyMissingError marks a validation failure caused by a referenced 
object
+// that is absent rather than temporarily unreadable. The distinction matters 
when
+// deciding what to do with configuration already published for the owner: an
+// absent reference will not come back on its own, so keeping the last good
+// configuration leaves the data plane contradicting the Accepted=False status
+// written alongside it, while a transient read failure must be retried 
instead.
+type DependencyMissingError struct {
+       Err error
+}
+
+func (e DependencyMissingError) Error() string {
+       return e.Err.Error()
+}
+
+func (e DependencyMissingError) Unwrap() error {
+       return e.Err
+}
+
+func IsDependencyMissing(err error) bool {
+       var dme DependencyMissingError
+       return errors.As(err, &dme)
+}
+
 func IsSomeReasonError[Reason ~string](err error, reasons ...Reason) bool {
        if err == nil {
                return false
diff --git a/test/e2e/crds/v2/pluginconfig.go b/test/e2e/crds/v2/pluginconfig.go
index 13867f1b..8112a1ef 100644
--- a/test/e2e/crds/v2/pluginconfig.go
+++ b/test/e2e/crds/v2/pluginconfig.go
@@ -115,6 +115,91 @@ spec:
                        Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusNotFound))
                })
 
+               It("Test ApisixRoute stops serving when its ApisixPluginConfig 
is deleted", func() {
+                       const pluginConfigSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixPluginConfig
+metadata:
+  name: shared-plugin-config
+spec:
+  ingressClassName: %s
+  plugins:
+  - name: response-rewrite
+    enable: true
+    config:
+      headers:
+        X-Revocation-Test: "must-disappear"
+`
+
+                       const routeSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: referencing-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      paths:
+      - /*
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    plugin_config_name: shared-plugin-config
+`
+
+                       applyPluginConfig := func() {
+                               var pluginConfig apiv2.ApisixPluginConfig
+                               
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: 
"shared-plugin-config"},
+                                       &pluginConfig, 
fmt.Sprintf(pluginConfigSpec, s.Namespace()))
+                       }
+
+                       By("apply ApisixPluginConfig and a route that 
references it")
+                       applyPluginConfig()
+                       var apisixRoute apiv2.ApisixRoute
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "referencing-route"},
+                               &apisixRoute, fmt.Sprintf(routeSpec, 
s.Namespace()))
+
+                       By("the plugin takes effect")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Revocation-Test", "must-disappear"),
+                               },
+                       })
+
+                       By("delete only the ApisixPluginConfig, leaving the 
route in place")
+                       Expect(s.DeleteResource("ApisixPluginConfig", 
"shared-plugin-config")).
+                               ShouldNot(HaveOccurred(), "deleting 
ApisixPluginConfig")
+
+                       By("the route stops being served")
+                       // Without the retraction the route keeps forwarding 
and keeps applying the
+                       // deleted plugin, so the header would still come back 
with a 200.
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:   "GET",
+                               Path:     "/get",
+                               Check:    
scaffold.WithExpectedStatus(http.StatusNotFound),
+                               Timeout:  time.Second * 30,
+                               Interval: time.Second * 2,
+                       })
+
+                       By("recreating the ApisixPluginConfig under the same 
name restores the route")
+                       applyPluginConfig()
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Revocation-Test", "must-disappear"),
+                               },
+                               Timeout:  time.Second * 30,
+                               Interval: time.Second * 2,
+                       })
+               })
+
                It("Test ApisixPluginConfig update", func() {
                        const apisixPluginConfigSpecV1 = `
 apiVersion: apisix.apache.org/v2
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 1a522c9b..622d0cb8 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -751,6 +751,91 @@ spec:
                        Expect(err).NotTo(HaveOccurred(), "unmarshalling echo 
plugin config")
                        Expect(echoConfig["body"]).To(Equal("hello from plugin 
config"), "checking echo plugin body")
                })
+               It("stops serving when the referenced ApisixPluginConfig is 
deleted", func() {
+                       pluginConfig := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixPluginConfig
+metadata:
+  name: revoked-plugin-config
+spec:
+  ingressClassName: %s
+  plugins:
+  - name: response-rewrite
+    enable: true
+    config:
+      headers:
+        X-Revocation-Test: "must-disappear"
+`
+                       ingressWithPluginConfig := `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: revoked-plugin-config-test
+  annotations:
+    k8s.apisix.apache.org/plugin-config-name: "revoked-plugin-config"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: revoked-plugin-config.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+                       applyPluginConfig := func() {
+                               
Expect(s.CreateResourceFromString(fmt.Sprintf(pluginConfig, s.Namespace()))).
+                                       ShouldNot(HaveOccurred(), "creating 
ApisixPluginConfig")
+                       }
+
+                       applyPluginConfig()
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressWithPluginConfig, 
s.Namespace()))).
+                               ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "revoked-plugin-config.example",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Revocation-Test", "must-disappear"),
+                               },
+                       })
+
+                       By("delete only the ApisixPluginConfig, leaving the 
Ingress in place")
+                       Expect(s.DeleteResource("ApisixPluginConfig", 
"revoked-plugin-config")).
+                               ShouldNot(HaveOccurred(), "deleting 
ApisixPluginConfig")
+
+                       // Without the retraction the Ingress keeps forwarding 
and keeps applying
+                       // the deleted plugin, so the header would still come 
back with a 200.
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:   "GET",
+                               Path:     "/get",
+                               Host:     "revoked-plugin-config.example",
+                               Check:    
scaffold.WithExpectedStatus(http.StatusNotFound),
+                               Timeout:  time.Second * 30,
+                               Interval: time.Second * 2,
+                       })
+
+                       By("recreating it under the same name restores the 
Ingress")
+                       applyPluginConfig()
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "revoked-plugin-config.example",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Revocation-Test", "must-disappear"),
+                               },
+                               Timeout:  time.Second * 30,
+                               Interval: time.Second * 2,
+                       })
+               })
+
                It("methods", func() {
                        
Expect(s.CreateResourceFromString(fmt.Sprintf(allowMethods, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
                        
Expect(s.CreateResourceFromString(fmt.Sprintf(blockMethods, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")

Reply via email to