diff --git a/go.mod b/go.mod index 9d90eed51..07da85ee4 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,8 @@ toolchain go1.24.11 require ( dubbo.apache.org/dubbo-go/v3 v3.3.0 - github.com/Masterminds/semver/v3 v3.2.1 - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/dubbogo/gost v1.14.0 + github.com/dubbogo/go-zookeeper v1.0.4 github.com/duke-git/lancet/v2 v2.3.6 - github.com/emicklei/go-restful/v3 v3.12.2 - github.com/envoyproxy/go-control-plane v0.13.4 github.com/envoyproxy/go-control-plane/envoy v1.32.4 github.com/fullstorydev/grpcurl v1.9.1 github.com/gin-contrib/sessions v1.0.4 @@ -35,27 +31,18 @@ require ( github.com/go-co-op/gocron v1.9.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.3.0 - github.com/goburrow/cache v0.1.4 github.com/golang/protobuf v1.5.4 - github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 github.com/jhump/protoreflect v1.16.0 - github.com/kelseyhightower/envconfig v1.4.0 - github.com/mitchellh/mapstructure v1.5.0 github.com/nacos-group/nacos-sdk-go/v2 v2.3.5 github.com/onsi/ginkgo/v2 v2.22.1 github.com/onsi/gomega v1.36.2 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.20.5 - github.com/slok/go-http-metrics v0.11.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 - golang.org/x/sys v0.38.0 golang.org/x/text v0.31.0 google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 @@ -68,7 +55,6 @@ require ( k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 - moul.io/zapgorm2 v1.3.0 sigs.k8s.io/controller-runtime v0.19.4 sigs.k8s.io/yaml v1.6.0 ) @@ -110,8 +96,9 @@ require ( ) require ( - cel.dev/expr v0.23.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/armon/go-radix v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bufbuild/protocompile v0.10.0 // indirect github.com/bytedance/sonic v1.13.2 // indirect @@ -120,8 +107,8 @@ require ( github.com/cloudwego/base64x v0.1.5 // indirect github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dubbogo/go-zookeeper v1.0.4 // indirect - github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect + github.com/dubbogo/gost v1.14.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect @@ -138,6 +125,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -162,6 +150,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.57.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -175,17 +164,17 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.16.0 // indirect golang.org/x/crypto v0.45.0 // indirect + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/go.sum b/go.sum index 265bc3a8a..835748c36 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= -cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -42,8 +40,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Workiva/go-datastructures v1.0.52 h1:PLSK6pwn8mYdaoaCZEMsXBpBotr4HHn9abU0yMQt0NI= github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= @@ -118,8 +114,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -196,12 +192,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= @@ -263,8 +255,6 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw= -github.com/goburrow/cache v0.1.4/go.mod h1:cDFesZDnIlrHoNlMYqqMpCRawuXulgx+y7mXU8HZ+/c= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -380,8 +370,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 h1:umaj0TCQ9lWUUKy2DxAhEzPbwd0jnxiw1EI2z3FiILM= -github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69/go.mod h1:zdLK9ilQRSMjSeLKoZ4BqUfBT7jswTGF8zRlKEsiRXA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -400,7 +388,6 @@ github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -427,8 +414,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= -github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -473,8 +458,6 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -568,8 +551,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/slok/go-http-metrics v0.11.0 h1:ABJUpekCZSkQT1wQrFvS4kGbhea/w6ndFJaWJeh3zL0= -github.com/slok/go-http-metrics v0.11.0/go.mod h1:ZGKeYG1ET6TEJpQx18BqAJAvxw9jBAZXCHU7bWQqqAc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -669,7 +650,6 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= @@ -1023,8 +1003,6 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= -google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1102,7 +1080,6 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= -gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1124,8 +1101,6 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk= -moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/pkg/console/service/application.go b/pkg/console/service/application.go index 80e7ee685..cd295e2bc 100644 --- a/pkg/console/service/application.go +++ b/pkg/console/service/application.go @@ -42,9 +42,9 @@ func GetApplicationDetail(ctx consolectx.Context, req *model.ApplicationDetailRe instanceResources, err := manager.ListByIndexes[*meshresource.InstanceResource]( ctx.ResourceManager(), meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByInstanceAppNameIndex: req.AppName, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByInstanceAppNameIndex, Value: req.AppName, Operator: index.Equals}, }, ) if err != nil { @@ -68,9 +68,9 @@ func GetAppInstanceInfo(ctx consolectx.Context, req *model.ApplicationTabInstanc pageData, err := manager.PageListByIndexes[*meshresource.InstanceResource]( ctx.ResourceManager(), meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByInstanceAppNameIndex: req.AppName, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByInstanceAppNameIndex, Value: req.AppName, Operator: index.Equals}, }, req.PageReq, ) @@ -120,23 +120,28 @@ func GetAppServiceInfo(ctx consolectx.Context, req *model.ApplicationServiceForm } func getAppProvideServiceInfo(ctx consolectx.Context, req *model.ApplicationServiceFormReq) (*model.SearchPaginationResult, error) { - var indexes map[string]string + var conditions []index.IndexCondition + conditions = append(conditions, index.IndexCondition{ + IndexName: index.ByMeshIndex, + Value: req.Mesh, + Operator: index.Equals, + }) + conditions = append(conditions, index.IndexCondition{ + IndexName: index.ByServiceProviderAppName, + Value: req.AppName, + Operator: index.Equals, + }) if strutil.IsNotBlank(req.ServiceName) { - indexes = map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByServiceProviderAppName: req.AppName, - index.ByServiceProviderServiceName: req.ServiceName, - } - } else { - indexes = map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByServiceProviderAppName: req.AppName, - } + conditions = append(conditions, index.IndexCondition{ + IndexName: index.ByServiceProviderServiceName, + Value: req.ServiceName, + Operator: index.Equals, + }) } pageData, err := manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource]( ctx.ResourceManager(), meshresource.ServiceProviderMetadataKind, - indexes, + conditions, req.PageReq, ) if err != nil { @@ -167,9 +172,9 @@ func getAppConsumeServiceInfo(ctx consolectx.Context, req *model.ApplicationServ pageData, err := manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource]( ctx.ResourceManager(), meshresource.ServiceConsumerMetadataKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByServiceConsumerAppName: req.AppName, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByServiceConsumerAppName, Value: req.AppName, Operator: index.Equals}, }, req.PageReq, ) @@ -220,8 +225,8 @@ func SearchApplications(ctx consolectx.Context, req *model.ApplicationSearchReq) pageData, err := manager.PageListByIndexes[*meshresource.ApplicationResource]( ctx.ResourceManager(), meshresource.ApplicationKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq, ) diff --git a/pkg/console/service/condition_rule.go b/pkg/console/service/condition_rule.go index de0ebfc83..9fae15940 100644 --- a/pkg/console/service/condition_rule.go +++ b/pkg/console/service/condition_rule.go @@ -40,8 +40,8 @@ func SearchConditionRules(ctx context.Context, req *model.SearchConditionRuleReq pageData, err := manager.PageListByIndexes[*meshresource.ConditionRouteResource]( ctx.ResourceManager(), meshresource.ConditionRouteKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq) if err != nil { diff --git a/pkg/console/service/configurator_rule.go b/pkg/console/service/configurator_rule.go index 16aa59e4c..13dd2284d 100644 --- a/pkg/console/service/configurator_rule.go +++ b/pkg/console/service/configurator_rule.go @@ -36,8 +36,8 @@ func PageListConfiguratorRule(ctx consolectx.Context, req *model.SearchReq) (*mo pageData, err := manager.PageListByIndexes[*meshresource.DynamicConfigResource]( ctx.ResourceManager(), meshresource.DynamicConfigKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq) if err != nil { diff --git a/pkg/console/service/instance.go b/pkg/console/service/instance.go index ed0f822df..0ef06fbce 100644 --- a/pkg/console/service/instance.go +++ b/pkg/console/service/instance.go @@ -44,9 +44,9 @@ func SearchInstanceByIp(ctx consolectx.Context, req *model.SearchReq) (*model.Se pageData, err := manager.PageListByIndexes[*meshresource.InstanceResource]( ctx.ResourceManager(), meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByInstanceIpIndex: req.Keywords, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByInstanceIpIndex, Value: req.Keywords, Operator: index.Equals}, }, req.PageReq) if err != nil { @@ -75,9 +75,9 @@ func SearchInstanceByName(ctx consolectx.Context, req *model.SearchReq) (*model. pageData, err := manager.PageListByIndexes[*meshresource.InstanceResource]( ctx.ResourceManager(), meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByInstanceNameIndex: req.Keywords, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByInstanceNameIndex, Value: req.Keywords, Operator: index.Equals}, }, req.PageReq) if err != nil { @@ -113,8 +113,8 @@ func SearchInstances(ctx consolectx.Context, req *model.SearchInstanceReq) (*mod pageData, err := manager.PageListByIndexes[*meshresource.InstanceResource]( ctx.ResourceManager(), meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq) if err != nil { diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go index 85d58a1a8..d5bb8d02b 100644 --- a/pkg/console/service/service.go +++ b/pkg/console/service/service.go @@ -39,17 +39,21 @@ import ( // GetServiceTabDistribution get service distribution func GetServiceTabDistribution(ctx consolectx.Context, req *model.ServiceTabDistributionReq) (*model.SearchPaginationResult, error) { - indexes := map[string]string{ - index.ByServiceConsumerServiceName: req.ServiceName, + conditions := []index.IndexCondition{ + {IndexName: index.ByServiceConsumerServiceName, Value: req.ServiceName, Operator: index.Equals}, } // for now, only support accurate name match if strutil.IsNotBlank(req.Keywords) { - indexes[index.ByServiceConsumerAppName] = req.Keywords + conditions = append(conditions, index.IndexCondition{ + IndexName: index.ByServiceConsumerAppName, + Value: req.Keywords, + Operator: index.Equals, + }) } pageData, err := manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource]( ctx.ResourceManager(), meshresource.ServiceConsumerMetadataKind, - indexes, + conditions, req.PageReq) if err != nil { logger.Errorf("get service consumer %s failed, cause: %v", req.ServiceName, err) @@ -96,8 +100,8 @@ func SearchServices(ctx consolectx.Context, req *model.ServiceSearchReq) (*model pageData, err := manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource]( ctx.ResourceManager(), meshresource.ServiceProviderMetadataKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq, ) @@ -123,9 +127,9 @@ func SearchServicesByKeywords(ctx consolectx.Context, req *model.ServiceSearchRe pageData, err := manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource]( ctx.ResourceManager(), meshresource.ServiceProviderMetadataKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - index.ByServiceProviderServiceName: req.Keywords, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, + {IndexName: index.ByServiceProviderServiceName, Value: req.Keywords, Operator: index.Equals}, }, req.PageReq, ) diff --git a/pkg/console/service/tag_rule.go b/pkg/console/service/tag_rule.go index 972cbf105..a051117ca 100644 --- a/pkg/console/service/tag_rule.go +++ b/pkg/console/service/tag_rule.go @@ -36,8 +36,8 @@ func PageListTagRule(ctx consolectx.Context, req *model.SearchReq) (*model.Searc pageData, err := manager.PageListByIndexes[*meshresource.TagRouteResource]( ctx.ResourceManager(), meshresource.TagRouteKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, + []index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals}, }, req.PageReq) if err != nil { diff --git a/pkg/core/discovery/subscriber/instance.go b/pkg/core/discovery/subscriber/instance.go index ded8a67d0..bb60fb8ea 100644 --- a/pkg/core/discovery/subscriber/instance.go +++ b/pkg/core/discovery/subscriber/instance.go @@ -70,9 +70,9 @@ func (s *InstanceEventSubscriber) ProcessEvent(event events.Event) error { } else { instanceRes = oldObj } - instanceResList, err := s.instanceStore.ListByIndexes(map[string]string{ - index.ByMeshIndex: instanceRes.Mesh, - index.ByInstanceAppNameIndex: instanceRes.Spec.AppName, + instanceResList, err := s.instanceStore.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: instanceRes.Mesh, Operator: index.Equals}, + {IndexName: index.ByInstanceAppNameIndex, Value: instanceRes.Spec.AppName, Operator: index.Equals}, }) appResKey := coremodel.BuildResourceKey(instanceRes.Mesh, instanceRes.Spec.AppName) diff --git a/pkg/core/discovery/subscriber/nacos_service.go b/pkg/core/discovery/subscriber/nacos_service.go index e5c9c6312..07c23f108 100644 --- a/pkg/core/discovery/subscriber/nacos_service.go +++ b/pkg/core/discovery/subscriber/nacos_service.go @@ -127,9 +127,9 @@ func (n *NacosServiceEventSubscriber) processConsumerMetadataUpsert(serviceRes * logger.Errorf("process service consumer metadata upsert event, but cannot route to service consumer metadata resource, cause: %v", err) return err } - resources, err := st.ListByIndexes(map[string]string{ - index.ByMeshIndex: serviceRes.Mesh, - index.ByServiceConsumerServiceName: serviceName, + resources, err := st.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, Operator: index.Equals}, + {IndexName: index.ByServiceConsumerServiceName, Value: serviceName, Operator: index.Equals}, }) if err != nil { logger.Errorf("process service consumer metadata upsert event, but cannot list service consumer metadata resource of %s, cause: %v", serviceRes.Name, err) @@ -212,9 +212,9 @@ func (n *NacosServiceEventSubscriber) processRPCInstanceUpsert(serviceRes *meshr logger.Errorf("process rpc instance upsert event, but cannot route to rpc instance resource, cause: %v", err) return err } - resources, err := st.ListByIndexes(map[string]string{ - index.ByMeshIndex: serviceRes.Mesh, - index.ByRPCInstanceAppName: serviceRes.Name, + resources, err := st.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, Operator: index.Equals}, + {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name, Operator: index.Equals}, }) if err != nil { logger.Errorf("process rpc instance upsert event, but cannot list rpc instance resource of %s, cause: %v", serviceRes.Name, err) @@ -287,9 +287,9 @@ func (n *NacosServiceEventSubscriber) processServiceConsumerDelete(serviceRes *m logger.Errorf("process service consumer delete event, but cannot route to service consumer metadata resource, cause: %v", err) return err } - resources, err := st.ListByIndexes(map[string]string{ - index.ByMeshIndex: serviceRes.Mesh, - index.ByServiceConsumerServiceName: serviceRes.Name, + resources, err := st.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, Operator: index.Equals}, + {IndexName: index.ByServiceConsumerServiceName, Value: serviceRes.Name, Operator: index.Equals}, }) if err != nil { logger.Errorf("process service consumer delete event, but cannot list service consumer metadata resource of %s, cause: %v", serviceRes.Name, err) @@ -311,9 +311,9 @@ func (n *NacosServiceEventSubscriber) processRPCInstanceDelete(serviceRes *meshr logger.Errorf("process rpc instance delete event, but cannot route to rpc instance resource, cause: %v", err) return err } - resources, err := st.ListByIndexes(map[string]string{ - index.ByMeshIndex: serviceRes.Mesh, - index.ByRPCInstanceAppName: serviceRes.Name, + resources, err := st.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, Operator: index.Equals}, + {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name, Operator: index.Equals}, }) if err != nil { logger.Errorf("process rpc instance delete event, but cannot list rpc instance resource of %s, cause: %v", serviceRes.Name, err) diff --git a/pkg/core/discovery/subscriber/rpc_instance.go b/pkg/core/discovery/subscriber/rpc_instance.go index e970239dd..0343e5cf7 100644 --- a/pkg/core/discovery/subscriber/rpc_instance.go +++ b/pkg/core/discovery/subscriber/rpc_instance.go @@ -185,8 +185,8 @@ func (s *RPCInstanceEventSubscriber) findRelatedRuntimeInstanceAndMerge(instance } func (s *RPCInstanceEventSubscriber) getRuntimeInstanceByIp(ip string) *meshresource.RuntimeInstanceResource { - resources, err := s.rtInstanceStore.ListByIndexes(map[string]string{ - index.ByRuntimeInstanceIPIndex: ip, + resources, err := s.rtInstanceStore.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByRuntimeInstanceIPIndex, Value: ip, Operator: index.Equals}, }) if err != nil { logger.Errorf("list runtime instance by ip index failed, ip: %s, err: %s", ip, err.Error()) diff --git a/pkg/core/engine/subscriber/runtime_instance.go b/pkg/core/engine/subscriber/runtime_instance.go index 6038462c0..f7d387090 100644 --- a/pkg/core/engine/subscriber/runtime_instance.go +++ b/pkg/core/engine/subscriber/runtime_instance.go @@ -161,8 +161,8 @@ func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByName( return nil, nil } instanceResName := meshresource.BuildInstanceResName(rtInstanceRes.Spec.AppName, rtInstanceRes.Spec.Ip, rtInstanceRes.Spec.RpcPort) - resources, err := s.instanceStore.ListByIndexes(map[string]string{ - index.ByInstanceNameIndex: instanceResName, + resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByInstanceNameIndex, Value: instanceResName, Operator: index.Equals}, }) if err != nil { return nil, err @@ -190,8 +190,8 @@ func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByName( func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByIP( rtInstanceRes *meshresource.RuntimeInstanceResource) (*meshresource.InstanceResource, error) { - resources, err := s.instanceStore.ListByIndexes(map[string]string{ - index.ByInstanceIpIndex: rtInstanceRes.Spec.Ip, + resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{ + {IndexName: index.ByInstanceIpIndex, Value: rtInstanceRes.Spec.Ip, Operator: index.Equals}, }) if err != nil { return nil, err diff --git a/pkg/core/manager/manager.go b/pkg/core/manager/manager.go index 44861ca7f..3e4ce0942 100644 --- a/pkg/core/manager/manager.go +++ b/pkg/core/manager/manager.go @@ -25,6 +25,7 @@ import ( "github.com/apache/dubbo-admin/pkg/core/governor" "github.com/apache/dubbo-admin/pkg/core/resource/model" "github.com/apache/dubbo-admin/pkg/core/store" + "github.com/apache/dubbo-admin/pkg/core/store/index" ) type ReadOnlyResourceManager interface { @@ -32,13 +33,10 @@ type ReadOnlyResourceManager interface { GetByKey(rk model.ResourceKind, key string) (r model.Resource, exist bool, err error) // GetByKeys returns the resources with the given resource keys GetByKeys(rk model.ResourceKind, keys []string) ([]model.Resource, error) - // ListByIndexes returns the resources with the given indexes, indexes is a map of index name and index value - ListByIndexes(rk model.ResourceKind, indexes map[string]string) ([]model.Resource, error) - // PageListByIndexes page list the resources with the given indexes, indexes is a map of index name and index value - PageListByIndexes(rk model.ResourceKind, indexes map[string]string, pr model.PageReq) (*model.PageData[model.Resource], error) - // PageSearchResourceByConditions page fuzzy search resource by conditions, conditions cannot be empty - // TODO support multiple conditions - PageSearchResourceByConditions(rk model.ResourceKind, conditions []string, pr model.PageReq) (*model.PageData[model.Resource], error) + // ListByIndexes returns the resources with the given index conditions + ListByIndexes(rk model.ResourceKind, indexes []index.IndexCondition) ([]model.Resource, error) + // PageListByIndexes page list the resources with the given index conditions + PageListByIndexes(rk model.ResourceKind, indexes []index.IndexCondition, pr model.PageReq) (*model.PageData[model.Resource], error) } type WriteOnlyResourceManager interface { @@ -100,7 +98,7 @@ func (rm *resourcesManager) GetByKeys(rk model.ResourceKind, keys []string) ([]m return resources, nil } -func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes map[string]string) ([]model.Resource, error) { +func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes []index.IndexCondition) ([]model.Resource, error) { rs, err := rm.storeRouter.ResourceKindRoute(rk) if err != nil { return nil, err @@ -114,7 +112,7 @@ func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes map[str func (rm *resourcesManager) PageListByIndexes( rk model.ResourceKind, - indexes map[string]string, + indexes []index.IndexCondition, pr model.PageReq) (*model.PageData[model.Resource], error) { rs, err := rm.storeRouter.ResourceKindRoute(rk) @@ -128,11 +126,6 @@ func (rm *resourcesManager) PageListByIndexes( return pageData, nil } -func (rm *resourcesManager) PageSearchResourceByConditions(rk model.ResourceKind, conditions []string, pr model.PageReq) (*model.PageData[model.Resource], error) { - //TODO implement me - panic("implement me") -} - func (rm *resourcesManager) Add(r model.Resource) error { if !governor.RuleResourceKinds.Contain(r.ResourceKind()) { return bizerror.New(bizerror.InvalidArgument, "invalid resource kind") diff --git a/pkg/core/manager/manager_helper.go b/pkg/core/manager/manager_helper.go index 80e40ba80..48a333631 100644 --- a/pkg/core/manager/manager_helper.go +++ b/pkg/core/manager/manager_helper.go @@ -22,6 +22,7 @@ import ( "github.com/apache/dubbo-admin/pkg/common/bizerror" "github.com/apache/dubbo-admin/pkg/core/resource/model" + "github.com/apache/dubbo-admin/pkg/core/store/index" ) // GetByKey is a helper function of ResourceManager.GeyByKey @@ -59,7 +60,7 @@ func GetByKeys[T model.Resource](rm ReadOnlyResourceManager, rk model.ResourceKi } // ListByIndexes is a helper function of ResourceManager.ListByIndexes -func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk model.ResourceKind, indexes map[string]string) ([]T, error) { +func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk model.ResourceKind, indexes []index.IndexCondition) ([]T, error) { resources, err := rm.ListByIndexes(rk, indexes) if err != nil { return nil, err @@ -81,7 +82,7 @@ func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk model.Resour func PageListByIndexes[T model.Resource]( rm ReadOnlyResourceManager, rk model.ResourceKind, - indexes map[string]string, + indexes []index.IndexCondition, pr model.PageReq) (*model.PageData[T], error) { pageData, err := rm.PageListByIndexes(rk, indexes, pr) @@ -107,33 +108,3 @@ func PageListByIndexes[T model.Resource]( } return newPageData, nil } - -// PageSearchResourceByConditions is a helper function of ResourceManager.PageSearchResourceByConditions -func PageSearchResourceByConditions[T model.Resource]( - rm ReadOnlyResourceManager, - rk model.ResourceKind, - conditions []string, - pr model.PageReq) (*model.PageData[T], error) { - pageData, err := rm.PageSearchResourceByConditions(rk, conditions, pr) - if err != nil { - return nil, err - } - - typedResources := make([]T, len(pageData.Data)) - for i, resource := range pageData.Data { - typedResource, ok := resource.(T) - if !ok { - return nil, bizerror.NewAssertionError(rk, reflect.TypeOf(typedResource).Name()) - } - typedResources[i] = typedResource - } - newPageData := &model.PageData[T]{ - Pagination: model.Pagination{ - Total: pageData.Total, - PageOffset: pageData.PageOffset, - PageSize: pageData.PageSize, - }, - Data: typedResources, - } - return newPageData, nil -} diff --git a/pkg/core/store/index/condition.go b/pkg/core/store/index/condition.go new file mode 100644 index 000000000..4bdcee2f7 --- /dev/null +++ b/pkg/core/store/index/condition.go @@ -0,0 +1,38 @@ +/* + * 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 index + +// IndexOperator defines the comparison operator for index queries +type IndexOperator string + +const ( + // Equals performs exact match on the index value + Equals IndexOperator = "Equals" + // HasPrefix performs prefix match on the index value + HasPrefix IndexOperator = "HasPrefix" +) + +// IndexCondition represents a single index query condition +type IndexCondition struct { + // IndexName is the name of the index to query + IndexName string + // Value is the value to match against the index + Value string + // Operator is the comparison operator to use (Equals, HasPrefix, etc.) + Operator IndexOperator +} diff --git a/pkg/core/store/store.go b/pkg/core/store/store.go index 15d2117e0..8923f6577 100644 --- a/pkg/core/store/store.go +++ b/pkg/core/store/store.go @@ -27,6 +27,7 @@ import ( "github.com/apache/dubbo-admin/pkg/core/resource/model" "github.com/apache/dubbo-admin/pkg/core/runtime" + "github.com/apache/dubbo-admin/pkg/core/store/index" ) // ResourceStore defines the interface for the persistance of a resource @@ -36,10 +37,10 @@ type ResourceStore interface { // GetByKeys get resources by keys, return list of resource. // if a resource of specified key doesn't exist in the store, resource list will not include it GetByKeys(keys []string) ([]model.Resource, error) - // ListByIndexes list resources by indexes, indexes is map of index name and index value - ListByIndexes(indexes map[string]string) ([]model.Resource, error) - // PageListByIndexes list resources by indexes pageable, indexes is map of index name and index value - PageListByIndexes(indexes map[string]string, pq model.PageReq) (*model.PageData[model.Resource], error) + // ListByIndexes list resources by index conditions + ListByIndexes(indexes []index.IndexCondition) ([]model.Resource, error) + // PageListByIndexes list resources by index conditions pageable + PageListByIndexes(indexes []index.IndexCondition, pq model.PageReq) (*model.PageData[model.Resource], error) } // ManagedResourceStore includes both functional interfaces and lifecycle interfaces diff --git a/pkg/store/dbcommon/gorm_store.go b/pkg/store/dbcommon/gorm_store.go index e823189b5..97a19c276 100644 --- a/pkg/store/dbcommon/gorm_store.go +++ b/pkg/store/dbcommon/gorm_store.go @@ -22,6 +22,7 @@ import ( "fmt" "reflect" "sort" + "sync" "gorm.io/gorm" "k8s.io/client-go/tools/cache" @@ -35,14 +36,15 @@ import ( ) // GormStore is a GORM-backed store implementation for Dubbo resources -// It uses GORM for database operations and maintains in-memory indices for fast lookups +// It uses GORM for database operations and persists all indices to the resource_indices table // This implementation is database-agnostic and works with any GORM-supported database type GormStore struct { - pool *ConnectionPool // Shared connection pool with reference counting - kind model.ResourceKind - address string - indices *Index // In-memory index with thread-safe operations - stopCh chan struct{} + pool *ConnectionPool // Shared connection pool with reference counting + kind model.ResourceKind + address string + indexers cache.Indexers // Index functions for creating indices + mu sync.RWMutex // Protects indexers + stopCh chan struct{} } var _ store.ManagedResourceStore = &GormStore{} @@ -50,15 +52,15 @@ var _ store.ManagedResourceStore = &GormStore{} // NewGormStore creates a new GORM store for the specified resource kind func NewGormStore(kind model.ResourceKind, address string, pool *ConnectionPool) *GormStore { return &GormStore{ - kind: kind, - address: address, - pool: pool, - indices: NewIndex(), - stopCh: make(chan struct{}), + kind: kind, + address: address, + pool: pool, + indexers: make(cache.Indexers), + stopCh: make(chan struct{}), } } -// Init initializes the GORM store by migrating the schema and rebuilding indices +// Init initializes the GORM store by migrating the schema and registering indexers func (gs *GormStore) Init(_ runtime.BuilderContext) error { // Perform table migration db := gs.pool.GetDB() @@ -66,17 +68,18 @@ func (gs *GormStore) Init(_ runtime.BuilderContext) error { if err := db.Scopes(TableScope(gs.kind.ToString())).AutoMigrate(&ResourceModel{}); err != nil { return fmt.Errorf("failed to migrate schema for %s: %w", gs.kind.ToString(), err) } + + // Migrate resource_indices table (shared across all resource kinds) + if err := db.AutoMigrate(&ResourceIndexModel{}); err != nil { + return fmt.Errorf("failed to migrate resource_indices: %w", err) + } + // Register indexers for the resource kind indexers := index.IndexersRegistry().Indexers(gs.kind) if err := gs.AddIndexers(indexers); err != nil { return err } - // Rebuild indices from existing data in the database - if err := gs.rebuildIndices(); err != nil { - return fmt.Errorf("failed to rebuild indices for %s: %w", gs.kind.ToString(), err) - } - logger.Infof("GORM store initialized for resource kind: %s", gs.kind.ToString()) return nil } @@ -142,8 +145,10 @@ func (gs *GormStore) Add(obj interface{}) error { return err } - // Update indices after successful DB operation - gs.indices.UpdateResource(resource, nil) + // Persist index entries to DB + if err := gs.persistIndexEntries(resource, nil); err != nil { + logger.Warnf("failed to persist index entries for %s: %v", resource.ResourceKey(), err) + } return nil } @@ -198,8 +203,10 @@ func (gs *GormStore) Update(obj interface{}) error { ) } - // Update indices: remove old and add new - gs.indices.UpdateResource(resource, oldResource.(model.Resource)) + // Persist index entries to DB + if err := gs.persistIndexEntries(resource, oldResource.(model.Resource)); err != nil { + logger.Warnf("failed to persist index entries for %s: %v", resource.ResourceKey(), err) + } return nil } @@ -228,8 +235,10 @@ func (gs *GormStore) Delete(obj interface{}) error { ) } - // Remove from indices - gs.indices.RemoveResource(resource) + // Delete index entries from DB + if err := gs.deleteIndexEntries(resource.ResourceKey()); err != nil { + logger.Warnf("failed to delete index entries for %s: %v", resource.ResourceKey(), err) + } return nil } @@ -308,8 +317,11 @@ func (gs *GormStore) Replace(list []interface{}, _ string) error { return err } - // Clear all indices - gs.clearIndices() + // Delete all index entries for this resource kind + if err := tx.Where("resource_kind = ?", gs.kind.ToString()). + Delete(&ResourceIndexModel{}).Error; err != nil { + return err + } // Return early if list is empty if len(list) == 0 { @@ -338,9 +350,31 @@ func (gs *GormStore) Replace(list []interface{}, _ string) error { return err } - // Rebuild indices for all resources + // Persist all index entries in bulk + var indexEntries []ResourceIndexModel + indexers := gs.GetIndexers() for _, resource := range resources { - gs.indices.UpdateResource(resource, nil) + for indexName, indexFunc := range indexers { + values, err := indexFunc(resource) + if err != nil { + continue + } + for _, v := range values { + indexEntries = append(indexEntries, ResourceIndexModel{ + ResourceKind: gs.kind.ToString(), + IndexName: indexName, + IndexValue: v, + ResourceKey: resource.ResourceKey(), + Operator: string(index.Equals), + }) + } + } + } + + if len(indexEntries) > 0 { + if err := tx.CreateInBatches(&indexEntries, 100).Error; err != nil { + logger.Warnf("failed to persist index entries during replace: %v", err) + } } return nil @@ -352,11 +386,12 @@ func (gs *GormStore) Resync() error { } func (gs *GormStore) Index(indexName string, obj interface{}) ([]interface{}, error) { - if !gs.indices.IndexExists(indexName) { + if !gs.IndexExists(indexName) { return nil, fmt.Errorf("index %s does not exist", indexName) } - indexFunc := gs.indices.GetIndexers()[indexName] + indexers := gs.GetIndexers() + indexFunc := indexers[indexName] indexValues, err := indexFunc(obj) if err != nil { return nil, err @@ -370,7 +405,7 @@ func (gs *GormStore) Index(indexName string, obj interface{}) ([]interface{}, er } func (gs *GormStore) IndexKeys(indexName, indexedValue string) ([]string, error) { - if !gs.indices.IndexExists(indexName) { + if !gs.IndexExists(indexName) { return nil, fmt.Errorf("index %s does not exist", indexName) } @@ -390,15 +425,24 @@ func (gs *GormStore) IndexKeys(indexName, indexedValue string) ([]string, error) } func (gs *GormStore) ListIndexFuncValues(indexName string) []string { - if !gs.indices.IndexExists(indexName) { + if !gs.IndexExists(indexName) { return []string{} } - return gs.indices.ListIndexFuncValues(indexName) + var values []string + db := gs.pool.GetDB() + if err := db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND index_name = ?", gs.kind.ToString(), indexName). + Distinct("index_value"). + Pluck("index_value", &values).Error; err != nil { + logger.Errorf("failed to list index func values for %s: %v", indexName, err) + return []string{} + } + return values } func (gs *GormStore) ByIndex(indexName, indexedValue string) ([]interface{}, error) { - if !gs.indices.IndexExists(indexName) { + if !gs.IndexExists(indexName) { return nil, fmt.Errorf("index %s does not exist", indexName) } @@ -406,11 +450,28 @@ func (gs *GormStore) ByIndex(indexName, indexedValue string) ([]interface{}, err } func (gs *GormStore) GetIndexers() cache.Indexers { - return gs.indices.GetIndexers() + gs.mu.RLock() + defer gs.mu.RUnlock() + + result := make(cache.Indexers, len(gs.indexers)) + for k, v := range gs.indexers { + result[k] = v + } + return result } func (gs *GormStore) AddIndexers(newIndexers cache.Indexers) error { - return gs.indices.AddIndexers(newIndexers) + gs.mu.Lock() + defer gs.mu.Unlock() + + for name, indexFunc := range newIndexers { + if _, exists := gs.indexers[name]; exists { + return fmt.Errorf("indexer %s already exists", name) + } + gs.indexers[name] = indexFunc + } + + return nil } func (gs *GormStore) GetByKeys(keys []string) ([]model.Resource, error) { @@ -439,7 +500,7 @@ func (gs *GormStore) GetByKeys(keys []string) ([]model.Resource, error) { return resources, nil } -func (gs *GormStore) ListByIndexes(indexes map[string]string) ([]model.Resource, error) { +func (gs *GormStore) ListByIndexes(indexes []index.IndexCondition) ([]model.Resource, error) { keys, err := gs.getKeysByIndexes(indexes) if err != nil { return nil, err @@ -457,7 +518,7 @@ func (gs *GormStore) ListByIndexes(indexes map[string]string) ([]model.Resource, return resources, nil } -func (gs *GormStore) PageListByIndexes(indexes map[string]string, pq model.PageReq) (*model.PageData[model.Resource], error) { +func (gs *GormStore) PageListByIndexes(indexes []index.IndexCondition, pq model.PageReq) (*model.PageData[model.Resource], error) { keys, err := gs.getKeysByIndexes(indexes) if err != nil { return nil, err @@ -485,17 +546,34 @@ func (gs *GormStore) PageListByIndexes(indexes map[string]string, pq model.PageR } func (gs *GormStore) findByIndex(indexName, indexedValue string) ([]interface{}, error) { - if !gs.indices.IndexExists(indexName) { + if !gs.IndexExists(indexName) { return nil, fmt.Errorf("index %s does not exist", indexName) } - // Get resource keys from in-memory index - keys := gs.indices.GetKeys(indexName, indexedValue) + // Get resource keys from database index + db := gs.pool.GetDB() + var entries []ResourceIndexModel + err := db.Where("resource_kind = ? AND index_name = ? AND index_value = ?", + gs.kind.ToString(), indexName, indexedValue). + Find(&entries).Error + if err != nil { + return nil, err + } - if len(keys) == 0 { + if len(entries) == 0 { return []interface{}{}, nil } + // Collect unique resource keys + keys := make([]string, 0, len(entries)) + seen := make(map[string]struct{}) + for _, e := range entries { + if _, ok := seen[e.ResourceKey]; !ok { + keys = append(keys, e.ResourceKey) + seen[e.ResourceKey] = struct{}{} + } + } + // Fetch resources from DB by keys resources, err := gs.GetByKeys(keys) if err != nil { @@ -511,7 +589,7 @@ func (gs *GormStore) findByIndex(indexName, indexedValue string) ([]interface{}, return result, nil } -func (gs *GormStore) getKeysByIndexes(indexes map[string]string) ([]string, error) { +func (gs *GormStore) getKeysByIndexes(indexes []index.IndexCondition) ([]string, error) { if len(indexes) == 0 { return gs.ListKeys(), nil } @@ -519,8 +597,17 @@ func (gs *GormStore) getKeysByIndexes(indexes map[string]string) ([]string, erro var keySet map[string]struct{} first := true - for indexName, indexValue := range indexes { - keys, err := gs.IndexKeys(indexName, indexValue) + for _, condition := range indexes { + var keys []string + var err error + switch condition.Operator { + case index.Equals: + keys, err = gs.IndexKeys(condition.IndexName, condition.Value) + case index.HasPrefix: + keys, err = gs.getKeysByPrefixFromDB(condition.IndexName, condition.Value) + default: + return nil, bizerror.New(bizerror.InvalidArgument, "operator not yet supported: "+string(condition.Operator)) + } if err != nil { return nil, err } @@ -550,35 +637,76 @@ func (gs *GormStore) getKeysByIndexes(indexes map[string]string) ([]string, erro return result, nil } -// clearIndices clears all in-memory indices -func (gs *GormStore) clearIndices() { - gs.indices.Clear() +// persistIndexEntries writes index entries for a resource to the database +// If oldResource is not nil, first deletes old entries, then inserts new ones +func (gs *GormStore) persistIndexEntries(resource model.Resource, oldResource model.Resource) error { + db := gs.pool.GetDB() + + // Delete old entries if updating + if oldResource != nil { + if err := db.Where("resource_key = ?", oldResource.ResourceKey()).Delete(&ResourceIndexModel{}).Error; err != nil { + return err + } + } + + // Get all index entries for this resource + indexers := gs.GetIndexers() + var entries []ResourceIndexModel + for indexName, indexFunc := range indexers { + values, err := indexFunc(resource) + if err != nil { + continue + } + for _, v := range values { + entries = append(entries, ResourceIndexModel{ + ResourceKind: gs.kind.ToString(), + IndexName: indexName, + IndexValue: v, + ResourceKey: resource.ResourceKey(), + Operator: string(index.Equals), + }) + } + } + + if len(entries) == 0 { + return nil + } + + return db.Create(&entries).Error } -// rebuildIndices rebuilds all in-memory indices from existing database records -// This is called during initialization to ensure indices are populated with existing data -func (gs *GormStore) rebuildIndices() error { - // Clear existing indices first - gs.clearIndices() +// deleteIndexEntries removes all index entries for a resource key +func (gs *GormStore) deleteIndexEntries(resourceKey string) error { + db := gs.pool.GetDB() + return db.Where("resource_key = ?", resourceKey).Delete(&ResourceIndexModel{}).Error +} - // Load all resources from the database - var models []ResourceModel +// getKeysByPrefixFromDB retrieves resource keys matching a prefix from the database +func (gs *GormStore) getKeysByPrefixFromDB(indexName, prefix string) ([]string, error) { db := gs.pool.GetDB() - if err := db.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).Find(&models).Error; err != nil { - return fmt.Errorf("failed to load resources for index rebuild: %w", err) + var entries []ResourceIndexModel + err := db.Where("resource_kind = ? AND index_name = ? AND index_value LIKE ?", + gs.kind.ToString(), indexName, prefix+"%"). + Find(&entries).Error + if err != nil { + return nil, err } - // Rebuild indices for all resources - for _, m := range models { - resource, err := m.ToResource() - if err != nil { - logger.Errorf("failed to deserialize resource during index rebuild: %v", err) - continue + keys := make([]string, 0, len(entries)) + seen := make(map[string]struct{}) + for _, e := range entries { + if _, ok := seen[e.ResourceKey]; !ok { + keys = append(keys, e.ResourceKey) + seen[e.ResourceKey] = struct{}{} } - // Add resource to indices (nil for oldResource since this is initial load) - gs.indices.UpdateResource(resource, nil) } + return keys, nil +} - logger.Infof("Rebuilt indices for %s: loaded %d resources", gs.kind.ToString(), len(models)) - return nil +// IndexExists checks if an indexer with the given name exists +func (gs *GormStore) IndexExists(indexName string) bool { + gs.mu.RLock() + defer gs.mu.RUnlock() + _, exists := gs.indexers[indexName] + return exists } diff --git a/pkg/store/dbcommon/gorm_store_test.go b/pkg/store/dbcommon/gorm_store_test.go index da9718ed2..b1dd1b8c6 100644 --- a/pkg/store/dbcommon/gorm_store_test.go +++ b/pkg/store/dbcommon/gorm_store_test.go @@ -33,6 +33,7 @@ import ( storecfg "github.com/apache/dubbo-admin/pkg/config/store" "github.com/apache/dubbo-admin/pkg/core/resource/model" + "github.com/apache/dubbo-admin/pkg/core/store/index" ) // mockResource is a mock implementation of model.Resource for testing @@ -170,7 +171,7 @@ func TestNewGormStore(t *testing.T) { assert.Equal(t, kind, store.kind) assert.Equal(t, "test-address", store.address) assert.NotNil(t, store.pool) - assert.NotNil(t, store.indices) + assert.NotNil(t, store.indexers) assert.NotNil(t, store.stopCh) } @@ -775,7 +776,7 @@ func TestGormStore_ListByIndexes(t *testing.T) { require.NoError(t, err) // List by indexes - indexes := map[string]string{"by-mesh": "mesh1"} + indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}} resources, err := store.ListByIndexes(indexes) assert.NoError(t, err) assert.Len(t, resources, 2) @@ -802,7 +803,7 @@ func TestGormStore_ListByIndexesEmpty(t *testing.T) { require.NoError(t, err) // List with empty indexes should return all resources - resources, err := store.ListByIndexes(map[string]string{}) + resources, err := store.ListByIndexes([]index.IndexCondition{}) assert.NoError(t, err) assert.Len(t, resources, 1) } @@ -855,7 +856,7 @@ func TestGormStore_PageListByIndexes(t *testing.T) { require.NoError(t, err) // Page list by indexes - indexes := map[string]string{"by-mesh": "mesh1"} + indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}} pageReq := model.PageReq{ PageOffset: 0, PageSize: 2, @@ -918,7 +919,7 @@ func TestGormStore_PageListByIndexesOffsetBeyondTotal(t *testing.T) { require.NoError(t, err) // Request page beyond total - indexes := map[string]string{"by-mesh": "default"} + indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: "default", Operator: index.Equals}} pageReq := model.PageReq{ PageOffset: 10, PageSize: 2, @@ -999,9 +1000,9 @@ func TestGormStore_MultipleIndexes(t *testing.T) { } // Test multiple indexes - get all resources in mesh1 and default namespace - indexes := map[string]string{ - "by-mesh": "mesh1", - "by-namespace": "default", + indexes := []index.IndexCondition{ + {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}, + {IndexName: "by-namespace", Value: "default", Operator: index.Equals}, } result, err := store.ListByIndexes(indexes) assert.NoError(t, err) @@ -1238,79 +1239,6 @@ func TestGormStore_ReplaceIndices(t *testing.T) { assert.Contains(t, keys, "test-key-2") } -func TestGormStore_InitRebuildIndices(t *testing.T) { - // This test verifies that indices are rebuilt from existing data during Init() - // Simulates the scenario where a GormStore starts with existing data in the database - - // Create store and add data - store, cleanup := setupTestStore(t) - defer cleanup() - - err := store.Init(nil) - require.NoError(t, err) - - // Add indexer before adding data - indexers := map[string]cache.IndexFunc{ - "by-mesh": func(obj interface{}) ([]string, error) { - resource := obj.(model.Resource) - return []string{resource.ResourceMesh()}, nil - }, - } - err = store.AddIndexers(indexers) - require.NoError(t, err) - - // Add some resources to the database - mockRes1 := &mockResource{ - Kind: "TestResource", - Key: "test-key-1", - Mesh: "mesh1", - Meta: metav1.ObjectMeta{Name: "test-resource-1"}, - } - mockRes2 := &mockResource{ - Kind: "TestResource", - Key: "test-key-2", - Mesh: "mesh2", - Meta: metav1.ObjectMeta{Name: "test-resource-2"}, - } - err = store.Add(mockRes1) - require.NoError(t, err) - err = store.Add(mockRes2) - require.NoError(t, err) - - // Verify indices are populated - keys, err := store.IndexKeys("by-mesh", "mesh1") - assert.NoError(t, err) - assert.Contains(t, keys, "test-key-1") - - // Now simulate a restart by creating a new store instance with the same pool - // This simulates the scenario where existing data exists in the database - pool := store.pool - pool.IncrementRef() // Increment ref count since we're creating another store using it - - newStore := NewGormStore("TestResource", pool.Address(), pool) - - // Add indexers BEFORE Init to ensure they're available during index rebuild - err = newStore.AddIndexers(indexers) - require.NoError(t, err) - - // Init should rebuild indices from existing database data - err = newStore.Init(nil) - require.NoError(t, err) - - // Verify indices were rebuilt with existing data - keys, err = newStore.IndexKeys("by-mesh", "mesh1") - assert.NoError(t, err) - assert.Contains(t, keys, "test-key-1", "Index should contain existing data after Init()") - - keys, err = newStore.IndexKeys("by-mesh", "mesh2") - assert.NoError(t, err) - assert.Contains(t, keys, "test-key-2", "Index should contain existing data after Init()") - - // Verify all keys are present - allKeys := newStore.ListKeys() - assert.Len(t, allKeys, 2) -} - func TestGormStore_Resync(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() @@ -1475,3 +1403,290 @@ func TestGormStore_InvalidResourceType(t *testing.T) { _, _, err = store.Get("not-a-resource") assert.Error(t, err) } + +func TestGormStore_IndexPersistence(t *testing.T) { + store, cleanup := setupTestStore(t) + defer cleanup() + + err := store.Init(nil) + require.NoError(t, err) + + // Add indexer for IP addresses + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + // Simulate an IP address from the resource key + return []string{resource.ResourceKey()}, nil + }, + } + err = store.AddIndexers(indexers) + require.NoError(t, err) + + // Create and add resources with IP-like keys + mockRes1 := &mockResource{ + Kind: "TestResource", + Key: "192.168.1.1", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "resource-1"}, + } + mockRes2 := &mockResource{ + Kind: "TestResource", + Key: "192.168.1.2", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "resource-2"}, + } + + err = store.Add(mockRes1) + require.NoError(t, err) + err = store.Add(mockRes2) + require.NoError(t, err) + + // Verify indices are persisted to resource_indices table + db := store.pool.GetDB() + var count int64 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND index_name = ?", "TestResource", "by-ip"). + Count(&count).Error + assert.NoError(t, err) + assert.Equal(t, int64(2), count, "Both index entries should be persisted to resource_indices table") + + // Verify operator field is set to "Equals" + var entries []ResourceIndexModel + err = db.Where("resource_kind = ? AND index_name = ?", "TestResource", "by-ip"). + Find(&entries).Error + assert.NoError(t, err) + for _, entry := range entries { + assert.Equal(t, "Equals", entry.Operator, "Operator field should be set to Equals") + } +} + +func TestGormStore_ListByIndexes_HasPrefix(t *testing.T) { + store, cleanup := setupTestStore(t) + defer cleanup() + + err := store.Init(nil) + require.NoError(t, err) + + // Add indexer for IP addresses + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + return []string{resource.ResourceKey()}, nil + }, + } + err = store.AddIndexers(indexers) + require.NoError(t, err) + + // Create resources with IP-like keys + mockRes1 := &mockResource{ + Kind: "TestResource", + Key: "192.168.1.1", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "resource-1"}, + } + mockRes2 := &mockResource{ + Kind: "TestResource", + Key: "192.168.1.2", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "resource-2"}, + } + mockRes3 := &mockResource{ + Kind: "TestResource", + Key: "10.0.0.1", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "resource-3"}, + } + + err = store.Add(mockRes1) + require.NoError(t, err) + err = store.Add(mockRes2) + require.NoError(t, err) + err = store.Add(mockRes3) + require.NoError(t, err) + + // Query with HasPrefix operator + indexes := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168", Operator: index.HasPrefix}, + } + resources, err := store.ListByIndexes(indexes) + assert.NoError(t, err) + assert.Len(t, resources, 2) + + // Verify the correct resources were returned + keys := make([]string, len(resources)) + for i, res := range resources { + keys[i] = res.ResourceKey() + } + assert.Contains(t, keys, "192.168.1.1") + assert.Contains(t, keys, "192.168.1.2") + assert.NotContains(t, keys, "10.0.0.1") +} + +func TestGormStore_PageListByIndexes_HasPrefix(t *testing.T) { + store, cleanup := setupTestStore(t) + defer cleanup() + + err := store.Init(nil) + require.NoError(t, err) + + // Add indexer for IP addresses + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + return []string{resource.ResourceKey()}, nil + }, + } + err = store.AddIndexers(indexers) + require.NoError(t, err) + + // Create resources with IP-like keys + for i := 1; i <= 5; i++ { + mockRes := &mockResource{ + Kind: "TestResource", + Key: fmt.Sprintf("192.168.1.%d", i), + Mesh: "default", + Meta: metav1.ObjectMeta{Name: fmt.Sprintf("resource-%d", i)}, + } + err = store.Add(mockRes) + require.NoError(t, err) + } + + // Query with HasPrefix operator and pagination + indexes := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168", Operator: index.HasPrefix}, + } + pageReq := model.PageReq{ + PageOffset: 0, + PageSize: 2, + } + pageData, err := store.PageListByIndexes(indexes, pageReq) + assert.NoError(t, err) + assert.Equal(t, 5, pageData.Total) + assert.Len(t, pageData.Data, 2) + + // Verify second page + pageReq.PageOffset = 2 + pageData, err = store.PageListByIndexes(indexes, pageReq) + assert.NoError(t, err) + assert.Equal(t, 5, pageData.Total) + assert.Len(t, pageData.Data, 2) + + // Verify last page + pageReq.PageOffset = 4 + pageData, err = store.PageListByIndexes(indexes, pageReq) + assert.NoError(t, err) + assert.Equal(t, 5, pageData.Total) + assert.Len(t, pageData.Data, 1) +} + +func TestGormStore_DeleteIndex_RemovesFromDB(t *testing.T) { + store, cleanup := setupTestStore(t) + defer cleanup() + + err := store.Init(nil) + require.NoError(t, err) + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-name": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + return []string{resource.ResourceMeta().Name}, nil + }, + } + err = store.AddIndexers(indexers) + require.NoError(t, err) + + // Create and add resource + mockRes := &mockResource{ + Kind: "TestResource", + Key: "test-key", + Mesh: "default", + Meta: metav1.ObjectMeta{Name: "test-resource"}, + } + err = store.Add(mockRes) + require.NoError(t, err) + + // Verify index entry exists in DB + db := store.pool.GetDB() + var count int64 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND resource_key = ?", "TestResource", "test-key"). + Count(&count).Error + assert.NoError(t, err) + assert.Greater(t, count, int64(0), "Index entry should exist after Add") + + // Delete the resource + err = store.Delete(mockRes) + require.NoError(t, err) + + // Verify index entry is removed from DB + count = 0 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND resource_key = ?", "TestResource", "test-key"). + Count(&count).Error + assert.NoError(t, err) + assert.Equal(t, int64(0), count, "Index entry should be removed after Delete") +} + +func TestGormStore_UpdateIndex_UpdatesInDB(t *testing.T) { + store, cleanup := setupTestStore(t) + defer cleanup() + + err := store.Init(nil) + require.NoError(t, err) + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-mesh": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + return []string{resource.ResourceMesh()}, nil + }, + } + err = store.AddIndexers(indexers) + require.NoError(t, err) + + // Create and add resource + mockRes := &mockResource{ + Kind: "TestResource", + Key: "test-key", + Mesh: "mesh1", + Meta: metav1.ObjectMeta{Name: "test-resource"}, + } + err = store.Add(mockRes) + require.NoError(t, err) + + // Verify initial index entry in DB + db := store.pool.GetDB() + var count int64 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND resource_key = ? AND index_value = ?", "TestResource", "test-key", "mesh1"). + Count(&count).Error + assert.NoError(t, err) + assert.Equal(t, int64(1), count, "Initial index entry should exist") + + // Update resource with different mesh + updatedRes := &mockResource{ + Kind: "TestResource", + Key: "test-key", + Mesh: "mesh2", + Meta: metav1.ObjectMeta{Name: "test-resource"}, + } + err = store.Update(updatedRes) + require.NoError(t, err) + + // Verify old index entry is removed + count = 0 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND resource_key = ? AND index_value = ?", "TestResource", "test-key", "mesh1"). + Count(&count).Error + assert.NoError(t, err) + assert.Equal(t, int64(0), count, "Old index entry should be removed") + + // Verify new index entry exists + count = 0 + err = db.Model(&ResourceIndexModel{}). + Where("resource_kind = ? AND resource_key = ? AND index_value = ?", "TestResource", "test-key", "mesh2"). + Count(&count).Error + assert.NoError(t, err) + assert.Equal(t, int64(1), count, "New index entry should exist") +} diff --git a/pkg/store/dbcommon/index.go b/pkg/store/dbcommon/index.go deleted file mode 100644 index 5c5356195..000000000 --- a/pkg/store/dbcommon/index.go +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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 dbcommon - -import ( - "fmt" - "sync" - - set "github.com/duke-git/lancet/v2/datastructure/set" - "k8s.io/client-go/tools/cache" - - "github.com/apache/dubbo-admin/pkg/core/resource/model" -) - -// ValueIndex represents the mapping from indexed values to resource keys for a single index. -// Structure: map[indexedValue]set[resourceKey] -// Example: map["default"]{"resource1", "resource2", "resource3"} -type ValueIndex struct { - values map[string]set.Set[string] -} - -// NewValueIndex creates a new ValueIndex -func NewValueIndex() *ValueIndex { - return &ValueIndex{ - values: make(map[string]set.Set[string]), - } -} - -// Add adds a resource key to the specified indexed value -func (vi *ValueIndex) Add(indexedValue, resourceKey string) { - if vi.values[indexedValue] == nil { - vi.values[indexedValue] = set.New[string]() - } - vi.values[indexedValue].Add(resourceKey) -} - -// Remove removes a resource key from the specified indexed value -// Returns true if the value entry becomes empty after removal -func (vi *ValueIndex) Remove(indexedValue, resourceKey string) bool { - if vi.values[indexedValue] == nil { - return false - } - - vi.values[indexedValue].Delete(resourceKey) - - // Check if the set is now empty - if vi.values[indexedValue].Size() == 0 { - delete(vi.values, indexedValue) - return true - } - - return false -} - -// GetKeys returns all resource keys for the specified indexed value -func (vi *ValueIndex) GetKeys(indexedValue string) []string { - if vi.values[indexedValue] == nil { - return []string{} - } - return vi.values[indexedValue].ToSlice() -} - -// GetAllValues returns all indexed values in this ValueIndex -func (vi *ValueIndex) GetAllValues() []string { - if len(vi.values) == 0 { - return []string{} - } - - result := make([]string, 0, len(vi.values)) - for value := range vi.values { - result = append(result, value) - } - return result -} - -// IsEmpty returns true if the ValueIndex has no entries -func (vi *ValueIndex) IsEmpty() bool { - return len(vi.values) == 0 -} - -// Index is a thread-safe in-memory index structure that manages multiple named indices. -// Each index maps values to sets of resource keys. -// -// Structure: map[indexName]*ValueIndex -// Example: map["mesh"]*ValueIndex where ValueIndex contains {"default": {"res1", "res2"}} -type Index struct { - mu sync.RWMutex - indices map[string]*ValueIndex // map[indexName]*ValueIndex - indexers cache.Indexers // Index functions for creating indices -} - -// NewIndex creates a new empty Index instance -func NewIndex() *Index { - return &Index{ - indices: make(map[string]*ValueIndex), - indexers: cache.Indexers{}, - } -} - -// AddIndexers adds new indexer functions to the Index -// Returns an error if an indexer with the same name already exists -func (idx *Index) AddIndexers(newIndexers cache.Indexers) error { - idx.mu.Lock() - defer idx.mu.Unlock() - - for name, indexFunc := range newIndexers { - if _, exists := idx.indexers[name]; exists { - return fmt.Errorf("indexer %s already exists", name) - } - idx.indexers[name] = indexFunc - } - - return nil -} - -// GetIndexers returns a copy of all registered indexers -func (idx *Index) GetIndexers() cache.Indexers { - idx.mu.RLock() - defer idx.mu.RUnlock() - - result := make(cache.Indexers, len(idx.indexers)) - for k, v := range idx.indexers { - result[k] = v - } - return result -} - -// UpdateResource atomically updates all indices for a resource -// If oldResource is nil, it's treated as an add operation -// If oldResource is not nil, it's treated as an update operation (remove old, add new) -// This is a high-level atomic operation that handles all indexers internally -func (idx *Index) UpdateResource(newResource model.Resource, oldResource model.Resource) { - idx.mu.Lock() - defer idx.mu.Unlock() - - // Remove old resource from indices if this is an update - if oldResource != nil { - idx.removeResourceUnsafe(oldResource) - } - - // Add new resource to indices - idx.addResourceUnsafe(newResource) -} - -// RemoveResource atomically removes a resource from all indices -func (idx *Index) RemoveResource(resource model.Resource) { - idx.mu.Lock() - defer idx.mu.Unlock() - - idx.removeResourceUnsafe(resource) -} - -// GetKeys returns all resource keys for a given index name and value -// Returns an empty slice if the index name or value doesn't exist -func (idx *Index) GetKeys(indexName, indexValue string) []string { - idx.mu.RLock() - defer idx.mu.RUnlock() - - valueIndex := idx.indices[indexName] - if valueIndex == nil { - return []string{} - } - - return valueIndex.GetKeys(indexValue) -} - -// ListIndexFuncValues returns all indexed values for a given index name -// This directly retrieves values from the in-memory index without recalculating -func (idx *Index) ListIndexFuncValues(indexName string) []string { - idx.mu.RLock() - defer idx.mu.RUnlock() - - valueIndex := idx.indices[indexName] - if valueIndex == nil { - return []string{} - } - - return valueIndex.GetAllValues() -} - -// Clear removes all entries from the index -func (idx *Index) Clear() { - idx.mu.Lock() - defer idx.mu.Unlock() - idx.indices = make(map[string]*ValueIndex) -} - -// IndexExists checks if an indexer with the given name exists -func (idx *Index) IndexExists(indexName string) bool { - idx.mu.RLock() - defer idx.mu.RUnlock() - _, exists := idx.indexers[indexName] - return exists -} - -// addResourceUnsafe adds a resource to all indices (must be called with lock held) -func (idx *Index) addResourceUnsafe(resource model.Resource) { - for indexName, indexFunc := range idx.indexers { - values, err := indexFunc(resource) - if err != nil { - continue - } - - // Ensure the ValueIndex exists for this index name - if idx.indices[indexName] == nil { - idx.indices[indexName] = NewValueIndex() - } - - // Add resource key to each indexed value - for _, value := range values { - idx.indices[indexName].Add(value, resource.ResourceKey()) - } - } -} - -// removeResourceUnsafe removes a resource from all indices (must be called with lock held) -func (idx *Index) removeResourceUnsafe(resource model.Resource) { - for indexName, indexFunc := range idx.indexers { - values, err := indexFunc(resource) - if err != nil { - continue - } - - valueIndex := idx.indices[indexName] - if valueIndex == nil { - continue - } - - // Remove resource key from each indexed value - for _, value := range values { - valueIndex.Remove(value, resource.ResourceKey()) - } - - // Clean up empty ValueIndex - if valueIndex.IsEmpty() { - delete(idx.indices, indexName) - } - } -} diff --git a/pkg/store/dbcommon/model.go b/pkg/store/dbcommon/model.go index 5cf66c695..4bdecc137 100644 --- a/pkg/store/dbcommon/model.go +++ b/pkg/store/dbcommon/model.go @@ -123,3 +123,22 @@ func FromResource(resource model.Resource) (*ResourceModel, error) { Data: data, }, nil } + +// ResourceIndexModel represents a persisted index entry in the database +// This table stores index mappings to enable prefix queries and provide index persistence +// across multiple replicas in distributed deployments +// Table: resource_indices (shared across all resource kinds) +type ResourceIndexModel struct { + ID uint `gorm:"primarykey"` // Auto-incrementing primary key + ResourceKind string `gorm:"type:varchar(64);not null;index:idx_kind_name_value"` // Resource kind (e.g., "Instance") + IndexName string `gorm:"type:varchar(128);not null;index:idx_kind_name_value"` // Index name (e.g., "idx_instance_ip") + IndexValue string `gorm:"type:varchar(255);not null;index:idx_kind_name_value"` // Indexed value (e.g., "192.168.1.1") + ResourceKey string `gorm:"type:varchar(255);not null;index:idx_resource_key"` // Resource unique key + Operator string `gorm:"type:varchar(32);not null;default:Equals"` // Index operator type, e.g., Equals, HasPrefix +} + +// TableName specifies the table name for ResourceIndexModel +// Unlike ResourceModel which uses dynamic per-kind tables, resource_indices is a shared global table +func (ResourceIndexModel) TableName() string { + return "resource_indices" +} diff --git a/pkg/store/memory/store.go b/pkg/store/memory/store.go index 28ce04d2a..088490dcb 100644 --- a/pkg/store/memory/store.go +++ b/pkg/store/memory/store.go @@ -18,9 +18,13 @@ package memory import ( + "fmt" "reflect" "sort" + "strings" + "sync" + "github.com/armon/go-radix" set "github.com/duke-git/lancet/v2/datastructure/set" "github.com/duke-git/lancet/v2/slice" "k8s.io/client-go/tools/cache" @@ -33,8 +37,10 @@ import ( ) type resourceStore struct { - rk coremodel.ResourceKind - storeProxy cache.Indexer + rk coremodel.ResourceKind + storeProxy cache.Indexer + prefixTrees map[string]*radix.Tree + treesMu sync.RWMutex } var _ store.ManagedResourceStore = &resourceStore{} @@ -55,6 +61,11 @@ func (rs *resourceStore) Init(_ runtime.BuilderContext) error { }, indexers, ) + // Initialize RadixTree for each index for prefix matching support + rs.prefixTrees = make(map[string]*radix.Tree) + for indexName := range indexers { + rs.prefixTrees[indexName] = radix.New() + } return nil } @@ -63,14 +74,42 @@ func (rs *resourceStore) Start(_ runtime.Runtime, _ <-chan struct{}) error { } func (rs *resourceStore) Add(obj interface{}) error { - return rs.storeProxy.Add(obj) + if err := rs.storeProxy.Add(obj); err != nil { + return err + } + r, ok := obj.(coremodel.Resource) + if ok { + rs.addToTrees(r) + } + return nil } func (rs *resourceStore) Update(obj interface{}) error { - return rs.storeProxy.Update(obj) + r, ok := obj.(coremodel.Resource) + if ok { + // Get the old resource from the store to properly remove it from trees + oldObj, exists, err := rs.storeProxy.Get(r) + if exists && err == nil { + if oldRes, ok := oldObj.(coremodel.Resource); ok { + rs.removeFromTrees(oldRes) + } + } + } + if err := rs.storeProxy.Update(obj); err != nil { + return err + } + if ok { + // Add new entry with updated values + rs.addToTrees(r) + } + return nil } func (rs *resourceStore) Delete(obj interface{}) error { + r, ok := obj.(coremodel.Resource) + if ok { + rs.removeFromTrees(r) + } return rs.storeProxy.Delete(obj) } @@ -91,7 +130,25 @@ func (rs *resourceStore) GetByKey(key string) (item interface{}, exists bool, er } func (rs *resourceStore) Replace(i []interface{}, s string) error { - return rs.storeProxy.Replace(i, s) + // Clear all trees before replace + rs.treesMu.Lock() + for indexName := range rs.prefixTrees { + rs.prefixTrees[indexName] = radix.New() + } + rs.treesMu.Unlock() + + if err := rs.storeProxy.Replace(i, s); err != nil { + return err + } + + // Add all new resources to trees + for _, obj := range i { + r, ok := obj.(coremodel.Resource) + if ok { + rs.addToTrees(r) + } + } + return nil } func (rs *resourceStore) Resync() error { @@ -119,7 +176,20 @@ func (rs *resourceStore) GetIndexers() cache.Indexers { } func (rs *resourceStore) AddIndexers(newIndexers cache.Indexers) error { - return rs.storeProxy.AddIndexers(newIndexers) + rs.treesMu.Lock() + defer rs.treesMu.Unlock() + + if err := rs.storeProxy.AddIndexers(newIndexers); err != nil { + return err + } + + // Add RadixTrees for new indexers + for indexName := range newIndexers { + if _, exists := rs.prefixTrees[indexName]; !exists { + rs.prefixTrees[indexName] = radix.New() + } + } + return nil } func (rs *resourceStore) GetByKeys(keys []string) ([]coremodel.Resource, error) { @@ -141,7 +211,7 @@ func (rs *resourceStore) GetByKeys(keys []string) ([]coremodel.Resource, error) return resources, nil } -func (rs *resourceStore) ListByIndexes(indexes map[string]string) ([]coremodel.Resource, error) { +func (rs *resourceStore) ListByIndexes(indexes []index.IndexCondition) ([]coremodel.Resource, error) { keys, err := rs.getKeysByIndexes(indexes) if err != nil { return nil, err @@ -156,7 +226,7 @@ func (rs *resourceStore) ListByIndexes(indexes map[string]string) ([]coremodel.R return resources, nil } -func (rs *resourceStore) PageListByIndexes(indexes map[string]string, pq coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) { +func (rs *resourceStore) PageListByIndexes(indexes []index.IndexCondition, pq coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) { keys, err := rs.getKeysByIndexes(indexes) if err != nil { return nil, err @@ -183,17 +253,29 @@ func (rs *resourceStore) PageListByIndexes(indexes map[string]string, pq coremod return pageData, nil } -func (rs *resourceStore) getKeysByIndexes(indexes map[string]string) ([]string, error) { +func (rs *resourceStore) getKeysByIndexes(indexes []index.IndexCondition) ([]string, error) { if len(indexes) == 0 { return []string{}, nil } keySet := set.New[string]() first := true - for indexName, indexValue := range indexes { - keys, err := rs.storeProxy.IndexKeys(indexName, indexValue) + for _, condition := range indexes { + var keys []string + var err error + + switch condition.Operator { + case index.Equals: + keys, err = rs.storeProxy.IndexKeys(condition.IndexName, condition.Value) + case index.HasPrefix: + keys, err = rs.getKeysByPrefix(condition.IndexName, condition.Value) + default: + return nil, bizerror.New(bizerror.InvalidArgument, "operator not yet supported: "+string(condition.Operator)) + } + if err != nil { return nil, err } + if first { keySet = set.FromSlice(keys) first = false @@ -204,3 +286,77 @@ func (rs *resourceStore) getKeysByIndexes(indexes map[string]string) ([]string, } return keySet.ToSlice(), nil } + +// addToTrees adds a resource to all relevant RadixTrees for prefix matching +func (rs *resourceStore) addToTrees(resource coremodel.Resource) { + rs.treesMu.Lock() + defer rs.treesMu.Unlock() + + // Get indexers from storeProxy, not from global registry + // This ensures we include both init-time and dynamically-added indexers + indexers := rs.storeProxy.GetIndexers() + for indexName, indexFunc := range indexers { + values, err := indexFunc(resource) + if err != nil { + continue + } + tree, ok := rs.prefixTrees[indexName] + if !ok || tree == nil { + continue + } + for _, v := range values { + // Key format: "indexValue/resourceKey" + key := v + "/" + resource.ResourceKey() + tree.Insert(key, struct{}{}) + } + } +} + +// removeFromTrees removes a resource from all relevant RadixTrees +func (rs *resourceStore) removeFromTrees(resource coremodel.Resource) { + rs.treesMu.Lock() + defer rs.treesMu.Unlock() + + // Get indexers from storeProxy, not from global registry + // This ensures we include both init-time and dynamically-added indexers + indexers := rs.storeProxy.GetIndexers() + for indexName, indexFunc := range indexers { + values, err := indexFunc(resource) + if err != nil { + continue + } + tree, ok := rs.prefixTrees[indexName] + if !ok || tree == nil { + continue + } + for _, v := range values { + // Key format: "indexValue/resourceKey" + key := v + "/" + resource.ResourceKey() + tree.Delete(key) + } + } +} + +// getKeysByPrefix retrieves resource keys by prefix match using RadixTree +func (rs *resourceStore) getKeysByPrefix(indexName, prefix string) ([]string, error) { + rs.treesMu.RLock() + defer rs.treesMu.RUnlock() + + tree, ok := rs.prefixTrees[indexName] + if !ok { + return nil, fmt.Errorf("index %s does not exist", indexName) + } + + var keys []string + tree.WalkPrefix(prefix, func(k string, v interface{}) bool { + // Key format: "indexValue/resourceKey" + // Extract the resourceKey part (after the last "/") + idx := strings.LastIndex(k, "/") + if idx >= 0 && idx < len(k)-1 { + keys = append(keys, k[idx+1:]) + } + return false // Continue walking + }) + + return keys, nil +} diff --git a/pkg/store/memory/store_test.go b/pkg/store/memory/store_test.go index 8d1f1104a..8ad401995 100644 --- a/pkg/store/memory/store_test.go +++ b/pkg/store/memory/store_test.go @@ -28,6 +28,7 @@ import ( "k8s.io/client-go/tools/cache" "github.com/apache/dubbo-admin/pkg/core/resource/model" + "github.com/apache/dubbo-admin/pkg/core/store/index" ) // mockResource is a mock implementation of model.Resource for testing @@ -377,7 +378,7 @@ func TestResourceStore_ListByIndexes(t *testing.T) { assert.NoError(t, err) // List by indexes - indexes := map[string]string{"by-mesh": "mesh1"} + indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}} resources, err := store.ListByIndexes(indexes) assert.NoError(t, err) assert.Len(t, resources, 2) @@ -432,7 +433,7 @@ func TestResourceStore_PageListByIndexes(t *testing.T) { assert.NoError(t, err) // Page list by indexes - indexes := map[string]string{"by-mesh": "mesh1"} + indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}} pageReq := model.PageReq{ PageOffset: 0, PageSize: 2, @@ -543,9 +544,9 @@ func TestResourceStore_MultipleIndexes(t *testing.T) { } // Test multiple indexes - get all prod env resources in mesh1 - indexes := map[string]string{ - "by-mesh": "mesh1", - "by-version": "v1", + indexes := []index.IndexCondition{ + {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}, + {IndexName: "by-version", Value: "v1", Operator: index.Equals}, } result, err := store.ListByIndexes(indexes) assert.NoError(t, err) @@ -784,3 +785,425 @@ func TestResourceStore_ListIndexFuncValues(t *testing.T) { assert.Contains(t, values, "active") assert.Contains(t, values, "inactive") } + +func TestResourceStore_HasPrefixMatch(t *testing.T) { + store := NewMemoryResourceStore("TestInstance") + err := store.Init(nil) + assert.NoError(t, err) + + // Create mock resources with IP-like values for prefix matching + mockRes1 := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "192.168.1.10", + }, + }, + } + + mockRes2 := &mockResource{ + kind: "TestInstance", + key: "instance-2", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-2", + Labels: map[string]string{ + "ip": "192.168.1.20", + }, + }, + } + + mockRes3 := &mockResource{ + kind: "TestInstance", + key: "instance-3", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-3", + Labels: map[string]string{ + "ip": "10.0.0.5", + }, + }, + } + + mockRes4 := &mockResource{ + kind: "TestInstance", + key: "instance-4", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-4", + Labels: map[string]string{ + "ip": "192.168.2.1", + }, + }, + } + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + ip := resource.ResourceMeta().Labels["ip"] + return []string{ip}, nil + }, + } + err = store.AddIndexers(indexers) + assert.NoError(t, err) + + // Add resources + err = store.Add(mockRes1) + assert.NoError(t, err) + err = store.Add(mockRes2) + assert.NoError(t, err) + err = store.Add(mockRes3) + assert.NoError(t, err) + err = store.Add(mockRes4) + assert.NoError(t, err) + + // Test HasPrefix match: find all IPs starting with "192.168.1." + conditions := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.1.", Operator: index.HasPrefix}, + } + result, err := store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 2) + + // Verify the correct instances are returned + keys := make([]string, len(result)) + for i, res := range result { + keys[i] = res.ResourceKey() + } + assert.Contains(t, keys, "instance-1") + assert.Contains(t, keys, "instance-2") + + // Test HasPrefix match: find all IPs starting with "192.168." + conditions = []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.", Operator: index.HasPrefix}, + } + result, err = store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 3) + + keys = make([]string, len(result)) + for i, res := range result { + keys[i] = res.ResourceKey() + } + assert.Contains(t, keys, "instance-1") + assert.Contains(t, keys, "instance-2") + assert.Contains(t, keys, "instance-4") + + // Test HasPrefix match: find all IPs starting with "10." + conditions = []index.IndexCondition{ + {IndexName: "by-ip", Value: "10.", Operator: index.HasPrefix}, + } + result, err = store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "instance-3", result[0].ResourceKey()) +} + +func TestResourceStore_HasPrefixPageList(t *testing.T) { + store := NewMemoryResourceStore("TestInstance") + err := store.Init(nil) + assert.NoError(t, err) + + // Create mock resources + mockRes1 := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "192.168.1.10", + }, + }, + } + + mockRes2 := &mockResource{ + kind: "TestInstance", + key: "instance-2", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-2", + Labels: map[string]string{ + "ip": "192.168.1.20", + }, + }, + } + + mockRes3 := &mockResource{ + kind: "TestInstance", + key: "instance-3", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-3", + Labels: map[string]string{ + "ip": "192.168.1.30", + }, + }, + } + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + ip := resource.ResourceMeta().Labels["ip"] + return []string{ip}, nil + }, + } + err = store.AddIndexers(indexers) + assert.NoError(t, err) + + // Add resources + err = store.Add(mockRes1) + assert.NoError(t, err) + err = store.Add(mockRes2) + assert.NoError(t, err) + err = store.Add(mockRes3) + assert.NoError(t, err) + + // Test HasPrefix with pagination + conditions := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.1.", Operator: index.HasPrefix}, + } + pageReq := model.PageReq{ + PageOffset: 0, + PageSize: 2, + } + + pageData, err := store.PageListByIndexes(conditions, pageReq) + assert.NoError(t, err) + assert.Equal(t, 3, pageData.Total) + assert.Equal(t, 0, pageData.PageOffset) + assert.Equal(t, 2, pageData.PageSize) + assert.Len(t, pageData.Data, 2) + + // Test second page + pageReq.PageOffset = 2 + pageData, err = store.PageListByIndexes(conditions, pageReq) + assert.NoError(t, err) + assert.Equal(t, 3, pageData.Total) + assert.Equal(t, 2, pageData.PageOffset) + assert.Len(t, pageData.Data, 1) +} + +func TestResourceStore_HasPrefixWithMultipleConditions(t *testing.T) { + store := NewMemoryResourceStore("TestInstance") + err := store.Init(nil) + assert.NoError(t, err) + + // Create mock resources + mockRes1 := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "mesh1", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "192.168.1.10", + }, + }, + } + + mockRes2 := &mockResource{ + kind: "TestInstance", + key: "instance-2", + mesh: "mesh1", + meta: metav1.ObjectMeta{ + Name: "instance-2", + Labels: map[string]string{ + "ip": "192.168.1.20", + }, + }, + } + + mockRes3 := &mockResource{ + kind: "TestInstance", + key: "instance-3", + mesh: "mesh2", + meta: metav1.ObjectMeta{ + Name: "instance-3", + Labels: map[string]string{ + "ip": "192.168.1.30", + }, + }, + } + + // Add indexers + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + ip := resource.ResourceMeta().Labels["ip"] + return []string{ip}, nil + }, + "by-mesh": func(obj interface{}) ([]string, error) { + return []string{obj.(model.Resource).ResourceMesh()}, nil + }, + } + err = store.AddIndexers(indexers) + assert.NoError(t, err) + + // Add resources + err = store.Add(mockRes1) + assert.NoError(t, err) + err = store.Add(mockRes2) + assert.NoError(t, err) + err = store.Add(mockRes3) + assert.NoError(t, err) + + // Test combined: HasPrefix on IP AND Equals on mesh + conditions := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.1.", Operator: index.HasPrefix}, + {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals}, + } + result, err := store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 2) + + keys := make([]string, len(result)) + for i, res := range result { + keys[i] = res.ResourceKey() + } + assert.Contains(t, keys, "instance-1") + assert.Contains(t, keys, "instance-2") + assert.NotContains(t, keys, "instance-3") +} + +func TestResourceStore_HasPrefixAfterUpdate(t *testing.T) { + store := NewMemoryResourceStore("TestInstance") + err := store.Init(nil) + assert.NoError(t, err) + + // Create initial mock resource + mockRes1 := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "192.168.1.10", + }, + }, + } + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + ip := resource.ResourceMeta().Labels["ip"] + return []string{ip}, nil + }, + } + err = store.AddIndexers(indexers) + assert.NoError(t, err) + + // Add resource + err = store.Add(mockRes1) + assert.NoError(t, err) + + // Verify it matches 192.168.1. + conditions := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.1.", Operator: index.HasPrefix}, + } + result, err := store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 1) + + // Update resource with different IP + updatedRes := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "10.0.0.5", + }, + }, + } + err = store.Update(updatedRes) + assert.NoError(t, err) + + // Verify it no longer matches 192.168.1. + result, err = store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 0) + + // Verify it matches 10.0. + conditions = []index.IndexCondition{ + {IndexName: "by-ip", Value: "10.0.", Operator: index.HasPrefix}, + } + result, err = store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "instance-1", result[0].ResourceKey()) +} + +func TestResourceStore_HasPrefixAfterDelete(t *testing.T) { + store := NewMemoryResourceStore("TestInstance") + err := store.Init(nil) + assert.NoError(t, err) + + // Create mock resources + mockRes1 := &mockResource{ + kind: "TestInstance", + key: "instance-1", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-1", + Labels: map[string]string{ + "ip": "192.168.1.10", + }, + }, + } + + mockRes2 := &mockResource{ + kind: "TestInstance", + key: "instance-2", + mesh: "default", + meta: metav1.ObjectMeta{ + Name: "instance-2", + Labels: map[string]string{ + "ip": "192.168.1.20", + }, + }, + } + + // Add indexer + indexers := map[string]cache.IndexFunc{ + "by-ip": func(obj interface{}) ([]string, error) { + resource := obj.(model.Resource) + ip := resource.ResourceMeta().Labels["ip"] + return []string{ip}, nil + }, + } + err = store.AddIndexers(indexers) + assert.NoError(t, err) + + // Add resources + err = store.Add(mockRes1) + assert.NoError(t, err) + err = store.Add(mockRes2) + assert.NoError(t, err) + + // Verify both match 192.168.1. + conditions := []index.IndexCondition{ + {IndexName: "by-ip", Value: "192.168.1.", Operator: index.HasPrefix}, + } + result, err := store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 2) + + // Delete one resource + err = store.Delete(mockRes1) + assert.NoError(t, err) + + // Verify only one still matches + result, err = store.ListByIndexes(conditions) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "instance-2", result[0].ResourceKey()) +}