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")