160 lines
3.9 KiB
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
|
|
}
|