mirror of
https://github.com/alibaba/higress.git
synced 2026-05-27 22:27:29 +08:00
support service-level match config (#1112)
This commit is contained in:
@@ -29,6 +29,7 @@ type Category int
|
|||||||
const (
|
const (
|
||||||
Route Category = iota
|
Route Category = iota
|
||||||
Host
|
Host
|
||||||
|
Service
|
||||||
)
|
)
|
||||||
|
|
||||||
type MatchType int
|
type MatchType int
|
||||||
@@ -40,9 +41,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RULES_KEY = "_rules_"
|
RULES_KEY = "_rules_"
|
||||||
MATCH_ROUTE_KEY = "_match_route_"
|
MATCH_ROUTE_KEY = "_match_route_"
|
||||||
MATCH_DOMAIN_KEY = "_match_domain_"
|
MATCH_DOMAIN_KEY = "_match_domain_"
|
||||||
|
MATCH_SERVICE_KEY = "_match_service_"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HostMatcher struct {
|
type HostMatcher struct {
|
||||||
@@ -53,6 +55,7 @@ type HostMatcher struct {
|
|||||||
type RuleConfig[PluginConfig any] struct {
|
type RuleConfig[PluginConfig any] struct {
|
||||||
category Category
|
category Category
|
||||||
routes map[string]struct{}
|
routes map[string]struct{}
|
||||||
|
services map[string]struct{}
|
||||||
hosts []HostMatcher
|
hosts []HostMatcher
|
||||||
config PluginConfig
|
config PluginConfig
|
||||||
}
|
}
|
||||||
@@ -72,14 +75,25 @@ func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
|
|||||||
if err != nil && err != types.ErrorStatusNotFound {
|
if err != nil && err != types.ErrorStatusNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
serviceName, err := proxywasm.GetProperty([]string{"cluster_name"})
|
||||||
|
if err != nil && err != types.ErrorStatusNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, rule := range m.ruleConfig {
|
for _, rule := range m.ruleConfig {
|
||||||
|
// category == Host
|
||||||
if rule.category == Host {
|
if rule.category == Host {
|
||||||
if m.hostMatch(rule, host) {
|
if m.hostMatch(rule, host) {
|
||||||
return &rule.config, nil
|
return &rule.config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// category == Route
|
// category == Route
|
||||||
if _, ok := rule.routes[string(routeName)]; ok {
|
if rule.category == Route {
|
||||||
|
if _, ok := rule.routes[string(routeName)]; ok {
|
||||||
|
return &rule.config, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// category == Cluster
|
||||||
|
if m.serviceMatch(rule, string(serviceName)) {
|
||||||
return &rule.config, nil
|
return &rule.config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,15 +151,19 @@ func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
|
|||||||
}
|
}
|
||||||
rule.routes = m.parseRouteMatchConfig(ruleJson)
|
rule.routes = m.parseRouteMatchConfig(ruleJson)
|
||||||
rule.hosts = m.parseHostMatchConfig(ruleJson)
|
rule.hosts = m.parseHostMatchConfig(ruleJson)
|
||||||
|
rule.services = m.parseServiceMatchConfig(ruleJson)
|
||||||
noRoute := len(rule.routes) == 0
|
noRoute := len(rule.routes) == 0
|
||||||
noHosts := len(rule.hosts) == 0
|
noHosts := len(rule.hosts) == 0
|
||||||
if (noRoute && noHosts) || (!noRoute && !noHosts) {
|
noService := len(rule.services) == 0
|
||||||
return errors.New("there is only one of '_match_route_' and '_match_domain_' can present in configuration.")
|
if boolToInt(noRoute)+boolToInt(noService)+boolToInt(noHosts) != 2 {
|
||||||
|
return errors.New("there is only one of '_match_route_', '_match_domain_' and '_match_service_' can present in configuration.")
|
||||||
}
|
}
|
||||||
if !noRoute {
|
if !noRoute {
|
||||||
rule.category = Route
|
rule.category = Route
|
||||||
} else {
|
} else if !noHosts {
|
||||||
rule.category = Host
|
rule.category = Host
|
||||||
|
} else {
|
||||||
|
rule.category = Service
|
||||||
}
|
}
|
||||||
m.ruleConfig = append(m.ruleConfig, rule)
|
m.ruleConfig = append(m.ruleConfig, rule)
|
||||||
}
|
}
|
||||||
@@ -164,6 +182,18 @@ func (m RuleMatcher[PluginConfig]) parseRouteMatchConfig(config gjson.Result) ma
|
|||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m RuleMatcher[PluginConfig]) parseServiceMatchConfig(config gjson.Result) map[string]struct{} {
|
||||||
|
keys := config.Get(MATCH_SERVICE_KEY).Array()
|
||||||
|
clusters := make(map[string]struct{})
|
||||||
|
for _, item := range keys {
|
||||||
|
clusterName := item.String()
|
||||||
|
if clusterName != "" {
|
||||||
|
clusters[clusterName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusters
|
||||||
|
}
|
||||||
|
|
||||||
func (m RuleMatcher[PluginConfig]) parseHostMatchConfig(config gjson.Result) []HostMatcher {
|
func (m RuleMatcher[PluginConfig]) parseHostMatchConfig(config gjson.Result) []HostMatcher {
|
||||||
keys := config.Get(MATCH_DOMAIN_KEY).Array()
|
keys := config.Get(MATCH_DOMAIN_KEY).Array()
|
||||||
var hostMatchers []HostMatcher
|
var hostMatchers []HostMatcher
|
||||||
@@ -224,3 +254,21 @@ func (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqH
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m RuleMatcher[PluginConfig]) serviceMatch(rule RuleConfig[PluginConfig], serviceName string) bool {
|
||||||
|
parts := strings.Split(serviceName, "|")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
port := parts[1]
|
||||||
|
fqdn := parts[3]
|
||||||
|
for configServiceName := range rule.services {
|
||||||
|
colonIndex := strings.LastIndexByte(configServiceName, ':')
|
||||||
|
if colonIndex != -1 && fqdn == string(configServiceName[:colonIndex]) && port == string(configServiceName[colonIndex+1:]) {
|
||||||
|
return true
|
||||||
|
} else if fqdn == string(configServiceName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,6 +153,62 @@ func TestHostMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceMatch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
config RuleConfig[customConfig]
|
||||||
|
service string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fqdn",
|
||||||
|
config: RuleConfig[customConfig]{
|
||||||
|
services: map[string]struct{}{
|
||||||
|
"qwen.dns": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: "outbound|443||qwen.dns",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fqdn with port",
|
||||||
|
config: RuleConfig[customConfig]{
|
||||||
|
services: map[string]struct{}{
|
||||||
|
"qwen.dns:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: "outbound|443||qwen.dns",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not match",
|
||||||
|
config: RuleConfig[customConfig]{
|
||||||
|
services: map[string]struct{}{
|
||||||
|
"moonshot.dns:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: "outbound|443||qwen.dns",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error config format",
|
||||||
|
config: RuleConfig[customConfig]{
|
||||||
|
services: map[string]struct{}{
|
||||||
|
"qwen.dns:": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: "outbound|443||qwen.dns",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
var m RuleMatcher[customConfig]
|
||||||
|
assert.Equal(t, c.result, m.serviceMatch(c.config, c.service))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseRuleConfig(t *testing.T) {
|
func TestParseRuleConfig(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -173,7 +229,7 @@ func TestParseRuleConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rules config",
|
name: "rules config",
|
||||||
config: `{"_rules_":[{"_match_domain_":["*.example.com","www.*","*","www.abc.com"],"name":"john", "age":18},{"_match_route_":["test1","test2"],"name":"ann", "age":16}]}`,
|
config: `{"_rules_":[{"_match_domain_":["*.example.com","www.*","*","www.abc.com"],"name":"john", "age":18},{"_match_route_":["test1","test2"],"name":"ann", "age":16},{"_match_service_":["test1.dns","test2.static:8080"],"name":"ann", "age":16}]}`,
|
||||||
expected: RuleMatcher[customConfig]{
|
expected: RuleMatcher[customConfig]{
|
||||||
ruleConfig: []RuleConfig[customConfig]{
|
ruleConfig: []RuleConfig[customConfig]{
|
||||||
{
|
{
|
||||||
@@ -196,7 +252,8 @@ func TestParseRuleConfig(t *testing.T) {
|
|||||||
host: "www.abc.com",
|
host: "www.abc.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routes: map[string]struct{}{},
|
routes: map[string]struct{}{},
|
||||||
|
services: map[string]struct{}{},
|
||||||
config: customConfig{
|
config: customConfig{
|
||||||
name: "john",
|
name: "john",
|
||||||
age: 18,
|
age: 18,
|
||||||
@@ -208,6 +265,19 @@ func TestParseRuleConfig(t *testing.T) {
|
|||||||
"test1": {},
|
"test1": {},
|
||||||
"test2": {},
|
"test2": {},
|
||||||
},
|
},
|
||||||
|
services: map[string]struct{}{},
|
||||||
|
config: customConfig{
|
||||||
|
name: "ann",
|
||||||
|
age: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: Service,
|
||||||
|
services: map[string]struct{}{
|
||||||
|
"test1.dns": {},
|
||||||
|
"test2.static:8080": {},
|
||||||
|
},
|
||||||
|
routes: map[string]struct{}{},
|
||||||
config: customConfig{
|
config: customConfig{
|
||||||
name: "ann",
|
name: "ann",
|
||||||
age: 16,
|
age: 16,
|
||||||
@@ -224,12 +294,17 @@ func TestParseRuleConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "invalid rule",
|
name: "invalid rule",
|
||||||
config: `{"_rules_":[{"_match_domain_":["*"],"_match_route_":["test"]}]}`,
|
config: `{"_rules_":[{"_match_domain_":["*"],"_match_route_":["test"]}]}`,
|
||||||
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
|
errMsg: "there is only one of '_match_route_', '_match_domain_' and '_match_service_' can present in configuration.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid rule",
|
||||||
|
config: `{"_rules_":[{"_match_domain_":["*"],"_match_service_":["test.dns"]}]}`,
|
||||||
|
errMsg: "there is only one of '_match_route_', '_match_domain_' and '_match_service_' can present in configuration.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid rule",
|
name: "invalid rule",
|
||||||
config: `{"_rules_":[{"age":16}]}`,
|
config: `{"_rules_":[{"age":16}]}`,
|
||||||
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
|
errMsg: "there is only one of '_match_route_', '_match_domain_' and '_match_service_' can present in configuration.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -303,6 +378,7 @@ func TestParseOverrideConfig(t *testing.T) {
|
|||||||
"r1": {},
|
"r1": {},
|
||||||
"r2": {},
|
"r2": {},
|
||||||
},
|
},
|
||||||
|
services: map[string]struct{}{},
|
||||||
config: completeConfig{
|
config: completeConfig{
|
||||||
consumers: []string{"c1", "c2", "c3"},
|
consumers: []string{"c1", "c2", "c3"},
|
||||||
allow: []string{"c1", "c3"},
|
allow: []string{"c1", "c3"},
|
||||||
|
|||||||
8
plugins/wasm-go/pkg/matcher/utils.go
Normal file
8
plugins/wasm-go/pkg/matcher/utils.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package matcher
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user