You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

152 lines
3.0 KiB

  1. package config
  2. import (
  3. "errors"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "reflect"
  8. "strings"
  9. "github.com/utahta/go-openuri"
  10. "github.com/ghodss/yaml"
  11. )
  12. // Policy validates a number of rules against a given configuration.
  13. //
  14. type Policy struct {
  15. Enforcements []Enforcement `json:"enforcements"`
  16. }
  17. // Validate checks the given config against all policy enforcements.
  18. //
  19. func (pol Policy) Validate(config Config) error {
  20. validate := newValidator()
  21. for _, enforcement := range pol.Enforcements {
  22. cfg, err := ResolveJSONPath(enforcement.Path, config)
  23. if err != nil {
  24. // If the path resolved nothing, there's nothing to enforce
  25. continue
  26. }
  27. // Flags are a special case in which the True field should be compared
  28. // against the validator, not the struct itself.
  29. if flag, ok := cfg.(Flag); ok {
  30. cfg = flag.True
  31. }
  32. err = validate.Var(cfg, enforcement.Rule)
  33. if err != nil {
  34. return fmt.Errorf(
  35. `value for "%s" violates policy rule "%s"`,
  36. enforcement.Path, enforcement.Rule,
  37. )
  38. }
  39. }
  40. return nil
  41. }
  42. // Enforcement represents a policy rule and config path on which to apply it.
  43. //
  44. type Enforcement struct {
  45. Path string `json:"path"`
  46. Rule string `json:"rule"`
  47. }
  48. // ReadYAMLPolicy converts YAML input to JSON and returns a new Policy struct.
  49. //
  50. func ReadYAMLPolicy(data []byte) (*Policy, error) {
  51. jsonData, err := yaml.YAMLToJSON(data)
  52. if err != nil {
  53. return nil, err
  54. }
  55. return ReadPolicy(jsonData)
  56. }
  57. // ReadPolicy unmarshals the given YAML/json bytes into a new Policy struct.
  58. //
  59. func ReadPolicy(data []byte) (*Policy, error) {
  60. var policy Policy
  61. err := json.Unmarshal(data, &policy)
  62. if err != nil {
  63. return nil, err
  64. }
  65. return &policy, err
  66. }
  67. // ReadPolicyFromURI fetches the policy file from the given URL or file path
  68. // and loads its contents with ReadPolicy.
  69. //
  70. func ReadPolicyFromURI(uri string) (*Policy, error) {
  71. io, err := openuri.Open(uri)
  72. if err != nil {
  73. return nil, err
  74. }
  75. defer io.Close()
  76. data, err := ioutil.ReadAll(io)
  77. if err != nil {
  78. return nil, err
  79. }
  80. return ReadYAMLPolicy(data)
  81. }
  82. // ResolveJSONPath returns the config value found at the given JSON-ish
  83. // namespace/path (e.g. "variants.production.runs.as").
  84. //
  85. func ResolveJSONPath(path string, cfg interface{}) (interface{}, error) {
  86. parts := strings.SplitN(path, ".", 2)
  87. name := parts[0]
  88. v := reflect.ValueOf(cfg)
  89. t := v.Type()
  90. var subcfg interface{}
  91. switch t.Kind() {
  92. case reflect.Struct:
  93. for i := 0; i < t.NumField(); i++ {
  94. if t.Field(i).Anonymous {
  95. if subsubcfg, err := ResolveJSONPath(path, v.Field(i).Interface()); err == nil {
  96. return subsubcfg, nil
  97. }
  98. }
  99. if name == resolveJSONTagName(t.Field(i)) {
  100. subcfg = v.Field(i).Interface()
  101. break
  102. }
  103. }
  104. case reflect.Map:
  105. if t.Key().Kind() == reflect.String {
  106. for _, key := range v.MapKeys() {
  107. if key.Interface().(string) == name {
  108. subcfg = v.MapIndex(key).Interface()
  109. break
  110. }
  111. }
  112. }
  113. }
  114. if subcfg == nil {
  115. return nil, errors.New("invalid path")
  116. }
  117. if len(parts) > 1 {
  118. return ResolveJSONPath(parts[1], subcfg)
  119. }
  120. return subcfg, nil
  121. }