1
0
mirror of https://github.com/coreos/ignition.git synced 2026-02-06 09:47:17 +01:00
Files
ignition/config/config_test.go
2024-10-14 15:33:52 +02:00

290 lines
10 KiB
Go

// Copyright 2019 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"reflect"
"testing"
"github.com/coreos/ignition/v2/config/util"
v3_0 "github.com/coreos/ignition/v2/config/v3_0/types"
v3_1 "github.com/coreos/ignition/v2/config/v3_1/types"
v3_2 "github.com/coreos/ignition/v2/config/v3_2/types"
v3_3 "github.com/coreos/ignition/v2/config/v3_3/types"
v3_4 "github.com/coreos/ignition/v2/config/v3_4/types"
v3_5 "github.com/coreos/ignition/v2/config/v3_5/types"
v3_6 "github.com/coreos/ignition/v2/config/v3_6_experimental/types"
)
type typeSet map[reflect.Type]struct{}
// helper to check whether a type and field matches a denylist of known problems
// examples are either structs or names of structs
func ignore(t reflect.Type, field reflect.StructField, fieldName string, examples ...interface{}) bool {
if field.Name != fieldName {
return false
}
for _, candidate := range examples {
if reflect.TypeOf(candidate).Kind() == reflect.String {
if t.Name() == candidate.(string) {
return true
}
} else if t == reflect.TypeOf(candidate) {
return true
}
}
return false
}
// vary the specified field value and check the given key function to see
// whether the field seems to affect it
// this function's heuristic can be fooled by complex key functions but it
// should be fine for typical cases
func fieldAffectsKey(key func() string, v reflect.Value) bool {
kind := v.Kind()
switch {
case util.IsPrimitive(kind):
old := key()
v.Set(util.NonZeroValue(v.Type()))
new := key()
v.Set(reflect.Zero(v.Type()))
return old != new
case kind == reflect.Ptr:
null := key()
v.Set(reflect.New(v.Type().Elem()))
allocated := key()
affectsKey := fieldAffectsKey(key, v.Elem())
v.Set(reflect.Zero(v.Type()))
return null != allocated || affectsKey
case kind == reflect.Struct:
ret := false
for i := 0; i < v.NumField(); i++ {
ret = ret || fieldAffectsKey(key, v.Field(i))
}
return ret
case kind == reflect.Slice:
if v.Len() > 0 {
panic("Slice started with non-zero length")
}
v.Set(reflect.MakeSlice(v.Type(), 1, 1))
ret := fieldAffectsKey(key, v.Index(0))
v.SetLen(0)
return ret
default:
panic(fmt.Sprintf("Unexpected value kind %v", kind.String()))
}
}
// check the fields that affect the key function of a keyed struct
// to ensure that we're using pointer and non-pointer fields properly.
// add the type of the struct and any anonymous embedded structs to
// keyedStructs.
func checkStructFieldKey(t reflect.Type, keyedStructs typeSet) error {
v := reflect.New(t).Elem()
// wrapper to get the current key of @v
getKey := func() string {
// outer function's caller should have ensured that type
// implements Keyed
return v.Interface().(util.Keyed).Key()
}
var haveNonPointerKey bool
// check the fields of one struct
var checkStruct func(t reflect.Type, v reflect.Value) error
checkStruct = func(t reflect.Type, v reflect.Value) error {
keyedStructs[t] = struct{}{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
affectsKey := fieldAffectsKey(getKey, v.Field(i))
switch {
case util.IsPrimitive(field.Type.Kind()):
// non-pointer primitive; must affect key
haveNonPointerKey = true
if !affectsKey &&
!ignore(t, field, "Target", v3_0.LinkEmbedded1{}, v3_1.LinkEmbedded1{}, v3_2.LinkEmbedded1{}) &&
!ignore(t, field, "Level", v3_0.Raid{}, v3_1.Raid{}, v3_2.Raid{}) {
return fmt.Errorf("Non-pointer %s.%s does not affect key", t.Name(), field.Name)
}
case field.Type.Kind() == reflect.Ptr && util.IsPrimitive(field.Type.Elem().Kind()):
// pointer primitive; may affect key if there's also
// a non-pointer key
case field.Type.Kind() == reflect.Struct && field.Anonymous:
// anonymous child struct; treat it as an extension of the
// parent
if err := checkStruct(field.Type, v.Field(i)); err != nil {
return err
}
default:
// slice, struct, or invalid type
if affectsKey {
return fmt.Errorf("Non-primitive %s.%s affects key", t.Name(), field.Name)
}
}
}
return nil
}
if err := checkStruct(t, v); err != nil {
return err
}
// The Resource struct in spec >= 3.1 uses Source as the key, but
// it's a pointer because in storage.files the source is optional.
// Allow this special case, and the similar ConfigReference one in
// 3.0. This rule is a consistency guideline anyway; there's no
// technical reason we can't have pointer keys.
if !haveNonPointerKey &&
t.Name() != "Resource" &&
t != reflect.TypeOf(v3_0.ConfigReference{}) {
return fmt.Errorf("No non-pointer key for %s", t.Name())
}
return nil
}
// keyedStructs is a running set of visited struct types that are either
// keyed and in a list, or anonymously embedded in such a type
func testConfigType(t reflect.Type, keyedStructs typeSet) error {
k := t.Kind()
switch {
case util.IsInvalidInConfig(k):
return fmt.Errorf("Type %s is of kind %s which is not valid in configs", t.Name(), k.String())
case util.IsPrimitive(k):
return nil
case k == reflect.Ptr:
pK := t.Elem().Kind()
if util.IsPrimitive(pK) {
return nil
}
switch t.Elem() {
case reflect.TypeOf(v3_2.Clevis{}), reflect.TypeOf(v3_2.Custom{}):
// these structs ended up with pointers; can't be helped now
if err := testConfigType(t.Elem(), keyedStructs); err != nil {
return fmt.Errorf("Type %s has invalid children: %v", t.Elem().Name(), err)
}
return nil
default:
return fmt.Errorf("Type %s is a pointer that points to a non-primitive type", t.Name())
}
case k == reflect.Slice:
eK := t.Elem().Kind()
switch {
case util.IsPrimitive(eK):
return nil
case eK == reflect.Struct:
if err := testConfigType(t.Elem(), keyedStructs); err != nil {
return fmt.Errorf("Type %s has invalid children: %v", t.Name(), err)
}
return nil
case eK == reflect.Slice:
return fmt.Errorf("Type %s is a slice of slices", t.Name())
case util.IsInvalidInConfig(eK):
return fmt.Errorf("Type %s is a slice of invalid types", t.Name())
default:
return fmt.Errorf("Testing code encountered a failure at %s", t.Name())
}
case k == reflect.Struct:
ignoredFields := map[string]struct{}{}
if ignorer, ok := reflect.New(t).Interface().(util.IgnoresDups); ok {
ignoredFields = ignorer.IgnoreDuplicates()
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if err := testConfigType(field.Type, keyedStructs); err != nil {
return fmt.Errorf("Type %s has invalid field %s: %v", t.Name(), field.Name, err)
}
if field.Type.Kind() == reflect.Slice && field.Type.Elem().Kind() != reflect.String {
elemType := field.Type.Elem()
if _, ignored := ignoredFields[field.Name]; !ignored {
// check this here, rather than in checkStructFieldKey(),
// so we can provide more context in the error
keyed, ok := reflect.New(elemType).Interface().(util.Keyed)
if !ok {
return fmt.Errorf("Type %s has slice field %s without Key() defined on %s debug: %v", t.Name(), field.Name, field.Type.Elem().Name(), ignoredFields)
}
// explicitly check for nil pointer dereference when calling Key() on zero value
keyed.Key()
if err := checkStructFieldKey(elemType, keyedStructs); err != nil {
return fmt.Errorf("Type %s has invalid field %s: %v", t.Name(), field.Name, err)
}
}
}
}
return nil
default:
return fmt.Errorf("Testing code encountered a failure at %s", t.Name())
}
}
// Walk a struct hierarchy, checking every struct type not in ignoreTypes
// for non-pointer fields. Return an error if any are found that aren't on
// an allowlist of known problems. ignoreTypes is a set of struct types
// that have already been checked against the rules for keyed structs, and
// shouldn't be checked against our stricter rules.
func checkNonKeyedStructFields(t reflect.Type, ignoreTypes typeSet) error {
kind := t.Kind()
switch {
case util.IsPrimitive(kind):
return nil
case kind == reflect.Ptr:
return checkNonKeyedStructFields(t.Elem(), ignoreTypes)
case kind == reflect.Slice:
return checkNonKeyedStructFields(t.Elem(), ignoreTypes)
case kind == reflect.Struct:
_, ignoreType := ignoreTypes[t]
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// ignition.version is allowed to be non-pointer in
// every spec version
if !ignoreType &&
util.IsPrimitive(f.Type.Kind()) &&
!ignore(t, f, "Version", "Ignition") &&
!ignore(t, f, "Config", v3_2.Custom{}) &&
!ignore(t, f, "Pin", v3_2.Custom{}) {
return fmt.Errorf("Type %s has non-pointer primitive field %s", t.Name(), f.Name)
}
if err := checkNonKeyedStructFields(f.Type, ignoreTypes); err != nil {
return fmt.Errorf("Type %s has invalid field %s: %v", t.Name(), f.Name, err)
}
}
return nil
default:
panic(fmt.Sprintf("unexpected kind %s", kind))
}
}
// TestConfigStructure walks the types of all our configs and ensures they don't contain
// anything the merge, translation, or validation logic doesn't know how to handle
func TestConfigStructure(t *testing.T) {
configs := []reflect.Type{
reflect.TypeOf(v3_0.Config{}),
reflect.TypeOf(v3_1.Config{}),
reflect.TypeOf(v3_2.Config{}),
reflect.TypeOf(v3_3.Config{}),
reflect.TypeOf(v3_4.Config{}),
reflect.TypeOf(v3_5.Config{}),
reflect.TypeOf(v3_6.Config{}),
}
for _, configType := range configs {
keyedStructs := make(typeSet)
if err := testConfigType(configType, keyedStructs); err != nil {
t.Errorf("Type %s/%s was invalid: %v", configType.PkgPath(), configType.Name(), err)
} else if err := checkNonKeyedStructFields(configType, keyedStructs); err != nil {
t.Errorf("Type %s/%s was invalid: %v", configType.PkgPath(), configType.Name(), err)
}
}
}