summaryrefslogtreecommitdiff
path: root/config
diff options
context:
space:
mode:
Diffstat (limited to 'config')
-rw-r--r--config/common.go12
-rw-r--r--config/policy.go141
-rw-r--r--config/policy_test.go117
-rw-r--r--config/validation.go31
4 files changed, 291 insertions, 10 deletions
diff --git a/config/common.go b/config/common.go
index a597a82..4cc300d 100644
--- a/config/common.go
+++ b/config/common.go
@@ -9,12 +9,12 @@ import (
//
type CommonConfig struct {
Base string `yaml:"base" validate:"omitempty,baseimage"` // name/path to base image
- Apt AptConfig `yaml:"apt"` // APT related configuration
- Node NodeConfig `yaml:"node"` // Node related configuration
- Python PythonConfig `yaml:"python"` // Python related configuration
- Lives LivesConfig `yaml:"lives"` // application owner/dir configuration
- Runs RunsConfig `yaml:"runs"` // runtime environment configuration
- SharedVolume Flag `yaml:"sharedvolume"` // define a volume for application
+ Apt AptConfig `yaml:"apt"` // APT related
+ Node NodeConfig `yaml:"node"` // Node related
+ Python PythonConfig `yaml:"python"` // Python related
+ Lives LivesConfig `yaml:"lives"` // application owner/dir
+ Runs RunsConfig `yaml:"runs"` // runtime environment
+ SharedVolume Flag `yaml:"sharedvolume"` // use volume for app
EntryPoint []string `yaml:"entrypoint"` // entry-point executable
}
diff --git a/config/policy.go b/config/policy.go
new file mode 100644
index 0000000..25fe02a
--- /dev/null
+++ b/config/policy.go
@@ -0,0 +1,141 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "reflect"
+ "strings"
+
+ "github.com/utahta/go-openuri"
+ "gopkg.in/yaml.v2"
+)
+
+// Policy validates a number of rules against a given configuration.
+//
+type Policy struct {
+ Enforcements []Enforcement `yaml:"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 := ResolveYAMLPath(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 `yaml:"path"`
+ Rule string `yaml:"rule"`
+}
+
+// ReadPolicy unmarshals the given YAML bytes into a new Policy struct.
+//
+func ReadPolicy(data []byte) (*Policy, error) {
+ var policy Policy
+
+ err := yaml.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 ReadPolicy(data)
+}
+
+// ResolveYAMLPath returns the config value found at the given YAML-ish
+// namespace/path (e.g. "variants.production.runs.as").
+//
+func ResolveYAMLPath(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 := ResolveYAMLPath(path, v.Field(i).Interface()); err == nil {
+ return subsubcfg, nil
+ }
+ }
+
+ if name == resolveYAMLTagName(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 ResolveYAMLPath(parts[1], subcfg)
+ }
+
+ return subcfg, nil
+}
diff --git a/config/policy_test.go b/config/policy_test.go
new file mode 100644
index 0000000..2d7a3a7
--- /dev/null
+++ b/config/policy_test.go
@@ -0,0 +1,117 @@
+package config_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "phabricator.wikimedia.org/source/blubber/config"
+)
+
+func TestPolicyRead(t *testing.T) {
+ policy, err := config.ReadPolicy([]byte(`---
+ enforcements:
+ - path: variants.production.runs.as
+ rule: ne=root
+ - path: base
+ rule: oneof=debian:jessie debian:stretch`))
+
+ if assert.NoError(t, err) {
+ if assert.Len(t, policy.Enforcements, 2) {
+ assert.Equal(t, "variants.production.runs.as", policy.Enforcements[0].Path)
+ assert.Equal(t, "ne=root", policy.Enforcements[0].Rule)
+
+ assert.Equal(t, "base", policy.Enforcements[1].Path)
+ assert.Equal(t, "oneof=debian:jessie debian:stretch", policy.Enforcements[1].Rule)
+ }
+ }
+}
+
+func TestPolicyValidate(t *testing.T) {
+ cfg := config.Config{
+ CommonConfig: config.CommonConfig{
+ Base: "foo:tag",
+ },
+ Variants: map[string]config.VariantConfig{
+ "foo": config.VariantConfig{
+ CommonConfig: config.CommonConfig{
+ Runs: config.RunsConfig{
+ UserConfig: config.UserConfig{
+ As: "root",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ policy := config.Policy{
+ Enforcements: []config.Enforcement{
+ {Path: "variants.foo.runs.as", Rule: "ne=root"},
+ },
+ }
+
+ assert.EqualError(t,
+ policy.Validate(cfg),
+ `value for "variants.foo.runs.as" violates policy rule "ne=root"`,
+ )
+
+ policy = config.Policy{
+ Enforcements: []config.Enforcement{
+ {Path: "base", Rule: "oneof=debian:jessie debian:stretch"},
+ },
+ }
+
+ assert.EqualError(t,
+ policy.Validate(cfg),
+ `value for "base" violates policy rule "oneof=debian:jessie debian:stretch"`,
+ )
+}
+
+func TestEnforcementOnFlag(t *testing.T) {
+ cfg := config.Config{
+ Variants: map[string]config.VariantConfig{
+ "production": config.VariantConfig{
+ CommonConfig: config.CommonConfig{
+ Node: config.NodeConfig{
+ Dependencies: config.Flag{True: true},
+ },
+ },
+ },
+ },
+ }
+
+ policy := config.Policy{
+ Enforcements: []config.Enforcement{
+ {Path: "variants.production.node.dependencies", Rule: "isfalse"},
+ },
+ }
+
+ assert.Error(t,
+ policy.Validate(cfg),
+ `value for "variants.production.node.dependencies" violates policy rule "isfalse"`,
+ )
+
+}
+
+func TestResolveYAMLPath(t *testing.T) {
+ cfg := config.Config{
+ Variants: map[string]config.VariantConfig{
+ "foo": config.VariantConfig{
+ CommonConfig: config.CommonConfig{
+ Runs: config.RunsConfig{
+ UserConfig: config.UserConfig{
+ As: "root",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ val, err := config.ResolveYAMLPath("variants.foo.runs.as", cfg)
+
+ if assert.NoError(t, err) {
+ assert.Equal(t, "root", val)
+ }
+}
diff --git a/config/validation.go b/config/validation.go
index 652a3c8..3359211 100644
--- a/config/validation.go
+++ b/config/validation.go
@@ -52,6 +52,8 @@ var (
"baseimage": isBaseImage,
"debianpackage": isDebianPackage,
"envvars": isEnvironmentVariables,
+ "isfalse": isFalse,
+ "istrue": isTrue,
"variantref": isVariantReference,
"variants": hasVariantNames,
}
@@ -61,11 +63,10 @@ type ctxKey uint8
const rootCfgCtx ctxKey = iota
-// Validate runs all validations defined for config fields against the given
-// Config value. If the returned error is not nil, it will contain a
-// user-friendly message describing all invalid field values.
+// NewValidator returns a validator instance for which our custom aliases and
+// functions are registered.
//
-func Validate(config Config) error {
+func NewValidator() *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(resolveYAMLTagName)
@@ -78,6 +79,16 @@ func Validate(config Config) error {
validate.RegisterValidationCtx(name, f)
}
+ return validate
+}
+
+// Validate runs all validations defined for config fields against the given
+// Config value. If the returned error is not nil, it will contain a
+// user-friendly message describing all invalid field values.
+//
+func Validate(config Config) error {
+ validate := NewValidator()
+
ctx := context.WithValue(context.Background(), rootCfgCtx, config)
return validate.StructCtx(ctx, config)
@@ -169,6 +180,18 @@ func isEnvironmentVariables(_ context.Context, fl validator.FieldLevel) bool {
return true
}
+func isFalse(_ context.Context, fl validator.FieldLevel) bool {
+ val, ok := fl.Field().Interface().(bool)
+
+ return ok && val == false
+}
+
+func isTrue(_ context.Context, fl validator.FieldLevel) bool {
+ val, ok := fl.Field().Interface().(bool)
+
+ return ok && val == true
+}
+
func isVariantReference(ctx context.Context, fl validator.FieldLevel) bool {
cfg := ctx.Value(rootCfgCtx).(Config)
ref := fl.Field().String()