1 package data
2 3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net"
8 "reflect"
9 "sort"
10 "strconv"
11 "strings"
12 )
13 14 // FeedPtr represents the dynamic metadata value in which a feed is providing the value.
15 type FeedPtr struct {
16 FeedID string `json:"feed,omitempty"`
17 }
18 19 // PulsarMeta is currently only used for validation
20 type PulsarMeta struct {
21 JobID string `json:"job_id,omitempty"`
22 Bias string `json:"bias,omitempty"`
23 A5MCutoff float64 `json:"a5m_cutoff,omitempty"`
24 }
25 26 // Meta contains information on an entity's metadata table. Metadata key/value
27 // pairs are used by a record's filter pipeline during a dns query.
28 // All values can be a feed id as well, indicating real-time updates of these values.
29 // Structure/Precedence of metadata tables:
30 // - Record
31 // - Meta <- lowest precedence in filter
32 // - Region(s)
33 // - Meta <- middle precedence in filter chain
34 // - ...
35 // - Answer(s)
36 // - Meta <- highest precedence in filter chain
37 // - ...
38 // - ...
39 type Meta struct {
40 // STATUS
41 42 // Indicates whether or not entity is considered 'up'
43 // bool or FeedPtr.
44 Up interface{} `json:"up,omitempty"`
45 46 // Indicates the number of active connections.
47 // Values must be positive.
48 // int or FeedPtr.
49 Connections interface{} `json:"connections,omitempty"`
50 51 // Indicates the number of active requests (HTTP or otherwise).
52 // Values must be positive.
53 // int or FeedPtr.
54 Requests interface{} `json:"requests,omitempty"`
55 56 // Indicates the "load average".
57 // Values must be positive, and will be rounded to the nearest tenth.
58 // float64 or FeedPtr.
59 LoadAvg interface{} `json:"loadavg,omitempty"`
60 61 // The Job ID of a Pulsar telemetry gathering job and associated metadata.
62 // list of PulsarMeta
63 Pulsar interface{} `json:"pulsar,omitempty"`
64 65 // GEOGRAPHICAL
66 67 // Must be between -180.0 and +180.0 where negative
68 // indicates South and positive indicates North.
69 // e.g., the longitude of the datacenter where a server resides.
70 // float64 or FeedPtr.
71 Latitude interface{} `json:"latitude,omitempty"`
72 73 // Must be between -180.0 and +180.0 where negative
74 // indicates West and positive indicates East.
75 // e.g., the longitude of the datacenter where a server resides.
76 // float64 or FeedPtr.
77 Longitude interface{} `json:"longitude,omitempty"`
78 79 // Valid geographic regions are: 'US-EAST', 'US-CENTRAL', 'US-WEST',
80 // 'EUROPE', 'ASIAPAC', 'SOUTH-AMERICA', 'AFRICA'.
81 // e.g., the rough geographic location of the Datacenter where a server resides.
82 // []string or FeedPtr.
83 Georegion interface{} `json:"georegion,omitempty"`
84 85 // Countr(ies) must be specified as ISO3166 2-character country code(s).
86 // []string or FeedPtr.
87 Country interface{} `json:"country,omitempty"`
88 89 // State(s) must be specified as standard 2-character state code(s).
90 // []string or FeedPtr.
91 USState interface{} `json:"us_state,omitempty"`
92 93 // Canadian Province(s) must be specified as standard 2-character province
94 // code(s).
95 // []string or FeedPtr.
96 CAProvince interface{} `json:"ca_province,omitempty"`
97 98 // INFORMATIONAL
99 100 // Notes to indicate any necessary details for operators.
101 // Up to 256 characters in length.
102 // string or FeedPtr.
103 Note interface{} `json:"note,omitempty"`
104 105 // NETWORK
106 107 // IP (v4 and v6) prefixes in CIDR format ("a.b.c.d/mask").
108 // May include up to 1000 prefixes.
109 // e.g., "1.2.3.4/24"
110 // []string or FeedPtr.
111 IPPrefixes interface{} `json:"ip_prefixes,omitempty"`
112 113 // Autonomous System (AS) number(s).
114 // May include up to 1000 AS numbers.
115 // []string or FeedPtr.
116 ASN interface{} `json:"asn,omitempty"`
117 118 // TRAFFIC
119 120 // Indicates the "priority tier".
121 // Lower values indicate higher priority.
122 // Values must be positive.
123 // int or FeedPtr.
124 Priority interface{} `json:"priority,omitempty"`
125 126 // Indicates a weight.
127 // Filters that use weights normalize them.
128 // Any positive values are allowed.
129 // Values between 0 and 100 are recommended for simplicity's sake.
130 // float64 or FeedPtr.
131 Weight interface{} `json:"weight,omitempty"`
132 133 // Indicates a cost.
134 // Filters that use costs normalize them.
135 // Any positive values are allowed.
136 // float64 or FeedPtr.
137 Cost interface{} `json:"cost,omitempty"`
138 139 // Indicates a "low watermark" to use for load shedding.
140 // The value should depend on the metric used to determine
141 // load (e.g., loadavg, connections, etc).
142 // int or FeedPtr.
143 LowWatermark interface{} `json:"low_watermark,omitempty"`
144 145 // Indicates a "high watermark" to use for load shedding.
146 // The value should depend on the metric used to determine
147 // load (e.g., loadavg, connections, etc).
148 // int or FeedPtr.
149 HighWatermark interface{} `json:"high_watermark,omitempty"`
150 151 // subdivisions must follow the ISO-3166-2 code for a country and subdivisions
152 // map[string]interface{} or FeedPtr.
153 Subdivisions interface{} `json:"subdivisions,omitempty"`
154 155 AdditionalMetadata interface{} `json:"additional_metadata,omitempty"`
156 }
157 158 // StringMap returns a map[string]interface{} representation of metadata (for use with terraform in nested structures)
159 func (meta *Meta) StringMap() map[string]interface{} {
160 m := make(map[string]interface{})
161 v := reflect.Indirect(reflect.ValueOf(meta))
162 t := v.Type()
163 for i := 0; i < t.NumField(); i++ {
164 f := t.Field(i)
165 fv := v.Field(i)
166 if fv.IsNil() {
167 continue
168 }
169 tag := f.Tag.Get("json")
170 171 tag = strings.Split(tag, ",")[0]
172 173 m[tag] = FormatInterface(fv.Interface())
174 }
175 return m
176 }
177 178 // FormatInterface takes an interface of types: string, bool, int, float64, []string, map[string]interface{} and FeedPtr, and returns a string representation of said interface
179 func FormatInterface(i interface{}) string {
180 switch v := i.(type) {
181 case string:
182 return v
183 case bool:
184 if v {
185 return "1"
186 }
187 return "0"
188 case int:
189 return strconv.FormatInt(int64(v), 10)
190 case float64:
191 return strconv.FormatFloat(v, 'f', -1, 64)
192 case []string:
193 return strings.Join(v, ",")
194 case []interface{}:
195 slc := make([]string, 0)
196 for _, s := range v {
197 switch ss := s.(type) {
198 // Pulsar
199 case map[string]interface{}:
200 data, _ := json.Marshal(v)
201 return string(data)
202 case string:
203 slc = append(slc, ss)
204 // The ASN field specifically is returned from the API as an integer,
205 // which Go treats as a float64 when it parses the json,
206 // so this is to account for that field.
207 case float64:
208 slc = append(slc, strconv.FormatFloat(ss, 'f', -1, 64))
209 }
210 }
211 return strings.Join(slc, ",")
212 case map[string]interface{}:
213 // Required for Terraform workaround to allow users to submit raw json of feed pointer
214 // as value for metadata. See https://github.com/terraform-providers/terraform-provider-ns1/issues/35
215 if val, ok := v["feed"].(string); ok {
216 feedPtr := FeedPtr{FeedID: val}
217 data, _ := json.Marshal(feedPtr)
218 return string(data)
219 }
220 data, _ := json.Marshal(v)
221 return string(data)
222 case FeedPtr:
223 data, _ := json.Marshal(v)
224 return string(data)
225 default:
226 panic(fmt.Sprintf("expected v to be convertible to a string, got: %+v, %T", v, v))
227 }
228 }
229 230 // ParseType returns an interface containing a string, bool, int, float64, []string, or FeedPtr
231 // float64 values with no decimal may be returned as integers, but that should be ok because the api won't know the difference
232 // when it's json encoded
233 func ParseType(s string) interface{} {
234 slc := strings.Split(s, ",")
235 if len(slc) > 1 {
236 sort.Strings(slc)
237 return slc
238 }
239 240 feedptr := FeedPtr{}
241 err := json.Unmarshal([]byte(s), &feedptr)
242 if err == nil {
243 return feedptr
244 }
245 246 f, err := strconv.ParseFloat(s, 64)
247 if err == nil {
248 if !isIntegral(f) {
249 return f
250 }
251 return int(f)
252 }
253 254 return s
255 }
256 257 func isIntegral(f float64) bool {
258 return f == float64(int(f))
259 }
260 261 // 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.
262 // This it to ensure compatibility with terraform
263 func MetaFromMap(m map[string]interface{}) *Meta {
264 meta := &Meta{}
265 mv := reflect.Indirect(reflect.ValueOf(meta))
266 mt := mv.Type()
267 for k, v := range m {
268 name := ToCamel(k)
269 switch name {
270 case "UsState":
271 name = "USState"
272 case "Loadavg":
273 name = "LoadAvg"
274 case "CaProvince":
275 name = "CAProvince"
276 case "IpPrefixes":
277 name = "IPPrefixes"
278 case "Asn":
279 name = "ASN"
280 }
281 if _, ok := mt.FieldByName(name); ok {
282 fv := mv.FieldByName(name)
283 switch name {
284 case "Up":
285 if v.(string) == "1" || strings.ToLower(v.(string)) == "true" {
286 fv.Set(reflect.ValueOf(true))
287 } else if v.(string) == "0" || strings.ToLower(v.(string)) == "false" {
288 fv.Set(reflect.ValueOf(false))
289 } else {
290 fv.Set(reflect.ValueOf(ParseType(v.(string))))
291 }
292 case "ASN":
293 // If there is only one ASN, it should still be treated as a string.-
294 // otherwise this gets parsed into a float64 and breaks stuff.
295 i := strings.Index(v.(string), ",")
296 if i == -1 {
297 fv.Set(reflect.ValueOf(v.(string)))
298 } else {
299 fv.Set(reflect.ValueOf(ParseType(v.(string))))
300 }
301 case "Pulsar":
302 var pulsars []map[string]interface{}
303 if err := json.Unmarshal([]byte(v.(string)), &pulsars); err == nil {
304 fv.Set(reflect.ValueOf(pulsars))
305 }
306 case "Subdivisions":
307 switch v.(type) {
308 case string:
309 var subMap map[string]interface{}
310 json.Unmarshal([]byte(v.(string)), &subMap)
311 fv.Set(reflect.ValueOf(subMap))
312 case map[string]interface{}:
313 fv.Set(reflect.ValueOf(v.(map[string]interface{})))
314 }
315 case "Note":
316 // If it's a Note, just pass the string without any type of parse.
317 fv.Set(reflect.ValueOf(v.(string)))
318 case "AdditionalMetadata":
319 var additional []map[string]interface{}
320 if err := json.Unmarshal([]byte(v.(string)), &additional); err == nil {
321 fv.Set(reflect.ValueOf(additional))
322 }
323 default:
324 fv.Set(reflect.ValueOf(ParseType(v.(string))))
325 }
326 }
327 }
328 return meta
329 }
330 331 // metaValidation is a validation struct for a metadata field.
332 // It contains the kinds of types that the field can be, and a list of check functions that will run on the field
333 type metaValidation struct {
334 kinds []reflect.Kind
335 checkFuncs []func(v reflect.Value) error
336 }
337 338 // validateLatLong makes sure that the given lat/long is within the range 180.0 to -180.0
339 func validateLatLong(v reflect.Value) error {
340 if v.Kind() == reflect.Float64 {
341 f := v.Interface().(float64)
342 if f < -180.0 || f > 180.0 {
343 return fmt.Errorf("latitude/longitude values must be between -180.0 and 180.0, got %f", f)
344 }
345 }
346 return nil
347 }
348 349 // validateCidr makes sure that the given string is a valid cidr
350 func validateCidr(v reflect.Value) error {
351 if v.Kind() == reflect.String {
352 s := v.Interface().(string)
353 _, _, err := net.ParseCIDR(s)
354 if err != nil {
355 return err
356 }
357 }
358 if v.Kind() == reflect.Slice {
359 if slc, ok := v.Interface().([]string); ok {
360 for _, s := range slc {
361 _, _, err := net.ParseCIDR(s)
362 if err != nil {
363 return fmt.Errorf("%s is not a valid CIDR block", s)
364 }
365 }
366 return nil
367 }
368 slc := v.Interface().([]interface{})
369 for _, s := range slc {
370 _, _, err := net.ParseCIDR(s.(string))
371 if err != nil {
372 return fmt.Errorf("%s is not a valid CIDR block", s.(string))
373 }
374 }
375 }
376 return nil
377 }
378 379 // validatePositiveNumber makes sure that the given number (float or int) is positive
380 func validatePositiveNumber(fieldName string, v reflect.Value) error {
381 i := 0
382 if v.Kind() == reflect.Int {
383 i = v.Interface().(int)
384 385 }
386 387 if v.Kind() == reflect.Float64 {
388 i = int(v.Interface().(float64))
389 }
390 391 if i < 0 {
392 return fmt.Errorf("%s must be a positive number, was %+v", fieldName, v.Interface())
393 }
394 395 return nil
396 }
397 398 // geoMap is a map of all of the georegions
399 var geoMap = map[string]struct{}{
400 "US-EAST": {}, "US-CENTRAL": {}, "US-WEST": {},
401 "EUROPE": {}, "ASIAPAC": {}, "SOUTH-AMERICA": {}, "AFRICA": {},
402 }
403 404 // geoKeyString returns a string representation of all of the georegions
405 func geoKeyString() string {
406 length := 0
407 slc := make([]string, 0)
408 for k := range geoMap {
409 slc = append(slc, k)
410 length += len(k) + 1
411 }
412 sort.Strings(slc)
413 414 b := bytes.NewBuffer(make([]byte, 0, length-1))
415 416 for _, k := range slc {
417 b.WriteString(k + ",")
418 }
419 420 return strings.TrimRight(b.String(), ",")
421 }
422 423 // validateGeoregion makes sure that the given georegion is correct
424 func validateGeoregion(v reflect.Value) error {
425 if v.Kind() == reflect.String {
426 s := v.String()
427 if _, ok := geoMap[s]; !ok {
428 return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
429 }
430 }
431 432 if v.Kind() == reflect.Slice {
433 if slc, ok := v.Interface().([]string); ok {
434 for _, s := range slc {
435 if _, ok := geoMap[s]; !ok {
436 return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
437 }
438 }
439 return nil
440 }
441 slc := v.Interface().([]interface{})
442 for _, s := range slc {
443 if _, ok := geoMap[s.(string)]; !ok {
444 return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
445 }
446 }
447 }
448 return nil
449 }
450 451 // validateCountryStateProvince makes sure that the given field only has two characters
452 func validateCountryStateProvince(v reflect.Value) error {
453 if v.Kind() == reflect.String {
454 s := v.String()
455 if len(s) != 2 {
456 return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
457 }
458 }
459 460 if v.Kind() == reflect.Slice {
461 if slc, ok := v.Interface().([]string); ok {
462 for _, s := range slc {
463 if len(s) != 2 {
464 return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
465 }
466 }
467 return nil
468 }
469 slc := v.Interface().([]interface{})
470 for _, s := range slc {
471 if len(s.(string)) != 2 {
472 return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
473 }
474 }
475 }
476 return nil
477 }
478 479 // validateNoteLength validates that a note's length is less than 256 characters
480 func validateNoteLength(v reflect.Value) error {
481 if v.Kind() == reflect.String {
482 s := v.String()
483 if len(s) > 256 {
484 return fmt.Errorf("note length must be less than 256 characters, was %d", len(s))
485 }
486 }
487 return nil
488 }
489 490 func validatePulsar(v reflect.Value) error {
491 var pulsars []*PulsarMeta
492 493 switch v.Kind() {
494 case reflect.Slice:
495 // Slice from API
496 bs, err := json.Marshal(v.Interface())
497 if err != nil {
498 return fmt.Errorf("pulsar: unexpected value: `%v`", v.Interface())
499 }
500 if err := json.Unmarshal(bs, &pulsars); err != nil {
501 return fmt.Errorf("pulsar: invalid value: `%v`", v.Interface())
502 }
503 case reflect.String:
504 // String from terraform
505 if err := json.Unmarshal([]byte(v.String()), &pulsars); err != nil {
506 return fmt.Errorf("pulsar: invalid value: `%v`", v.String())
507 }
508 }
509 510 for _, p := range pulsars {
511 if p.JobID == "" {
512 return fmt.Errorf("pulsar Job ID is required")
513 }
514 }
515 return nil
516 }
517 518 func validateAdditionalMetadata(v reflect.Value) error {
519 // API expects additional_metadata to be array of length 1
520 if v.Len() > 1 {
521 return fmt.Errorf("unexpected length of `%d`, expected 1", v.Len())
522 }
523 524 return nil
525 }
526 527 // checkFuncs is shorthand for returning a slice of functions that take a reflect.Value and return an error
528 func checkFuncs(f ...func(v reflect.Value) error) []func(v reflect.Value) error {
529 return f
530 }
531 532 // kinds is shorthand for returning a slice of reflect.Kind
533 func kinds(k ...reflect.Kind) []reflect.Kind {
534 return k
535 }
536 537 // validationMap is a map of meta fields to validation types and functions
538 var validationMap = map[string]metaValidation{
539 "Up": {kinds(reflect.Bool), nil},
540 "Connections": {kinds(reflect.Int), checkFuncs(
541 func(v reflect.Value) error {
542 return validatePositiveNumber("Connections", v)
543 })},
544 "Requests": {kinds(reflect.Int), checkFuncs(
545 func(v reflect.Value) error {
546 return validatePositiveNumber("Requests", v)
547 })},
548 "LoadAvg": {kinds(reflect.Float64, reflect.Int), checkFuncs(
549 func(v reflect.Value) error {
550 return validatePositiveNumber("LoadAvg", v)
551 })},
552 "Pulsar": {kinds(reflect.String, reflect.Slice), checkFuncs(validatePulsar)},
553 "Latitude": {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)},
554 "Longitude": {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)},
555 "Georegion": {kinds(reflect.String, reflect.Slice), checkFuncs(validateGeoregion)},
556 "Country": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
557 "USState": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
558 "CAProvince": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
559 "Note": {kinds(reflect.String), checkFuncs(validateNoteLength)},
560 "IPPrefixes": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCidr)},
561 "ASN": {kinds(reflect.String, reflect.Slice), nil},
562 "Priority": {kinds(reflect.Int), checkFuncs(
563 func(v reflect.Value) error {
564 return validatePositiveNumber("Priority", v)
565 })},
566 "Weight": {kinds(reflect.Float64, reflect.Int), checkFuncs(
567 func(v reflect.Value) error {
568 return validatePositiveNumber("Weight", v)
569 })},
570 "Cost": {kinds(reflect.Float64, reflect.Int), checkFuncs(
571 func(v reflect.Value) error {
572 return validatePositiveNumber("Cost", v)
573 })},
574 "LowWatermark": {kinds(reflect.Int), nil},
575 "HighWatermark": {kinds(reflect.Int), nil},
576 "Subdivisions": {kinds(reflect.String, reflect.Map), nil},
577 "AdditionalMetadata": {kinds(reflect.String, reflect.Slice), checkFuncs(validateAdditionalMetadata)},
578 }
579 580 // validate takes a field name, a reflect value, and metaValidation and validates the given field
581 func validate(name string, v reflect.Value, m metaValidation) (errs []error) {
582 583 check := true
584 // if this is a FeedPtr or a *FeedPtr then we're ok, skip checking the rest of the types
585 if v.Kind() == reflect.Struct || v.Kind() == reflect.Invalid {
586 check = false
587 }
588 589 if check {
590 match := false
591 for _, k := range m.kinds {
592 if k == v.Kind() {
593 match = true
594 }
595 }
596 597 if !match {
598 errs = append(errs, fmt.Errorf("found type mismatch for meta field '%s'. expected %+v, got: %+v", name, m.kinds, v.Kind()))
599 }
600 601 for _, f := range m.checkFuncs {
602 err := f(v)
603 if err != nil {
604 errs = append(errs, err)
605 }
606 }
607 }
608 609 if v.Kind() == reflect.Struct {
610 if _, ok := v.Interface().(FeedPtr); !ok {
611 errs = append(errs, fmt.Errorf("if a meta field is a struct, it must be a FeedPtr, got: %s", v.Type()))
612 }
613 }
614 615 return
616 }
617 618 // Validate validates metadata fields and returns a list of errors if any are found
619 func (meta *Meta) Validate() (errs []error) {
620 mv := reflect.Indirect(reflect.ValueOf(meta))
621 mt := mv.Type()
622 for i := 0; i < mt.NumField(); i++ {
623 fv := mt.Field(i)
624 err := validate(fv.Name, mv.Field(i).Elem(), validationMap[fv.Name])
625 if err != nil {
626 errs = append(errs, err...)
627 }
628 }
629 630 return errs
631 }
632