blob: a10d0b05c98078506a2e1059ffa9883d31bc1f4e [file] [log] [blame]
package validate
import (
"encoding/base64"
"net/url"
"regexp"
"go.skia.org/infra/go/skerr"
sheriff_configpb "go.skia.org/infra/perf/go/sheriffconfig/proto/v1"
"google.golang.org/protobuf/encoding/prototext"
)
// Does the following checks at Pattern level:
// - The match and exclude strings are in correct formats.
// - All pattern specify at least 1 key.
// - Exclude patterns only specify 1 key.
// - If a value starts with "~", check that the rest of the string is a
// compileable Regex.
func validatePattern(pattern string, singleField bool) error {
query, err := url.ParseQuery(pattern)
if err != nil {
return skerr.Fmt("Pattern '%s' has incorrect format: %s", pattern, err)
}
if len(query) == 0 {
return skerr.Fmt("Pattern must have at least 1 key declared.")
}
if singleField && len(query) > 1 {
return skerr.Fmt("Pattern must only have 1 key declared.")
}
for key, values := range query {
if len(values) == 0 {
return skerr.Fmt("Key must have at least 1 explicit value declared. Key: %s.", key)
}
for _, value := range values {
if len(value) == 0 {
return skerr.Fmt("Explicit value for key must be non-empty. Key: %s.", key)
}
if value[:1] == "~" {
_, err := regexp.Compile(value[1:])
if err != nil {
return skerr.Fmt("Invalid Regex for '%s' key: %s.", key, value[1:])
}
}
}
}
return nil
}
// Does the following checks at Anomaly Config level:
// - At least 1 matching pattern exists.
func validateAnomalyConfig(ac *sheriff_configpb.AnomalyConfig) error {
if len(ac.Rules.Match) == 0 {
return skerr.Fmt("Anomaly config must have at least one match pattern.")
}
for i, pattern := range ac.Rules.Match {
err := validatePattern(pattern, false)
if err != nil {
return skerr.Fmt("Error for Match Pattern at index %d: %s.", i, err)
}
}
for i, pattern := range ac.Rules.Exclude {
err := validatePattern(pattern, true)
if err != nil {
return skerr.Fmt("Error for Exclude Pattern at index %d: %s.", i, err)
}
}
return nil
}
// Does the following checks at subscription level:
// - No missing Name, ContactEmail or BugComponent.
// - At least 1 Anomaly Config defined.
func validateSubscription(sub *sheriff_configpb.Subscription) error {
if sub.Name == "" {
return skerr.Fmt("Missing name.")
}
if sub.ContactEmail == "" {
return skerr.Fmt("Subscription '%s' is missing contact_email.", sub.Name)
}
if sub.BugComponent == "" {
return skerr.Fmt("Subscription '%s' is missing bug_component.", sub.Name)
}
if len(sub.AnomalyConfigs) == 0 {
return skerr.Fmt("Subscription '%s' must have at least one Anomaly Config.", sub.Name)
}
for i, anomalyConfig := range sub.AnomalyConfigs {
err := validateAnomalyConfig(anomalyConfig)
if err != nil {
return skerr.Fmt("Error for Anomaly Config at index %d: %s.", i, err)
}
}
return nil
}
// Does the following checks at config level:
// - There's at least 1 subscription defined.
// - All subscriptions have unique names.
func ValidateConfig(config *sheriff_configpb.SheriffConfig) error {
if len(config.Subscriptions) == 0 {
return skerr.Fmt("Config must have at least one Subscription.")
}
namesSeen := make(map[string]bool)
for i, sub := range config.Subscriptions {
// Check if we have duplicate subscription names.
if _, exists := namesSeen[sub.Name]; exists {
return skerr.Fmt("Found duplicated subscription name: %s. Names must be unique.", sub.Name)
}
namesSeen[sub.Name] = true
err := validateSubscription(sub)
if err != nil {
return skerr.Fmt("Error for Subscription at index %d: %s.", i, err)
}
}
return nil
}
// Transform Base64 encoded data into SheriffConfig proto.
// LUCI Config returns content encoded in base64. It then needs to be
// Unmarshaled into Sheriff Config proto.
func DeserializeProto(encoded string) (*sheriff_configpb.SheriffConfig, error) {
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, skerr.Fmt("Failed to decode Base64 string: %s", err)
}
config := &sheriff_configpb.SheriffConfig{}
err = prototext.Unmarshal(decoded, config)
if err != nil {
return nil, skerr.Fmt("Failed to unmarshal prototext: %s", err)
}
return config, nil
}