From c00c8827f96eec373089e1dc08edf7f7279bb2d3 Mon Sep 17 00:00:00 2001 From: rinfx <893383980@qq.com> Date: Mon, 15 Jul 2024 14:00:02 +0800 Subject: [PATCH] support service-level match config (#1112) --- plugins/wasm-go/pkg/matcher/rule_matcher.go | 62 ++++++++++++-- .../wasm-go/pkg/matcher/rule_matcher_test.go | 84 ++++++++++++++++++- plugins/wasm-go/pkg/matcher/utils.go | 8 ++ 3 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 plugins/wasm-go/pkg/matcher/utils.go diff --git a/plugins/wasm-go/pkg/matcher/rule_matcher.go b/plugins/wasm-go/pkg/matcher/rule_matcher.go index 86b3d0b76..317044634 100644 --- a/plugins/wasm-go/pkg/matcher/rule_matcher.go +++ b/plugins/wasm-go/pkg/matcher/rule_matcher.go @@ -29,6 +29,7 @@ type Category int const ( Route Category = iota Host + Service ) type MatchType int @@ -40,9 +41,10 @@ const ( ) const ( - RULES_KEY = "_rules_" - MATCH_ROUTE_KEY = "_match_route_" - MATCH_DOMAIN_KEY = "_match_domain_" + RULES_KEY = "_rules_" + MATCH_ROUTE_KEY = "_match_route_" + MATCH_DOMAIN_KEY = "_match_domain_" + MATCH_SERVICE_KEY = "_match_service_" ) type HostMatcher struct { @@ -53,6 +55,7 @@ type HostMatcher struct { type RuleConfig[PluginConfig any] struct { category Category routes map[string]struct{} + services map[string]struct{} hosts []HostMatcher config PluginConfig } @@ -72,14 +75,25 @@ func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) { if err != nil && err != types.ErrorStatusNotFound { return nil, err } + serviceName, err := proxywasm.GetProperty([]string{"cluster_name"}) + if err != nil && err != types.ErrorStatusNotFound { + return nil, err + } for _, rule := range m.ruleConfig { + // category == Host if rule.category == Host { if m.hostMatch(rule, host) { return &rule.config, nil } } // 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 } } @@ -137,15 +151,19 @@ func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result, } rule.routes = m.parseRouteMatchConfig(ruleJson) rule.hosts = m.parseHostMatchConfig(ruleJson) + rule.services = m.parseServiceMatchConfig(ruleJson) noRoute := len(rule.routes) == 0 noHosts := len(rule.hosts) == 0 - if (noRoute && noHosts) || (!noRoute && !noHosts) { - return errors.New("there is only one of '_match_route_' and '_match_domain_' can present in configuration.") + noService := len(rule.services) == 0 + 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 { rule.category = Route - } else { + } else if !noHosts { rule.category = Host + } else { + rule.category = Service } m.ruleConfig = append(m.ruleConfig, rule) } @@ -164,6 +182,18 @@ func (m RuleMatcher[PluginConfig]) parseRouteMatchConfig(config gjson.Result) ma 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 { keys := config.Get(MATCH_DOMAIN_KEY).Array() var hostMatchers []HostMatcher @@ -224,3 +254,21 @@ func (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqH } 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 +} diff --git a/plugins/wasm-go/pkg/matcher/rule_matcher_test.go b/plugins/wasm-go/pkg/matcher/rule_matcher_test.go index a475f441b..7b4b84605 100644 --- a/plugins/wasm-go/pkg/matcher/rule_matcher_test.go +++ b/plugins/wasm-go/pkg/matcher/rule_matcher_test.go @@ -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) { cases := []struct { name string @@ -173,7 +229,7 @@ func TestParseRuleConfig(t *testing.T) { }, { 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]{ ruleConfig: []RuleConfig[customConfig]{ { @@ -196,7 +252,8 @@ func TestParseRuleConfig(t *testing.T) { host: "www.abc.com", }, }, - routes: map[string]struct{}{}, + routes: map[string]struct{}{}, + services: map[string]struct{}{}, config: customConfig{ name: "john", age: 18, @@ -208,6 +265,19 @@ func TestParseRuleConfig(t *testing.T) { "test1": {}, "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{ name: "ann", age: 16, @@ -224,12 +294,17 @@ func TestParseRuleConfig(t *testing.T) { { name: "invalid rule", 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", 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 { @@ -303,6 +378,7 @@ func TestParseOverrideConfig(t *testing.T) { "r1": {}, "r2": {}, }, + services: map[string]struct{}{}, config: completeConfig{ consumers: []string{"c1", "c2", "c3"}, allow: []string{"c1", "c3"}, diff --git a/plugins/wasm-go/pkg/matcher/utils.go b/plugins/wasm-go/pkg/matcher/utils.go new file mode 100644 index 000000000..daa8c1b1c --- /dev/null +++ b/plugins/wasm-go/pkg/matcher/utils.go @@ -0,0 +1,8 @@ +package matcher + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +}