This is an automated email from the ASF dual-hosted git repository.

bzp2010 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 9c4146cc feat(apisix): attribute a GatewayProxy's global rules and 
plugin metadata to itself (#2885)
9c4146cc is described below

commit 9c4146cc3df13458ccde742f9a945d10da678c9b
Author: Zeping Bai <[email protected]>
AuthorDate: Mon Sep 21 15:16:32 2026 +0800

    feat(apisix): attribute a GatewayProxy's global rules and plugin metadata 
to itself (#2885)
---
 internal/adc/cache/store.go               |  13 +++-
 internal/adc/cache/store_test.go          |   6 +-
 internal/provider/apisix/provider.go      | 107 ++++++++++++++++++++++--------
 internal/provider/apisix/provider_test.go |  70 +++++++++++++++++++
 4 files changed, 163 insertions(+), 33 deletions(-)

diff --git a/internal/adc/cache/store.go b/internal/adc/cache/store.go
index fb4e5192..708b8565 100644
--- a/internal/adc/cache/store.go
+++ b/internal/adc/cache/store.go
@@ -232,6 +232,9 @@ func (s *Store) Insert(name string, resourceTypes []string, 
resources *adctypes.
        return nil
 }
 
+// Delete removes the services, consumers, ssls and global_rules the resource 
identified
+// by Labels contributes to the cacheKey name. An empty resourceTypes deletes 
nothing;
+// see DeleteAll for wiping a whole cacheKey.
 func (s *Store) Delete(name string, resourceTypes []string, Labels 
map[string]string) error {
        s.Lock()
        defer s.Unlock()
@@ -286,12 +289,16 @@ func (s *Store) Delete(name string, resourceTypes 
[]string, Labels map[string]st
                        }
                }
        }
-       if len(resourceTypes) == 0 {
-               delete(s.cacheMap, name)
-       }
        return nil
 }
 
+// DeleteAll wipes everything the cacheKey name holds.
+func (s *Store) DeleteAll(name string) {
+       s.Lock()
+       defer s.Unlock()
+       delete(s.cacheMap, name)
+}
+
 func (s *Store) GetResources(name string) (*adctypes.Resources, error) {
        s.Lock()
        defer s.Unlock()
diff --git a/internal/adc/cache/store_test.go b/internal/adc/cache/store_test.go
index 1fdd46b7..f0e02f83 100644
--- a/internal/adc/cache/store_test.go
+++ b/internal/adc/cache/store_test.go
@@ -186,13 +186,17 @@ func TestInsertForgetsTheOwnerOfReplacedResources(t 
*testing.T) {
        assert.False(t, ok)
 }
 
-func TestDeleteWithoutResourceTypesForgetsEveryOwnerToo(t *testing.T) {
+func TestDeleteWithoutResourceTypesDeletesNothing(t *testing.T) {
        route := ownerNamed(types.KindApisixRoute, "route")
        s := NewStore(logr.Discard())
        require.NoError(t, s.Insert(configName, []string{adctypes.TypeService}, 
&adctypes.Resources{Services: []*adctypes.Service{service("svc", route)}}, 
labelsOf(route)))
 
        require.NoError(t, s.Delete(configName, nil, nil))
        _, ok := s.Lookup(configName, adctypes.TypeService, "svc")
+       assert.True(t, ok, "an empty resourceTypes deletes nothing; see 
DeleteAll for wiping a whole cacheKey")
+
+       s.DeleteAll(configName)
+       _, ok = s.Lookup(configName, adctypes.TypeService, "svc")
        assert.False(t, ok)
 }
 
diff --git a/internal/provider/apisix/provider.go 
b/internal/provider/apisix/provider.go
index 4f375c7c..403d1528 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -56,6 +56,19 @@ const (
        MinSyncPeriod = 1 * time.Second
 )
 
+// pluginSource says whose global_rules and plugin_metadata a translate result 
carries.
+type pluginSource int
+
+const (
+       pluginsNone pluginSource = iota
+       // pluginsFromGatewayProxy is GatewayProxy.Spec.Plugins and 
.PluginMetadata, owned by
+       // each target config's GatewayProxy rather than the Gateway or 
IngressClass that was
+       // reconciled.
+       pluginsFromGatewayProxy
+       // pluginsFromResource is the reconciled resource's own global rules.
+       pluginsFromResource
+)
+
 // apisixProvider owns AIC's own view of what should be live: which Kubernetes 
resource
 // targets which GatewayProxy config (configManager) and the merged, 
translated resource
 // snapshot per config (store). It builds the input the adc client package 
needs and hands
@@ -135,6 +148,7 @@ func (d *apisixProvider) Update(ctx context.Context, tctx 
*provider.TranslateCon
        var (
                result        *translator.TranslateResult
                resourceTypes []string
+               plugins       pluginSource
                err           error
        )
 
@@ -158,7 +172,8 @@ func (d *apisixProvider) Update(ctx context.Context, tctx 
*provider.TranslateCon
                resourceTypes = append(resourceTypes, adctypes.TypeService)
        case *gatewayv1.Gateway:
                result, err = d.translator.TranslateGateway(tctx, t.DeepCopy())
-               resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule, 
adctypes.TypeSSL, adctypes.TypePluginMetadata)
+               resourceTypes = append(resourceTypes, adctypes.TypeSSL)
+               plugins = pluginsFromGatewayProxy
        case *networkingv1.Ingress:
                result, err = d.translator.TranslateIngress(tctx, t.DeepCopy())
                resourceTypes = append(resourceTypes, adctypes.TypeService, 
adctypes.TypeSSL)
@@ -167,13 +182,13 @@ func (d *apisixProvider) Update(ctx context.Context, tctx 
*provider.TranslateCon
                resourceTypes = append(resourceTypes, adctypes.TypeConsumer)
        case *networkingv1.IngressClass:
                result, err = d.translator.TranslateIngressClass(tctx, 
t.DeepCopy())
-               resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule, 
adctypes.TypePluginMetadata)
+               plugins = pluginsFromGatewayProxy
        case *apiv2.ApisixRoute:
                result, err = d.translator.TranslateApisixRoute(tctx, 
t.DeepCopy())
                resourceTypes = append(resourceTypes, adctypes.TypeService)
        case *apiv2.ApisixGlobalRule:
                result, err = d.translator.TranslateApisixGlobalRule(tctx, 
t.DeepCopy())
-               resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule)
+               plugins = pluginsFromResource
        case *apiv2.ApisixTls:
                result, err = d.translator.TranslateApisixTls(tctx, 
t.DeepCopy())
                resourceTypes = append(resourceTypes, adctypes.TypeSSL)
@@ -211,30 +226,34 @@ func (d *apisixProvider) Update(ctx context.Context, tctx 
*provider.TranslateCon
        labels := label.GenLabel(obj)
        d.log.V(1).Info("updating config", "resourceKey", rk, "configs", 
configs, "resourceTypes", resourceTypes)
 
-       return d.applyResourceState(rk, configs, resourceTypes, resources, 
labels)
+       return d.applyResourceState(rk, configs, resourceTypes, resources, 
labels, plugins)
 }
 
 func (d *apisixProvider) Delete(ctx context.Context, obj client.Object) error {
        d.log.V(1).Info("deleting object", "object", 
utils.NamespacedNameKind(obj))
 
-       var resourceTypes []string
-       var labels map[string]string
+       var (
+               resourceTypes []string
+               labels        map[string]string
+               plugins       pluginSource
+               // wholeConfig is set for a Gateway or IngressClass, whose 
deletion wipes every
+               // config it referenced.
+               wholeConfig bool
+       )
        switch obj.(type) {
        case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute, *gatewayv1.GRPCRoute, 
*gatewayv1.TCPRoute, *gatewayv1.UDPRoute, *gatewayv1.TLSRoute:
                resourceTypes = append(resourceTypes, adctypes.TypeService)
                labels = label.GenLabel(obj)
-       case *gatewayv1.Gateway:
-               // delete all resources
+       case *gatewayv1.Gateway, *networkingv1.IngressClass:
+               wholeConfig = true
        case *networkingv1.Ingress:
                resourceTypes = append(resourceTypes, adctypes.TypeService, 
adctypes.TypeSSL)
                labels = label.GenLabel(obj)
        case *v1alpha1.Consumer:
                resourceTypes = append(resourceTypes, adctypes.TypeConsumer)
                labels = label.GenLabel(obj)
-       case *networkingv1.IngressClass:
-               // delete all resources
        case *apiv2.ApisixGlobalRule:
-               resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule)
+               plugins = pluginsFromResource
                labels = label.GenLabel(obj)
        case *apiv2.ApisixTls:
                resourceTypes = append(resourceTypes, adctypes.TypeSSL)
@@ -245,22 +264,17 @@ func (d *apisixProvider) Delete(ctx context.Context, obj 
client.Object) error {
        }
        nnk := utils.NamespacedNameKind(obj)
 
+       removed, err := d.removeResourceState(nnk, resourceTypes, labels, 
plugins, wholeConfig)
+       if err != nil {
+               return err
+       }
        // Full synchronization is performed on a gateway by gateway basis
        // and it is not possible to perform scheduled synchronization
        // on deleted gateway level resources
-       if len(resourceTypes) == 0 {
-               removed, err := d.removeResourceState(nnk, resourceTypes, 
labels)
-               if err != nil {
-                       return err
-               }
+       if wholeConfig {
                d.syncEvictedConfigsNow(ctx, removed, resourceTypes, labels)
                return nil
        }
-
-       removed, err := d.removeResourceState(nnk, resourceTypes, labels)
-       if err != nil {
-               return err
-       }
        // Syncing pushes the whole store to every data plane. Objects this 
controller never
        // configured delete nothing, and reconciles for them are frequent, so 
notify only
        // when the store actually changed.
@@ -279,17 +293,35 @@ func (d *apisixProvider) applyResourceState(
        resourceTypes []string,
        resources *adctypes.Resources,
        labels map[string]string,
+       plugins pluginSource,
 ) error {
        d.Lock()
        defer d.Unlock()
 
        evicted := d.configManager.Update(rk, configs)
-       if err := d.evictFromStore(evicted, resourceTypes, labels); err != nil {
+       if err := d.evictFromStore(rk, evicted, resourceTypes, labels, 
plugins); err != nil {
                return err
        }
-       for _, cfg := range configs {
-               if err := d.store.Insert(cfg.Name, resourceTypes, resources, 
labels); err != nil {
-                       return fmt.Errorf("store insert failed for config %s: 
%w", cfg.Name, err)
+       for gatewayProxy, cfg := range configs {
+               if len(resourceTypes) > 0 {
+                       if err := d.store.Insert(cfg.Name, resourceTypes, 
resources, labels); err != nil {
+                               return fmt.Errorf("store insert failed for 
config %s: %w", cfg.Name, err)
+                       }
+               }
+               switch plugins {
+               case pluginsNone:
+                       // Most resource kinds carry no global_rules or 
plugin_metadata at all.
+               case pluginsFromGatewayProxy:
+                       if err := d.store.SetGlobalRules(cfg.Name, 
gatewayProxy, resources.GlobalRules); err != nil {
+                               return fmt.Errorf("store global rules failed 
for config %s: %w", cfg.Name, err)
+                       }
+                       if err := d.store.SetPluginMetadata(cfg.Name, 
resources.PluginMetadata); err != nil {
+                               return fmt.Errorf("store plugin metadata failed 
for config %s: %w", cfg.Name, err)
+                       }
+               case pluginsFromResource:
+                       if err := d.store.SetGlobalRules(cfg.Name, rk, 
resources.GlobalRules); err != nil {
+                               return fmt.Errorf("store global rules failed 
for config %s: %w", cfg.Name, err)
+                       }
                }
        }
        return nil
@@ -297,34 +329,51 @@ func (d *apisixProvider) applyResourceState(
 
 // removeResourceState forgets a resource's config associations and evicts its 
contribution
 // from each config it used to reference, returning those configs so an 
immediate-push
-// caller (see syncEvictedConfigsNow) knows what to push right away.
+// caller (see syncEvictedConfigsNow) knows what to push right away. 
wholeConfig wipes each
+// of those configs entirely instead.
 func (d *apisixProvider) removeResourceState(
        rk types.NamespacedNameKind,
        resourceTypes []string,
        labels map[string]string,
+       plugins pluginSource,
+       wholeConfig bool,
 ) (map[types.NamespacedNameKind]adctypes.Config, error) {
        d.Lock()
        defer d.Unlock()
 
        evicted := d.configManager.Get(rk)
        d.configManager.Delete(rk)
-       if err := d.evictFromStore(evicted, resourceTypes, labels); err != nil {
+       if wholeConfig {
+               for _, cfg := range evicted {
+                       d.store.DeleteAll(cfg.Name)
+               }
+               return evicted, nil
+       }
+       if err := d.evictFromStore(rk, evicted, resourceTypes, labels, 
plugins); err != nil {
                return nil, err
        }
        return evicted, nil
 }
 
-// evictFromStore deletes a resource's contribution from each of the given 
configs' cached
-// snapshots. Callers must already hold d.Lock.
+// evictFromStore deletes rk's contribution from each of the given configs' 
cached
+// snapshots. global_rules and plugin_metadata sourced from a GatewayProxy 
belong to that
+// GatewayProxy's own config and are left in place. Callers must already hold 
d.Lock.
 func (d *apisixProvider) evictFromStore(
+       rk types.NamespacedNameKind,
        configs map[types.NamespacedNameKind]adctypes.Config,
        resourceTypes []string,
        labels map[string]string,
+       plugins pluginSource,
 ) error {
        for _, cfg := range configs {
                if err := d.store.Delete(cfg.Name, resourceTypes, labels); err 
!= nil {
                        return fmt.Errorf("store delete failed for config %s: 
%w", cfg.Name, err)
                }
+               if plugins == pluginsFromResource {
+                       if err := d.store.SetGlobalRules(cfg.Name, rk, nil); 
err != nil {
+                               return fmt.Errorf("store global rules failed 
for config %s: %w", cfg.Name, err)
+                       }
+               }
        }
        return nil
 }
diff --git a/internal/provider/apisix/provider_test.go 
b/internal/provider/apisix/provider_test.go
index 96be3e52..01412d10 100644
--- a/internal/provider/apisix/provider_test.go
+++ b/internal/provider/apisix/provider_test.go
@@ -38,6 +38,7 @@ import (
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/adc/cache"
        adcclient 
"github.com/apache/apisix-ingress-controller/internal/adc/client"
+       "github.com/apache/apisix-ingress-controller/internal/controller/label"
        "github.com/apache/apisix-ingress-controller/internal/provider/common"
        "github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
@@ -69,6 +70,14 @@ func newTestProvider(t *testing.T) *apisixProvider {
        }
 }
 
+func labelsOf(owner types.NamespacedNameKind) map[string]string {
+       return map[string]string{
+               label.LabelKind:      owner.Kind,
+               label.LabelNamespace: owner.Namespace,
+               label.LabelName:      owner.Name,
+       }
+}
+
 // TestDeleteNotifiesSyncOnlyWhenConfigWasRemoved covers the cost side of route
 // ownership: a sync pushes the whole store to every data plane, and reconciles
 // for routes this controller never configured are frequent (any EndpointSlice
@@ -207,3 +216,64 @@ func TestSyncStillPushesHealthyConfigsWhenAnotherFails(t 
*testing.T) {
        assert.True(t, seen["bad"], "the failing config must still have been 
attempted")
        assert.True(t, seen["good"], "a config failing must not stop the others 
from being pushed")
 }
+
+func TestApplyResourceStateAttributesGatewayProxyPluginsToTheGatewayProxy(t 
*testing.T) {
+       d := newTestProvider(t)
+       gw1 := types.NamespacedNameKind{Kind: types.KindGateway, Namespace: 
"ns", Name: "gw1"}
+       gw2 := types.NamespacedNameKind{Kind: types.KindGateway, Namespace: 
"ns", Name: "gw2"}
+       oldProxy := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "old"}
+       newProxy := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "new"}
+       configFor := func(proxy types.NamespacedNameKind) 
map[types.NamespacedNameKind]adctypes.Config {
+               return map[types.NamespacedNameKind]adctypes.Config{proxy: 
{Name: proxy.String()}}
+       }
+       resourcesOf := func(sslID string) *adctypes.Resources {
+               return &adctypes.Resources{
+                       SSLs:           []*adctypes.SSL{{Metadata: 
adctypes.Metadata{ID: sslID, Labels: labelsOf(gw1)}}},
+                       GlobalRules:    adctypes.GlobalRule{"cors": 
map[string]any{}},
+                       PluginMetadata: adctypes.PluginMetadata{"http-logger": 
map[string]any{}},
+               }
+       }
+
+       require.NoError(t, d.applyResourceState(gw1, configFor(oldProxy), 
[]string{adctypes.TypeSSL}, resourcesOf("ssl1"), labelsOf(gw1), 
pluginsFromGatewayProxy))
+       gw2Resources := resourcesOf("ssl2")
+       gw2Resources.SSLs[0].Labels = labelsOf(gw2)
+       require.NoError(t, d.applyResourceState(gw2, configFor(oldProxy), 
[]string{adctypes.TypeSSL}, gw2Resources, labelsOf(gw2), 
pluginsFromGatewayProxy))
+
+       ssl, ok := d.store.Lookup(oldProxy.String(), adctypes.TypeSSL, "ssl1")
+       require.True(t, ok)
+       assert.Equal(t, gw1, ssl.Owner)
+       for _, resourceType := range []string{adctypes.TypeGlobalRule, 
adctypes.TypePluginMetadata} {
+               id := map[string]string{adctypes.TypeGlobalRule: "cors", 
adctypes.TypePluginMetadata: "http-logger"}[resourceType]
+               entity, ok := d.store.Lookup(oldProxy.String(), resourceType, 
id)
+               require.True(t, ok, resourceType)
+               assert.Equal(t, oldProxy, entity.Owner, "%s comes from the 
GatewayProxy, not the Gateway that was reconciled", resourceType)
+       }
+       assert.Len(t, d.store.OwnedEntities(oldProxy.String(), oldProxy), 2, 
"Gateways sharing a GatewayProxy write its plugins once")
+
+       require.NoError(t, d.applyResourceState(gw1, configFor(newProxy), 
[]string{adctypes.TypeSSL}, resourcesOf("ssl1"), labelsOf(gw1), 
pluginsFromGatewayProxy))
+       _, ok = d.store.Lookup(oldProxy.String(), adctypes.TypeSSL, "ssl1")
+       assert.False(t, ok, "the Gateway's own certificate leaves the config it 
no longer references")
+       _, ok = d.store.Lookup(oldProxy.String(), adctypes.TypeGlobalRule, 
"cors")
+       assert.True(t, ok, "the old GatewayProxy's own plugins stay in its own 
config")
+}
+
+func TestRemoveResourceStateRemovesAnApisixGlobalRulesPlugins(t *testing.T) {
+       d := newTestProvider(t)
+       globalRule := types.NamespacedNameKind{Kind: 
types.KindApisixGlobalRule, Namespace: "ns", Name: "global"}
+       proxy := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "gp"}
+       configs := map[types.NamespacedNameKind]adctypes.Config{proxy: {Name: 
proxy.String()}}
+       require.NoError(t, d.store.SetGlobalRules(proxy.String(), proxy, 
adctypes.GlobalRule{"cors": map[string]any{}}))
+       require.NoError(t, d.applyResourceState(globalRule, configs, nil, 
&adctypes.Resources{
+               GlobalRules: adctypes.GlobalRule{"prometheus": 
map[string]any{}},
+       }, labelsOf(globalRule), pluginsFromResource))
+
+       resources, err := d.store.GetResources(proxy.String())
+       require.NoError(t, err)
+       assert.Len(t, resources.GlobalRules, 2)
+
+       _, err = d.removeResourceState(globalRule, nil, labelsOf(globalRule), 
pluginsFromResource, false)
+       require.NoError(t, err)
+       resources, err = d.store.GetResources(proxy.String())
+       require.NoError(t, err)
+       assert.Equal(t, adctypes.GlobalRule{"cors": map[string]any{}}, 
resources.GlobalRules, "only the deleted resource's own plugins go away")
+}

Reply via email to