package data import ( "bytes" "encoding/json" "fmt" "net" "reflect" "sort" "strconv" "strings" ) // FeedPtr represents the dynamic metadata value in which a feed is providing the value. type FeedPtr struct { FeedID string `json:"feed,omitempty"` } // Meta contains information on an entity's metadata table. Metadata key/value // pairs are used by a record's filter pipeline during a dns query. // All values can be a feed id as well, indicating real-time updates of these values. // Structure/Precendence of metadata tables: // - Record // - Meta <- lowest precendence in filter // - Region(s) // - Meta <- middle precedence in filter chain // - ... // - Answer(s) // - Meta <- highest precedence in filter chain // - ... // - ... type Meta struct { // STATUS // Indicates whether or not entity is considered 'up' // bool or FeedPtr. Up interface{} `json:"up,omitempty"` // Indicates the number of active connections. // Values must be positive. // int or FeedPtr. Connections interface{} `json:"connections,omitempty"` // Indicates the number of active requests (HTTP or otherwise). // Values must be positive. // int or FeedPtr. Requests interface{} `json:"requests,omitempty"` // Indicates the "load average". // Values must be positive, and will be rounded to the nearest tenth. // float64 or FeedPtr. LoadAvg interface{} `json:"loadavg,omitempty"` // The Job ID of a Pulsar telemetry gathering job and routing granularities // to associate with. // string or FeedPtr. Pulsar interface{} `json:"pulsar,omitempty"` // GEOGRAPHICAL // Must be between -180.0 and +180.0 where negative // indicates South and positive indicates North. // e.g., the longitude of the datacenter where a server resides. // float64 or FeedPtr. Latitude interface{} `json:"latitude,omitempty"` // Must be between -180.0 and +180.0 where negative // indicates West and positive indicates East. // e.g., the longitude of the datacenter where a server resides. // float64 or FeedPtr. Longitude interface{} `json:"longitude,omitempty"` // Valid geographic regions are: 'US-EAST', 'US-CENTRAL', 'US-WEST', // 'EUROPE', 'ASIAPAC', 'SOUTH-AMERICA', 'AFRICA'. // e.g., the rough geographic location of the Datacenter where a server resides. // []string or FeedPtr. Georegion interface{} `json:"georegion,omitempty"` // Countr(ies) must be specified as ISO3166 2-character country code(s). // []string or FeedPtr. Country interface{} `json:"country,omitempty"` // State(s) must be specified as standard 2-character state code(s). // []string or FeedPtr. USState interface{} `json:"us_state,omitempty"` // Canadian Province(s) must be specified as standard 2-character province // code(s). // []string or FeedPtr. CAProvince interface{} `json:"ca_province,omitempty"` // INFORMATIONAL // Notes to indicate any necessary details for operators. // Up to 256 characters in length. // string or FeedPtr. Note interface{} `json:"note,omitempty"` // NETWORK // IP (v4 and v6) prefixes in CIDR format ("a.b.c.d/mask"). // May include up to 1000 prefixes. // e.g., "1.2.3.4/24" // []string or FeedPtr. IPPrefixes interface{} `json:"ip_prefixes,omitempty"` // Autonomous System (AS) number(s). // May include up to 1000 AS numbers. // []string or FeedPtr. ASN interface{} `json:"asn,omitempty"` // TRAFFIC // Indicates the "priority tier". // Lower values indicate higher priority. // Values must be positive. // int or FeedPtr. Priority interface{} `json:"priority,omitempty"` // Indicates a weight. // Filters that use weights normalize them. // Any positive values are allowed. // Values between 0 and 100 are recommended for simplicity's sake. // float64 or FeedPtr. Weight interface{} `json:"weight,omitempty"` // Indicates a "low watermark" to use for load shedding. // The value should depend on the metric used to determine // load (e.g., loadavg, connections, etc). // int or FeedPtr. LowWatermark interface{} `json:"low_watermark,omitempty"` // Indicates a "high watermark" to use for load shedding. // The value should depend on the metric used to determine // load (e.g., loadavg, connections, etc). // int or FeedPtr. HighWatermark interface{} `json:"high_watermark,omitempty"` } // StringMap returns a map[string]interface{} representation of metadata (for use with terraform in nested structures) func (meta *Meta) StringMap() map[string]interface{} { m := make(map[string]interface{}) v := reflect.Indirect(reflect.ValueOf(meta)) t := v.Type() for i := 0; i < t.NumField(); i++ { f := t.Field(i) fv := v.Field(i) if fv.IsNil() { continue } tag := f.Tag.Get("json") tag = strings.Split(tag, ",")[0] m[tag] = FormatInterface(fv.Interface()) } return m } // FormatInterface takes an interface of types: string, bool, int, float64, []string, and FeedPtr, and returns a string representation of said interface func FormatInterface(i interface{}) string { switch v := i.(type) { case string: return v case bool: if v { return "1" } return "0" case int: return strconv.FormatInt(int64(v), 10) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case []string: return strings.Join(v, ",") case []interface{}: slc := make([]string, 0) for _, s := range v { slc = append(slc, s.(string)) } return strings.Join(slc, ",") case FeedPtr: data, _ := json.Marshal(v) return string(data) default: panic(fmt.Sprintf("expected v to be convertible to a string, got: %+v, %T", v, v)) } } // ParseType returns an interface containing a string, bool, int, float64, []string, or FeedPtr // float64 values with no decimal may be returned as integers, but that should be ok because the api won't know the difference // when it's json encoded func ParseType(s string) interface{} { slc := strings.Split(s, ",") if len(slc) > 1 { sort.Strings(slc) return slc } feedptr := FeedPtr{} err := json.Unmarshal([]byte(s), &feedptr) if err == nil { return feedptr } f, err := strconv.ParseFloat(s, 64) if err == nil { if !isIntegral(f) { return f } return int(f) } return s } func isIntegral(f float64) bool { return f == float64(int(f)) } // MetaFromMap creates a *Meta and uses reflection to set fields from a map. This will panic if a value for a key is not a string. // This it to ensure compatibility with terraform func MetaFromMap(m map[string]interface{}) *Meta { meta := &Meta{} mv := reflect.Indirect(reflect.ValueOf(meta)) mt := mv.Type() for k, v := range m { name := ToCamel(k) if name == "UsState" { name = "USState" } else if name == "Loadavg" { name = "LoadAvg" } else if name == "CaProvince" { name = "CAProvince" } else if name == "IpPrefixes" { name = "IPPrefixes" } else if name == "Asn" { name = "ASN" } if _, ok := mt.FieldByName(name); ok { fv := mv.FieldByName(name) if name == "Up" { if v.(string) == "1" { fv.Set(reflect.ValueOf(true)) } else { fv.Set(reflect.ValueOf(false)) } } else { fv.Set(reflect.ValueOf(ParseType(v.(string)))) } } } return meta } // metaValidation is a validation struct for a metadata field. // It contains the kinds of types that the field can be, and a list of check functions that will run on the field type metaValidation struct { kinds []reflect.Kind checkFuncs []func(v reflect.Value) error } // validateLatLong makes sure that the given lat/long is within the range 180.0 to -180.0 func validateLatLong(v reflect.Value) error { if v.Kind() == reflect.Float64 { f := v.Interface().(float64) if f < -180.0 || f > 180.0 { return fmt.Errorf("latitude/longitude values must be between -180.0 and 180.0, got %f", f) } } return nil } // validateCidr makes sure that the given string is a valid cidr func validateCidr(v reflect.Value) error { if v.Kind() == reflect.String { s := v.Interface().(string) _, _, err := net.ParseCIDR(s) if err != nil { return err } } var last error if v.Kind() == reflect.Slice { slc := v.Interface().([]interface{}) for _, s := range slc { _, _, err := net.ParseCIDR(s.(string)) last = err } } return last } // validatePositiveNumber makes sure that the given number (float or int) is positive func validatePositiveNumber(fieldName string, v reflect.Value) error { i := 0 if v.Kind() == reflect.Int { i = v.Interface().(int) } if v.Kind() == reflect.Float64 { i = int(v.Interface().(float64)) } if i < 0 { return fmt.Errorf("%s must be a positive number, was %+v", fieldName, v.Interface()) } return nil } // geoMap is a map of all of the georegions var geoMap = map[string]struct{}{ "US-EAST": {}, "US-CENTRAL": {}, "US-WEST": {}, "EUROPE": {}, "ASIAPAC": {}, "SOUTH-AMERICA": {}, "AFRICA": {}, } // geoKeyString returns a string representation of all of the georegions func geoKeyString() string { length := 0 slc := make([]string, 0) for k := range geoMap { slc = append(slc, k) length += len(k) + 1 } sort.Strings(slc) b := bytes.NewBuffer(make([]byte, 0, length-1)) for _, k := range slc { b.WriteString(k + ",") } return strings.TrimRight(b.String(), ",") } // validateGeoregion makes sure that the given georegion is correct func validateGeoregion(v reflect.Value) error { if v.Kind() == reflect.String { s := v.String() if _, ok := geoMap[s]; !ok { return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s) } } if v.Kind() == reflect.Slice { if slc, ok := v.Interface().([]string); ok { for _, s := range slc { if _, ok := geoMap[s]; !ok { return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s) } } return nil } slc := v.Interface().([]interface{}) for _, s := range slc { if _, ok := geoMap[s.(string)]; !ok { return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s) } } } return nil } // validateCountryStateProvince makes sure that the given field only has two characters func validateCountryStateProvince(v reflect.Value) error { if v.Kind() == reflect.String { s := v.String() if len(s) != 2 { return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s) } } if v.Kind() == reflect.Slice { if slc, ok := v.Interface().([]string); ok { for _, s := range slc { if len(s) != 2 { return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s) } } return nil } slc := v.Interface().([]interface{}) for _, s := range slc { if len(s.(string)) != 2 { return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s) } } } return nil } // validateNoteLength validates that a note's length is less than 256 characters func validateNoteLength(v reflect.Value) error { if v.Kind() == reflect.String { s := v.String() if len(s) > 256 { return fmt.Errorf("note length must be less than 256 characters, was %d", len(s)) } } return nil } // checkFuncs is shorthand for returning a slice of functions that take a reflect.Value and return an error func checkFuncs(f ...func(v reflect.Value) error) []func(v reflect.Value) error { return f } // kinds is shorthand for returning a slice of reflect.Kind func kinds(k ...reflect.Kind) []reflect.Kind { return k } // validationMap is a map of meta fields to validation types and functions var validationMap = map[string]metaValidation{ "Up": {kinds(reflect.Bool), nil}, "Connections": {kinds(reflect.Int), checkFuncs( func(v reflect.Value) error { return validatePositiveNumber("Connections", v) })}, "Requests": {kinds(reflect.Int), checkFuncs( func(v reflect.Value) error { return validatePositiveNumber("Requests", v) })}, "LoadAvg": {kinds(reflect.Float64, reflect.Int), checkFuncs( func(v reflect.Value) error { return validatePositiveNumber("LoadAvg", v) })}, "Pulsar": {kinds(reflect.String), nil}, "Latitude": {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)}, "Longitude": {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)}, "Georegion": {kinds(reflect.String, reflect.Slice), checkFuncs(validateGeoregion)}, "Country": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)}, "USState": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)}, "CAProvince": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)}, "Note": {kinds(reflect.String), checkFuncs(validateNoteLength)}, "IPPrefixes": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCidr)}, "ASN": {kinds(reflect.String, reflect.Slice), nil}, "Priority": {kinds(reflect.Int), checkFuncs( func(v reflect.Value) error { return validatePositiveNumber("Priority", v) })}, "Weight": {kinds(reflect.Float64, reflect.Int), checkFuncs( func(v reflect.Value) error { return validatePositiveNumber("Weight", v) })}, "LowWatermark": {kinds(reflect.Int), nil}, "HighWatermark": {kinds(reflect.Int), nil}, } // validate takes a field name, a reflect value, and metaValidation and validates the given field func validate(name string, v reflect.Value, m metaValidation) (errs []error) { check := true // if this is a FeedPtr or a *FeedPtr then we're ok, skip checking the rest of the types if v.Kind() == reflect.Struct || v.Kind() == reflect.Invalid { check = false } if check { match := false for _, k := range m.kinds { if k == v.Kind() { match = true } } if !match { errs = append(errs, fmt.Errorf("found type mismatch for meta field '%s'. expected %+v, got: %+v", name, m.kinds, v.Kind())) } for _, f := range m.checkFuncs { err := f(v) if err != nil { errs = append(errs, err) } } } if v.Kind() == reflect.Struct { if _, ok := v.Interface().(FeedPtr); !ok { errs = append(errs, fmt.Errorf("if a meta field is a struct, it must be a FeedPtr, got: %s", v.Type())) } } return } // Validate validates metadata fields and returns a list of errors if any are found func (meta *Meta) Validate() (errs []error) { mv := reflect.Indirect(reflect.ValueOf(meta)) mt := mv.Type() for i := 0; i < mt.NumField(); i++ { fv := mt.Field(i) err := validate(fv.Name, mv.Field(i).Elem(), validationMap[fv.Name]) if err != nil { errs = append(errs, err...) } } return errs }