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

package config
import (
"errors"
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"strings"
"github.com/utahta/go-openuri"
"github.com/ghodss/yaml"
)
// Policy validates a number of rules against a given configuration.
//
type Policy struct {
Enforcements []Enforcement `json:"enforcements"`
}
// Validate checks the given config against all policy enforcements.
//
func (pol Policy) Validate(config Config) error {
validate := newValidator()
for _, enforcement := range pol.Enforcements {
cfg, err := ResolveJSONPath(enforcement.Path, config)
if err != nil {
// If the path resolved nothing, there's nothing to enforce
continue
}
// Flags are a special case in which the True field should be compared
// against the validator, not the struct itself.
if flag, ok := cfg.(Flag); ok {
cfg = flag.True
}
err = validate.Var(cfg, enforcement.Rule)
if err != nil {
return fmt.Errorf(
`value for "%s" violates policy rule "%s"`,
enforcement.Path, enforcement.Rule,
)
}
}
return nil
}
// Enforcement represents a policy rule and config path on which to apply it.
//
type Enforcement struct {
Path string `json:"path"`
Rule string `json:"rule"`
}
// ReadYAMLPolicy converts YAML input to JSON and returns a new Policy struct.
//
func ReadYAMLPolicy(data []byte) (*Policy, error) {
jsonData, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, err
}
return ReadPolicy(jsonData)
}
// ReadPolicy unmarshals the given YAML/json bytes into a new Policy struct.
//
func ReadPolicy(data []byte) (*Policy, error) {
var policy Policy
err := json.Unmarshal(data, &policy)
if err != nil {
return nil, err
}
return &policy, err
}
// ReadPolicyFromURI fetches the policy file from the given URL or file path
// and loads its contents with ReadPolicy.
//
func ReadPolicyFromURI(uri string) (*Policy, error) {
io, err := openuri.Open(uri)
if err != nil {
return nil, err
}
defer io.Close()
data, err := ioutil.ReadAll(io)
if err != nil {
return nil, err
}
return ReadYAMLPolicy(data)
}
// ResolveJSONPath returns the config value found at the given JSON-ish
// namespace/path (e.g. "variants.production.runs.as").
//
func ResolveJSONPath(path string, cfg interface{}) (interface{}, error) {
parts := strings.SplitN(path, ".", 2)
name := parts[0]
v := reflect.ValueOf(cfg)
t := v.Type()
var subcfg interface{}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
if t.Field(i).Anonymous {
if subsubcfg, err := ResolveJSONPath(path, v.Field(i).Interface()); err == nil {
return subsubcfg, nil
}
}
if name == resolveJSONTagName(t.Field(i)) {
subcfg = v.Field(i).Interface()
break
}
}
case reflect.Map:
if t.Key().Kind() == reflect.String {
for _, key := range v.MapKeys() {
if key.Interface().(string) == name {
subcfg = v.MapIndex(key).Interface()
break
}
}
}
}
if subcfg == nil {
return nil, errors.New("invalid path")
}
if len(parts) > 1 {
return ResolveJSONPath(parts[1], subcfg)
}
return subcfg, nil
}