support service-level match config (#1112)

This commit is contained in:
rinfx
2024-07-15 14:00:02 +08:00
committed by GitHub
parent 46218058d1
commit c00c8827f9
3 changed files with 143 additions and 11 deletions

View File

@@ -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
}

View File

@@ -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"},

View File

@@ -0,0 +1,8 @@
package matcher
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}