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]
|
|
}
|