Files
configcenter/internal/gray/matcher.go

160 lines
3.9 KiB
Go

package gray
import (
"crypto/sha256"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"sort"
"strings"
"github.com/longpeng/configcenter/internal/domain"
)
const (
RuleIP = "ip"
RuleInstance = "instance"
RulePercentage = "percentage"
)
type Target struct {
IP string
Instance string
}
type ipRule struct {
IPs []string `json:"ips"`
}
type instanceRule struct {
Instances []string `json:"instances"`
}
type percentageRule struct {
Percentage int `json:"percentage"`
Salt string `json:"salt"`
}
func Validate(rule domain.GrayRule) error {
if rule.NamespaceID <= 0 || rule.EnvironmentID <= 0 || len(rule.Overrides) == 0 {
return fmt.Errorf("scope and overrides are required")
}
for key := range rule.Overrides {
if strings.TrimSpace(key) == "" {
return fmt.Errorf("override key must not be empty")
}
}
switch rule.RuleType {
case RuleIP:
var value ipRule
if err := strictUnmarshal(rule.RuleValue, &value); err != nil || len(value.IPs) == 0 {
return fmt.Errorf("ip rule requires a non-empty ips array")
}
for _, item := range value.IPs {
if net.ParseIP(item) == nil {
if _, _, err := net.ParseCIDR(item); err != nil {
return fmt.Errorf("invalid IP or CIDR %q", item)
}
}
}
case RuleInstance:
var value instanceRule
if err := strictUnmarshal(rule.RuleValue, &value); err != nil || len(value.Instances) == 0 {
return fmt.Errorf("instance rule requires a non-empty instances array")
}
case RulePercentage:
var value percentageRule
if err := strictUnmarshal(rule.RuleValue, &value); err != nil || value.Percentage < 0 || value.Percentage > 100 {
return fmt.Errorf("percentage rule requires percentage between 0 and 100")
}
default:
return fmt.Errorf("unsupported gray rule type %q", rule.RuleType)
}
return nil
}
func Apply(base map[string]string, rules []domain.GrayRule, target Target) (map[string]string, []int64) {
result := clone(base)
sorted := append([]domain.GrayRule(nil), rules...)
sort.SliceStable(sorted, func(i, j int) bool {
if sorted[i].Priority == sorted[j].Priority {
return sorted[i].ID < sorted[j].ID
}
return sorted[i].Priority < sorted[j].Priority
})
matched := make([]int64, 0)
for _, rule := range sorted {
if !rule.Enabled || !matches(rule, target) {
continue
}
for key, value := range rule.Overrides {
result[key] = value
}
matched = append(matched, rule.ID)
}
return result, matched
}
func matches(rule domain.GrayRule, target Target) bool {
switch rule.RuleType {
case RuleIP:
parsed := net.ParseIP(target.IP)
if parsed == nil {
return false
}
var value ipRule
if json.Unmarshal(rule.RuleValue, &value) != nil {
return false
}
for _, item := range value.IPs {
if ip := net.ParseIP(item); ip != nil && ip.Equal(parsed) {
return true
}
if _, network, err := net.ParseCIDR(item); err == nil && network.Contains(parsed) {
return true
}
}
case RuleInstance:
var value instanceRule
if target.Instance == "" || json.Unmarshal(rule.RuleValue, &value) != nil {
return false
}
for _, instance := range value.Instances {
if instance == target.Instance {
return true
}
}
case RulePercentage:
identity := target.Instance
if identity == "" {
identity = target.IP
}
if identity == "" {
return false
}
var value percentageRule
if json.Unmarshal(rule.RuleValue, &value) != nil {
return false
}
digest := sha256.Sum256([]byte(value.Salt + "\x00" + identity))
bucket := binary.BigEndian.Uint64(digest[:8]) % 100
return int(bucket) < value.Percentage
}
return false
}
func strictUnmarshal(payload []byte, target any) error {
decoder := json.NewDecoder(strings.NewReader(string(payload)))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
func clone(input map[string]string) map[string]string {
result := make(map[string]string, len(input))
for key, value := range input {
result[key] = value
}
return result
}