mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-26 16:09:45 -05:00
744a605602
* re-use `ReadFromFile` in `CreateOrRead` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip]: add constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] error formatting Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * wip Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * formatted error messages Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * state file validation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overriding the constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * dont validate on read Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add pre-create constraints Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * finish pre-init validation test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * finish post-init validation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use state file validation in CLI Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix apply tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Update internal/validation/errors.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * use transformator for tests * tidy * use empty check directly Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update cli/internal/state/state.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * conditional validation per CSP Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix rebase Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add default case Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * validate state-file as last input Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
318 lines
8.9 KiB
Go
318 lines
8.9 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package validation
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// TreeError is returned when a document is not valid.
|
|
// It contains the path to the field that failed validation, the error
|
|
// that occurred, as well as a list of child errors, as one constraint
|
|
// can embed multiple other constraints, e.g. in an OR.
|
|
type TreeError struct {
|
|
path string
|
|
err error
|
|
children []*TreeError
|
|
}
|
|
|
|
// NewErrorTree creates a new error tree from the given error.
|
|
func NewErrorTree(err error) *TreeError {
|
|
return &TreeError{
|
|
err: err,
|
|
children: []*TreeError{},
|
|
}
|
|
}
|
|
|
|
/*
|
|
newTraceError creates a new validation error, traced to a field.
|
|
|
|
To find the path to the exported field that failed validation, it traverses "doc"
|
|
recursively until it finds a field in "doc" that matches the reference to "field".
|
|
*/
|
|
func newTraceError(doc, field referenceableValue, errMsg error) *TreeError {
|
|
// traverse the top level struct (i.e. the "haystack") until addr (i.e. the "needle") is found
|
|
path, err := traverse(doc, field, newPathBuilder(doc._type.Name()))
|
|
if err != nil {
|
|
return &TreeError{
|
|
path: "unknown",
|
|
err: fmt.Errorf("cannot find path to field: %w. original error: %w", err, errMsg),
|
|
}
|
|
}
|
|
|
|
return &TreeError{
|
|
path: path,
|
|
err: errMsg,
|
|
children: []*TreeError{},
|
|
}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *TreeError) Error() string {
|
|
return e.format(0)
|
|
}
|
|
|
|
// Unwrap implements the error interface.
|
|
func (e *TreeError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// format formats the error tree and all of its children.
|
|
func (e *TreeError) format(indent int) string {
|
|
var sb strings.Builder
|
|
if e.path != "" {
|
|
sb.WriteString(fmt.Sprintf(
|
|
"%svalidating %s: %s",
|
|
strings.Repeat(" ", indent),
|
|
e.path,
|
|
e.err,
|
|
))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(
|
|
"%s%s",
|
|
strings.Repeat(" ", indent),
|
|
e.err,
|
|
))
|
|
}
|
|
for _, child := range e.children {
|
|
sb.WriteString(fmt.Sprintf(
|
|
"\n%s",
|
|
child.format(indent+1),
|
|
))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// appendChild adds the given child error to the tree.
|
|
func (e *TreeError) appendChild(child *TreeError) {
|
|
e.children = append(e.children, child)
|
|
}
|
|
|
|
/*
|
|
traverse "haystack" recursively until it finds a field that matches
|
|
the reference saved in "needle", while building a pseudo-JSONPath to the field.
|
|
|
|
If it traverses a level down, it appends the name of the struct tag
|
|
or another entity like array index or map field to path.
|
|
|
|
When a field matches the reference to the given field, it returns the
|
|
path to the field.
|
|
*/
|
|
func traverse(haystack referenceableValue, needle referenceableValue, path pathBuilder) (string, error) {
|
|
// recursion anchor: doc is the field we are looking for.
|
|
// Join the path and return.
|
|
if foundNeedle(haystack, needle) {
|
|
return path.string(), nil
|
|
}
|
|
|
|
kind := haystack._type.Kind()
|
|
switch kind {
|
|
case reflect.Struct:
|
|
// Traverse all visible struct fields.
|
|
for _, field := range reflect.VisibleFields(haystack._type) {
|
|
// skip unexported fields
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
fieldVal := recPointerDeref(haystack.value.FieldByName(field.Name))
|
|
if isNilPtrOrInvalid(fieldVal) {
|
|
continue
|
|
}
|
|
|
|
fieldAddr := haystack.addr + field.Offset
|
|
newHaystack := referenceableValue{
|
|
value: fieldVal,
|
|
addr: fieldVal.UnsafeAddr(),
|
|
_type: fieldVal.Type(),
|
|
}
|
|
if canTraverse(fieldVal) {
|
|
// When a field is not the needle and cannot be traversed further,
|
|
// a errCannotTraverse is returned. Therefore, we only want to handle
|
|
// the case where the field is the needle.
|
|
if path, err := traverse(newHaystack, needle, path.appendStructField(field)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
if foundNeedle(referenceableValue{addr: fieldAddr, _type: field.Type}, needle) {
|
|
return path.appendStructField(field).string(), nil
|
|
}
|
|
}
|
|
case reflect.Slice, reflect.Array:
|
|
// Traverse slice / Array elements
|
|
for i := 0; i < haystack.value.Len(); i++ {
|
|
// see struct case
|
|
itemVal := recPointerDeref(haystack.value.Index(i))
|
|
if isNilPtrOrInvalid(itemVal) {
|
|
continue
|
|
}
|
|
newHaystack := referenceableValue{
|
|
value: itemVal,
|
|
addr: itemVal.UnsafeAddr(),
|
|
_type: itemVal.Type(),
|
|
}
|
|
if canTraverse(itemVal) {
|
|
if path, err := traverse(newHaystack, needle, path.appendArrayIndex(i)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
if foundNeedle(newHaystack, needle) {
|
|
return path.appendArrayIndex(i).string(), nil
|
|
}
|
|
}
|
|
case reflect.Map:
|
|
// Traverse map elements
|
|
iter := haystack.value.MapRange()
|
|
for iter.Next() {
|
|
// see struct case
|
|
mapKey := iter.Key().String()
|
|
mapVal := recPointerDeref(iter.Value())
|
|
if isNilPtrOrInvalid(mapVal) {
|
|
continue
|
|
}
|
|
if canTraverse(mapVal) {
|
|
newHaystack := referenceableValue{
|
|
value: mapVal,
|
|
addr: mapVal.UnsafeAddr(),
|
|
_type: mapVal.Type(),
|
|
mapKey: mapKey,
|
|
}
|
|
if path, err := traverse(newHaystack, needle, path.appendMapKey(mapKey)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
// check if reference to map is the needle and the map key matches
|
|
if foundNeedle(referenceableValue{addr: haystack.addr, _type: haystack._type, mapKey: mapKey}, needle) {
|
|
return path.appendMapKey(mapKey).string(), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Primitive type, but not the value we are looking for.
|
|
return "", errCannotTraverse
|
|
}
|
|
|
|
// referenceableValue is a type that can be passed as any (thus being copied) without losing the reference to the actual value.
|
|
type referenceableValue struct {
|
|
value reflect.Value
|
|
_type reflect.Type
|
|
mapKey string // special case for map values, which are not addressable
|
|
addr uintptr
|
|
}
|
|
|
|
// errCannotTraverse is returned when a field cannot be traversed further.
|
|
var errCannotTraverse = errors.New("cannot traverse anymore")
|
|
|
|
// recPointerDeref recursively dereferences pointers and unpacks interfaces until a non-pointer value is found.
|
|
func recPointerDeref(val reflect.Value) reflect.Value {
|
|
switch val.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface:
|
|
return recPointerDeref(val.Elem())
|
|
}
|
|
return val
|
|
}
|
|
|
|
// pointerDeref dereferences pointers and unpacks interfaces.
|
|
// If the value is not a pointer, it is returned unchanged.
|
|
func pointerDeref(val reflect.Value) reflect.Value {
|
|
switch val.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface:
|
|
return val.Elem()
|
|
}
|
|
return val
|
|
}
|
|
|
|
/*
|
|
canTraverse whether a value can be further traversed.
|
|
|
|
For pointer types, false is returned.
|
|
*/
|
|
func canTraverse(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isNilPtrOrInvalid returns true if a value is a nil pointer or if the value is of an invalid kind.
|
|
func isNilPtrOrInvalid(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice, reflect.Map:
|
|
return v.IsNil()
|
|
case reflect.Invalid:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/*
|
|
foundNeedle returns whether the given value is the needle.
|
|
|
|
It does so by comparing the address and type of the value to the address and type of the needle.
|
|
The comparison of types is necessary because the first value of a struct has the same address as the struct itself.
|
|
*/
|
|
func foundNeedle(haystack, needle referenceableValue) bool {
|
|
return haystack.addr == needle.addr &&
|
|
haystack._type == needle._type &&
|
|
haystack.mapKey == needle.mapKey
|
|
}
|
|
|
|
// pathBuilder is a helper to build a field path.
|
|
type pathBuilder struct {
|
|
buf []string // slice can be copied by value when its non-zero, contrary to a strings.Builder
|
|
}
|
|
|
|
// newPathBuilder creates a new pathBuilder from the identifier of a top level document.
|
|
func newPathBuilder(topLevelDoc string) pathBuilder {
|
|
return pathBuilder{
|
|
buf: []string{topLevelDoc},
|
|
}
|
|
}
|
|
|
|
// appendStructField appends the JSON / YAML struct tag of a field to the path.
|
|
// If no struct tag is present, the field name is used.
|
|
func (p pathBuilder) appendStructField(field reflect.StructField) pathBuilder {
|
|
switch {
|
|
case field.Tag.Get("json") != "":
|
|
// cut off omitempty or other options
|
|
jsonTagName, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", jsonTagName))
|
|
case field.Tag.Get("yaml") != "":
|
|
// cut off omitempty or other options
|
|
yamlTagName, _, _ := strings.Cut(field.Tag.Get("yaml"), ",")
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", yamlTagName))
|
|
default:
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", field.Name))
|
|
}
|
|
return p
|
|
}
|
|
|
|
// appendArrayIndex appends the index of an array to the path.
|
|
func (p pathBuilder) appendArrayIndex(i int) pathBuilder {
|
|
p.buf = append(p.buf, fmt.Sprintf("[%d]", i))
|
|
return p
|
|
}
|
|
|
|
// appendMapKey appends the key of a map to the path.
|
|
func (p pathBuilder) appendMapKey(k string) pathBuilder {
|
|
p.buf = append(p.buf, fmt.Sprintf("[\"%s\"]", k))
|
|
return p
|
|
}
|
|
|
|
// string returns the path.
|
|
func (p pathBuilder) string() string {
|
|
// Remove struct tag prefix
|
|
return strings.TrimPrefix(
|
|
strings.Join(p.buf, ""),
|
|
".",
|
|
)
|
|
}
|