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 }