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.
 
 
 

245 lines
6.8 KiB

package config
import (
"bytes"
"context"
"fmt"
"path"
"reflect"
"regexp"
"strings"
"text/template"
"github.com/docker/distribution/reference"
"gopkg.in/go-playground/validator.v9"
)
var (
// See Debian Policy
// https://www.debian.org/doc/debian-policy/#s-f-source
// https://www.debian.org/doc/debian-policy/#s-f-version
debianPackageName = `[a-z0-9][a-z0-9+.-]+`
debianVersionSpec = `(?:[0-9]+:)?[0-9]+[a-zA-Z0-9\.\+\-~]*`
debianReleaseName = `[a-zA-Z](?:[a-zA-Z0-9\-]*[a-zA-Z0-9]+)?`
debianPackageRegexp = regexp.MustCompile(fmt.Sprintf(
`^%s(?:=%s|/%s)?$`, debianPackageName, debianVersionSpec, debianReleaseName))
// See IEEE Std 1003.1-2008 (http://pubs.opengroup.org/onlinepubs/9699919799/)
environmentVariableRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]+$`)
// Pattern for valid variant names
variantNameRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$`)
humanizedErrors = map[string]string{
"abspath": `{{.Field}}: "{{.Value}}" is not a valid absolute non-root path`,
"baseimage": `{{.Field}}: "{{.Value}}" is not a valid base image reference`,
"currentversion": `{{.Field}}: config version "{{.Value}}" is unsupported`,
"debianpackage": `{{.Field}}: "{{.Value}}" is not a valid Debian package name`,
"envvars": `{{.Field}}: contains invalid environment variable names`,
"nodeenv": `{{.Field}}: "{{.Value}}" is not a valid Node environment name`,
"relativelocal": `{{.Field}}: path must be relative when "from" is "local"`,
"required": `{{.Field}}: is required`,
"requiredwith": `{{.Field}}: is required if "{{.Param}}" is also set`,
"unique": `{{.Field}}: cannot contain duplicates`,
"username": `{{.Field}}: "{{.Value}}" is not a valid user name`,
"variantref": `{{.Field}}: references an unknown variant "{{.Value}}"`,
"variants": `{{.Field}}: contains a bad variant name`,
}
validatorAliases = map[string]string{
"currentversion": "eq=" + CurrentVersion,
"nodeenv": "alphanum",
"username": "hostname,ne=root",
}
validatorFuncs = map[string]validator.FuncCtx{
"abspath": isAbsNonRootPath,
"baseimage": isBaseImage,
"debianpackage": isDebianPackage,
"envvars": isEnvironmentVariables,
"isfalse": isFalse,
"istrue": isTrue,
"relativelocal": isRelativePathForLocalArtifact,
"requiredwith": isSetIfOtherFieldIsSet,
"variantref": isVariantReference,
"variants": hasVariantNames,
}
)
type ctxKey uint8
const rootCfgCtx ctxKey = iota
// newValidator returns a validator instance for which our custom aliases and
// functions are registered.
//
func newValidator() *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(resolveJSONTagName)
for name, tags := range validatorAliases {
validate.RegisterAlias(name, tags)
}
for name, f := range validatorFuncs {
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 interface{}) error {
validate := newValidator()
ctx := context.WithValue(context.Background(), rootCfgCtx, config)
return validate.StructCtx(ctx, config)
}
// HumanizeValidationError transforms the given validator.ValidationErrors
// into messages more likely to be understood by human beings.
//
func HumanizeValidationError(err error) string {
var message bytes.Buffer
if err == nil {
return ""
} else if !IsValidationError(err) {
return err.Error()
}
templates := map[string]*template.Template{}
for name, tmplString := range humanizedErrors {
if tmpl, err := template.New(name).Parse(tmplString); err == nil {
templates[name] = tmpl
}
}
for _, ferr := range err.(validator.ValidationErrors) {
if tmpl, ok := templates[ferr.Tag()]; ok {
tmpl.Execute(&message, ferr)
} else if trueErr, ok := err.(error); ok {
message.WriteString(trueErr.Error())
}
message.WriteString("\n")
}
return strings.TrimSpace(message.String())
}
// IsValidationError tests whether the given error is a
// validator.ValidationErrors and can be safely iterated over as such.
//
func IsValidationError(err error) bool {
if err == nil {
return false
} else if _, ok := err.(*validator.InvalidValidationError); ok {
return false
} else if _, ok := err.(validator.ValidationErrors); ok {
return true
}
return false
}
func hasVariantNames(_ context.Context, fl validator.FieldLevel) bool {
for _, name := range fl.Field().MapKeys() {
if !variantNameRegexp.MatchString(name.String()) {
return false
}
}
return true
}
func isAbsNonRootPath(_ context.Context, fl validator.FieldLevel) bool {
value := fl.Field().String()
return path.IsAbs(value) && path.Base(path.Clean(value)) != "/"
}
func isBaseImage(_ context.Context, fl validator.FieldLevel) bool {
value := fl.Field().String()
return reference.ReferenceRegexp.MatchString(value)
}
func isDebianPackage(_ context.Context, fl validator.FieldLevel) bool {
value := fl.Field().String()
return debianPackageRegexp.MatchString(value)
}
func isEnvironmentVariables(_ context.Context, fl validator.FieldLevel) bool {
for _, key := range fl.Field().MapKeys() {
if !environmentVariableRegexp.MatchString(key.String()) {
return false
}
}
return true
}
func isFalse(_ context.Context, fl validator.FieldLevel) bool {
val, ok := fl.Field().Interface().(bool)
return ok && val == false
}
func isRelativePathForLocalArtifact(_ context.Context, fl validator.FieldLevel) bool {
value := fl.Field().String()
from := fl.Parent().FieldByName("From").String()
if value == "" || from != LocalArtifactKeyword {
return true
}
// path must be relative and do no "../" funny business
return !(path.IsAbs(value) || strings.HasPrefix(path.Clean(value), ".."))
}
func isSetIfOtherFieldIsSet(_ context.Context, fl validator.FieldLevel) bool {
if otherField, err := ResolveJSONPath(fl.Param(), fl.Parent().Interface()); err == nil {
return isZeroValue(reflect.ValueOf(otherField)) || !isZeroValue(fl.Field())
}
return 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()
if ref == LocalArtifactKeyword {
return true
}
for name := range cfg.Variants {
if name == ref {
return true
}
}
return false
}
func isZeroValue(v reflect.Value) bool {
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
func resolveJSONTagName(field reflect.StructField) string {
return strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
}