Update smd repo to handle CRDs

This commit is contained in:
Antoine Pelisse 2019-01-29 14:24:00 -08:00
parent 7e4cc38401
commit 1751fc013f
14 changed files with 606 additions and 141 deletions

View File

@ -35,6 +35,12 @@ func (fp Path) String() string {
return strings.Join(strs, "")
}
func (fp Path) Copy() Path {
new := make(Path, len(fp))
copy(new, fp)
return new
}
// MakePath constructs a Path. The parts may be PathElements, ints, strings.
func MakePath(parts ...interface{}) (Path, error) {
var fp Path

View File

@ -154,6 +154,16 @@ func (s *Set) iteratePrefix(prefix Path, f func(Path)) {
s.Children.iteratePrefix(prefix, f)
}
// WithPrefix returns the subset of paths which begin with the given prefix,
// with the prefix not included.
func (s *Set) WithPrefix(pe PathElement) *Set {
subset, ok := s.Children.Get(pe)
if !ok {
return NewSet()
}
return subset
}
// setNode is a pair of PathElement / Set, for the purpose of expressing
// nested set membership.
type setNode struct {

View File

@ -85,6 +85,15 @@ func (s *Updater) update(oldObject, newObject typed.TypedValue, version fieldpat
for manager, conflictSet := range conflicts {
managers[manager].Set = managers[manager].Set.Difference(conflictSet.Set)
if managers[manager].Set.Empty() {
delete(managers, manager)
}
}
if _, ok := managers[workflow]; !ok {
managers[workflow] = &fieldpath.VersionedSet{
Set: fieldpath.NewSet(),
}
}
return managers, nil
@ -105,13 +114,11 @@ func (s *Updater) Update(liveObject, newObject typed.TypedValue, version fieldpa
if err != nil {
return fieldpath.ManagedFields{}, fmt.Errorf("failed to compare live and new objects: %v", err)
}
if _, ok := managers[manager]; !ok {
managers[manager] = &fieldpath.VersionedSet{
Set: fieldpath.NewSet(),
}
}
managers[manager].Set = managers[manager].Set.Union(compare.Modified).Union(compare.Added).Difference(compare.Removed)
managers[manager].APIVersion = version
if managers[manager].Set.Empty() {
delete(managers, manager)
}
return managers, nil
}
@ -121,22 +128,41 @@ func (s *Updater) Update(liveObject, newObject typed.TypedValue, version fieldpa
func (s *Updater) Apply(liveObject, configObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (typed.TypedValue, fieldpath.ManagedFields, error) {
newObject, err := liveObject.Merge(configObject)
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
}
managers, err = s.update(liveObject, newObject, version, managers, manager, force)
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, err
return nil, fieldpath.ManagedFields{}, err
}
newObject, err = s.removeDisownedItems(newObject, configObject, managers[manager])
if err != nil {
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to remove fields: %v", err)
}
// TODO: Remove unconflicting removed fields
set, err := configObject.ToFieldSet()
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
}
managers[manager] = &fieldpath.VersionedSet{
Set: set,
APIVersion: version,
}
if managers[manager].Set.Empty() {
delete(managers, manager)
}
return newObject, managers, nil
}
func (s *Updater) removeDisownedItems(merged, applied typed.TypedValue, lastSet *fieldpath.VersionedSet) (typed.TypedValue, error) {
if lastSet.Set.Empty() {
return merged, nil
}
convertedApplied, err := s.Converter.Convert(applied, lastSet.APIVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert applied config to last applied version: %v", err)
}
appliedSet, err := convertedApplied.ToFieldSet()
if err != nil {
return nil, fmt.Errorf("failed to create field set from applied config in last applied version: %v", err)
}
return merged.RemoveItems(lastSet.Set.Difference(appliedSet)), nil
}

View File

@ -5,11 +5,13 @@ go_library(
srcs = [
"doc.go",
"elements.go",
"fromvalue.go",
"schemaschema.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/schema",
importpath = "sigs.k8s.io/structured-merge-diff/schema",
visibility = ["//visibility:public"],
deps = ["//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library"],
)
filegroup(

View File

@ -207,7 +207,7 @@ func (s Schema) FindNamedType(name string) (TypeDef, bool) {
//
// This allows callers to not care about the difference between a (possibly
// inlined) reference and a definition.
func (s Schema) Resolve(tr TypeRef) (Atom, bool) {
func (s *Schema) Resolve(tr TypeRef) (Atom, bool) {
if tr.NamedType != nil {
t, ok := s.FindNamedType(*tr.NamedType)
if !ok {

View File

@ -0,0 +1,57 @@
/*
Copyright 2019 The Kubernetes Authors.
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 schema
import (
"sigs.k8s.io/structured-merge-diff/value"
)
// TypeRefFromValue creates an inlined type from a value v
func TypeRefFromValue(v value.Value) TypeRef {
atom := atomFor(v)
return TypeRef{
Inlined: atom,
}
}
func atomFor(v value.Value) Atom {
switch {
// Untyped cases (handled at the bottom of this function)
case v.Null:
case v.ListValue != nil:
case v.FloatValue != nil:
case v.IntValue != nil:
case v.StringValue != nil:
case v.BooleanValue != nil:
// Recursive case
case v.MapValue != nil:
s := Struct{}
for i := range v.MapValue.Items {
child := v.MapValue.Items[i]
field := StructField{
Name: child.Name,
Type: TypeRef{
Inlined: atomFor(child.Value),
},
}
s.Fields = append(s.Fields, field)
}
return Atom{Struct: &s}
}
return Atom{Untyped: &Untyped{}}
}

View File

@ -3,10 +3,12 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"deduced.go",
"doc.go",
"helpers.go",
"merge.go",
"parser.go",
"remove.go",
"typed.go",
"validate.go",
],

View File

@ -0,0 +1,178 @@
/*
Copyright 2018 The Kubernetes Authors.
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 typed
import (
"reflect"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/value"
)
// deducedTypedValue holds a value and guesses what it is and what to
// do with it.
type deducedTypedValue struct {
value value.Value
}
// AsTypedDeduced is going to generate it's own type definition based on
// the content of the object. This is useful for CRDs that don't have a
// validation field.
func AsTypedDeduced(v value.Value) TypedValue {
return deducedTypedValue{value: v}
}
func (dv deducedTypedValue) AsValue() *value.Value {
return &dv.value
}
func (deducedTypedValue) Validate() error {
return nil
}
func (dv deducedTypedValue) ToFieldSet() (*fieldpath.Set, error) {
set := fieldpath.NewSet()
fieldsetDeduced(dv.value, fieldpath.Path{}, set)
return set, nil
}
func fieldsetDeduced(v value.Value, path fieldpath.Path, set *fieldpath.Set) {
if v.MapValue == nil {
set.Insert(path)
return
}
// We have a map.
// copy the existing path, append each item, and recursively call.
for i := range v.MapValue.Items {
child := v.MapValue.Items[i]
np := path.Copy()
np = append(np, fieldpath.PathElement{FieldName: &child.Name})
fieldsetDeduced(child.Value, np, set)
}
}
func (dv deducedTypedValue) Merge(pso TypedValue) (TypedValue, error) {
tpso, ok := pso.(deducedTypedValue)
if !ok {
return nil, errorFormatter{}.
errorf("can't merge deducedTypedValue with %T", tpso)
}
return AsTypedDeduced(mergeDeduced(dv.value, tpso.value)), nil
}
func mergeDeduced(lhs, rhs value.Value) value.Value {
// If both sides are maps, merge them, otherwise return right
// side.
if rhs.MapValue == nil || lhs.MapValue == nil {
return rhs
}
v := value.Value{MapValue: &value.Map{}}
for i := range lhs.MapValue.Items {
child := lhs.MapValue.Items[i]
v.MapValue.Set(child.Name, child.Value)
}
for i := range rhs.MapValue.Items {
child := rhs.MapValue.Items[i]
if sub, ok := v.MapValue.Get(child.Name); ok {
new := mergeDeduced(sub.Value, child.Value)
v.MapValue.Set(child.Name, new)
} else {
v.MapValue.Set(child.Name, child.Value)
}
}
return v
}
func (dv deducedTypedValue) Compare(rhs TypedValue) (c *Comparison, err error) {
trhs, ok := rhs.(deducedTypedValue)
if !ok {
return nil, errorFormatter{}.
errorf("can't merge deducedTypedValue with %T", rhs)
}
c = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
added(dv.value, trhs.value, fieldpath.Path{}, c.Added)
added(trhs.value, dv.value, fieldpath.Path{}, c.Removed)
modified(dv.value, trhs.value, fieldpath.Path{}, c.Modified)
merge, err := dv.Merge(rhs)
if err != nil {
return nil, err
}
c.Merged = merge
return c, nil
}
func added(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
if lhs.MapValue == nil && rhs.MapValue == nil {
// Both non-maps, nothing added, do nothing.
} else if lhs.MapValue == nil && rhs.MapValue != nil {
// From leaf to map, add leaf fields of map.
fieldsetDeduced(rhs, path, set)
} else if lhs.MapValue != nil && rhs.MapValue == nil {
// Went from map to field, add field.
set.Insert(path)
} else {
// Both are maps.
for i := range rhs.MapValue.Items {
child := rhs.MapValue.Items[i]
np := path.Copy()
np = append(np, fieldpath.PathElement{FieldName: &child.Name})
if v, ok := lhs.MapValue.Get(child.Name); ok {
added(v.Value, child.Value, np, set)
} else {
fieldsetDeduced(child.Value, np, set)
}
}
}
}
func modified(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
if lhs.MapValue == nil && rhs.MapValue == nil {
if !reflect.DeepEqual(lhs, rhs) {
set.Insert(path)
}
} else if lhs.MapValue != nil && rhs.MapValue != nil {
// Both are maps.
for i := range rhs.MapValue.Items {
child := rhs.MapValue.Items[i]
v, ok := lhs.MapValue.Get(child.Name)
if !ok {
continue
}
np := path.Copy()
np = append(np, fieldpath.PathElement{FieldName: &child.Name})
modified(v.Value, child.Value, np, set)
}
}
}
// RemoveItems does nothing because all lists in a deducedTypedValue are considered atomic,
// and there are no maps because it is indistinguishable from a struct.
func (dv deducedTypedValue) RemoveItems(_ *fieldpath.Set) TypedValue {
return dv
}

View File

@ -129,6 +129,9 @@ func (ef errorFormatter) validateScalar(t schema.Scalar, v *value.Value, prefix
if v == nil {
return nil
}
if v.Null {
return nil
}
switch t {
case schema.Numeric:
if v.FloatValue == nil && v.IntValue == nil {

View File

@ -68,47 +68,80 @@ func (p *Parser) TypeNames() (names []string) {
// Type returns a helper which can produce objects of the given type. Any
// errors are deferred until a further function is called.
func (p *Parser) Type(name string) *ParseableType {
return &ParseableType{
func (p *Parser) Type(name string) ParseableType {
return &parseableType{
parser: p,
typename: name,
}
}
// ParseableType allows for easy production of typed objects.
type ParseableType struct {
type ParseableType interface {
IsValid() bool
FromYAML(YAMLObject) (TypedValue, error)
FromUnstructured(interface{}) (TypedValue, error)
}
type parseableType struct {
parser *Parser
typename string
}
var _ ParseableType = &parseableType{}
// IsValid return true if p's schema and typename are valid.
func (p *ParseableType) IsValid() bool {
func (p *parseableType) IsValid() bool {
_, ok := p.parser.Schema.Resolve(schema.TypeRef{NamedType: &p.typename})
return ok
}
// New returns a new empty object with the current schema and the
// type "typename".
func (p *ParseableType) New() (TypedValue, error) {
return p.FromYAML(YAMLObject("{}"))
}
// FromYAML parses a yaml string into an object with the current schema
// and the type "typename" or an error if validation fails.
func (p *ParseableType) FromYAML(object YAMLObject) (TypedValue, error) {
func (p *parseableType) FromYAML(object YAMLObject) (TypedValue, error) {
v, err := value.FromYAML([]byte(object))
if err != nil {
return TypedValue{}, err
return nil, err
}
return AsTyped(v, &p.parser.Schema, p.typename)
}
// FromUnstructured converts a go interface to a TypedValue. It will return an
// error if the resulting object fails schema validation.
func (p *ParseableType) FromUnstructured(in interface{}) (TypedValue, error) {
func (p *parseableType) FromUnstructured(in interface{}) (TypedValue, error) {
v, err := value.FromUnstructured(in)
if err != nil {
return TypedValue{}, err
return nil, err
}
return AsTyped(v, &p.parser.Schema, p.typename)
}
// DeducedParseableType is a ParseableType that deduces the type from
// the content of the object.
type DeducedParseableType struct{}
var _ ParseableType = DeducedParseableType{}
// IsValid always returns true for a DeducedParseableType.
func (p DeducedParseableType) IsValid() bool {
return true
}
// FromYAML parses a yaml string into an object and deduces the type for
// that object.
func (p DeducedParseableType) FromYAML(object YAMLObject) (TypedValue, error) {
v, err := value.FromYAML([]byte(object))
if err != nil {
return nil, err
}
return AsTypedDeduced(v), nil
}
// FromUnstructured converts a go interface to a TypedValue. It will return an
// error if the input object uses un-handled types.
func (p DeducedParseableType) FromUnstructured(in interface{}) (TypedValue, error) {
v, err := value.FromUnstructured(in)
if err != nil {
return nil, err
}
return AsTypedDeduced(v), nil
}

View File

@ -0,0 +1,119 @@
/*
Copyright 2019 The Kubernetes Authors.
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 typed
import (
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/schema"
"sigs.k8s.io/structured-merge-diff/value"
)
type removingWalker struct {
value *value.Value
schema *schema.Schema
toRemove *fieldpath.Set
}
func removeItemsWithSchema(value *value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef) {
w := &removingWalker{
value: value,
schema: schema,
toRemove: toRemove,
}
resolveSchema(schema, typeRef, w)
}
// doLeaf should be called on leaves before descending into children, if there
// will be a descent. It modifies w.inLeaf.
func (w *removingWalker) doLeaf() ValidationErrors { return nil }
func (w *removingWalker) doScalar(t schema.Scalar) ValidationErrors { return nil }
func (w *removingWalker) doStruct(t schema.Struct) ValidationErrors {
s := w.value.MapValue
// If struct is null, empty, or atomic just return
if s == nil || len(s.Items) == 0 || t.ElementRelationship == schema.Atomic {
return nil
}
fieldTypes := map[string]schema.TypeRef{}
for _, structField := range t.Fields {
fieldTypes[structField.Name] = structField.Type
}
for i, _ := range s.Items {
item := s.Items[i]
pe := fieldpath.PathElement{FieldName: &item.Name}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
removeItemsWithSchema(&s.Items[i].Value, subset, w.schema, fieldTypes[item.Name])
}
}
return nil
}
func (w *removingWalker) doList(t schema.List) (errs ValidationErrors) {
l := w.value.ListValue
// If list is null, empty, or atomic just return
if l == nil || len(l.Items) == 0 || t.ElementRelationship == schema.Atomic {
return nil
}
newItems := []value.Value{}
for i, _ := range l.Items {
item := l.Items[i]
// Ignore error because we have already validated this list
pe, _ := listItemToPathElement(t, i, item)
path, _ := fieldpath.MakePath(pe)
if w.toRemove.Has(path) {
continue
}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
removeItemsWithSchema(&l.Items[i], subset, w.schema, t.ElementType)
}
newItems = append(newItems, l.Items[i])
}
l.Items = newItems
return nil
}
func (w *removingWalker) doMap(t schema.Map) ValidationErrors {
m := w.value.MapValue
// If map is null, empty, or atomic just return
if m == nil || len(m.Items) == 0 || t.ElementRelationship == schema.Atomic {
return nil
}
newMap := &value.Map{}
for i, _ := range m.Items {
item := m.Items[i]
pe := fieldpath.PathElement{FieldName: &item.Name}
path, _ := fieldpath.MakePath(pe)
if w.toRemove.Has(path) {
continue
}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
removeItemsWithSchema(&m.Items[i].Value, subset, w.schema, t.ElementType)
}
newMap.Set(item.Name, m.Items[i].Value)
}
w.value.MapValue = newMap
return nil
}
func (*removingWalker) doUntyped(_ schema.Untyped) ValidationErrors { return nil }
func (*removingWalker) errorf(_ string, _ ...interface{}) ValidationErrors { return nil }

View File

@ -25,44 +25,87 @@ import (
"sigs.k8s.io/structured-merge-diff/value"
)
// TypedValue is a value of some specific type.
type TypedValue struct {
value value.Value
typeRef schema.TypeRef
schema *schema.Schema
// TypedValue is a value with an associated type.
type TypedValue interface {
// AsValue removes the type from the TypedValue and only keeps the value.
AsValue() *value.Value
// Validate returns an error with a list of every spec violation.
Validate() error
// ToFieldSet creates a set containing every leaf field mentioned, or
// validation errors, if any were encountered.
ToFieldSet() (*fieldpath.Set, error)
// Merge returns the result of merging tv and pso ("partially specified
// object") together. Of note:
// * No fields can be removed by this operation.
// * If both tv and pso specify a given leaf field, the result will keep pso's
// value.
// * Container typed elements will have their items ordered:
// * like tv, if pso doesn't change anything in the container
// * like pso, if pso does change something in the container.
// tv and pso must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
Merge(pso TypedValue) (TypedValue, error)
// Compare compares the two objects. See the comments on the `Comparison`
// struct for details on the return value.
//
// tv and rhs must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
Compare(rhs TypedValue) (c *Comparison, err error)
// RemoveItems removes each provided list or map item from the value.
RemoveItems(items *fieldpath.Set) TypedValue
}
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
// type 'typeName' in the schema. An error is returned if the v doesn't conform
// to the schema.
func AsTyped(v value.Value, s *schema.Schema, typeName string) (TypedValue, error) {
tv := TypedValue{
tv := typedValue{
value: v,
typeRef: schema.TypeRef{NamedType: &typeName},
schema: s,
}
if err := tv.Validate(); err != nil {
return TypedValue{}, err
return nil, err
}
return tv, nil
}
// AsValue removes the type from the TypedValue and only keeps the value.
func (tv TypedValue) AsValue() *value.Value {
// AsTypeUnvalidated is just like AsTyped, but doesn't validate that the type
// conforms to the schema, for cases where that has already been checked or
// where you're going to call a method that validates as a side-effect (like
// ToFieldSet).
func AsTypedUnvalidated(v value.Value, s *schema.Schema, typeName string) TypedValue {
tv := typedValue{
value: v,
typeRef: schema.TypeRef{NamedType: &typeName},
schema: s,
}
return tv
}
// typedValue is a value of some specific type.
type typedValue struct {
value value.Value
typeRef schema.TypeRef
schema *schema.Schema
}
var _ TypedValue = typedValue{}
func (tv typedValue) AsValue() *value.Value {
return &tv.value
}
// Validate returns an error with a list of every spec violation.
func (tv TypedValue) Validate() error {
func (tv typedValue) Validate() error {
if errs := tv.walker().validate(); len(errs) != 0 {
return errs
}
return nil
}
// ToFieldSet creates a set containing every leaf field mentioned in tv, or
// validation errors, if any were encountered.
func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
func (tv typedValue) ToFieldSet() (*fieldpath.Set, error) {
s := fieldpath.NewSet()
w := tv.walker()
w.leafFieldCallback = func(p fieldpath.Path) { s.Insert(p) }
@ -72,19 +115,91 @@ func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
return s, nil
}
// Merge returns the result of merging tv and pso ("partially specified
// object") together. Of note:
// * No fields can be removed by this operation.
// * If both tv and pso specify a given leaf field, the result will keep pso's
// value.
// * Container typed elements will have their items ordered:
// * like tv, if pso doesn't change anything in the container
// * like pso, if pso does change something in the container.
// tv and pso must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Merge(pso TypedValue) (TypedValue, error) {
return merge(tv, pso, ruleKeepRHS, nil)
func (tv typedValue) Merge(pso TypedValue) (TypedValue, error) {
tpso, ok := pso.(typedValue)
if !ok {
return nil, errorFormatter{}.
errorf("can't merge typedValue with %T", pso)
}
return merge(tv, tpso, ruleKeepRHS, nil)
}
func (tv typedValue) Compare(rhs TypedValue) (c *Comparison, err error) {
trhs, ok := rhs.(typedValue)
if !ok {
return nil, errorFormatter{}.
errorf("can't compare typedValue with %T", rhs)
}
c = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
c.Merged, err = merge(tv, trhs, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
} else if !reflect.DeepEqual(w.rhs, w.lhs) {
// TODO: reflect.DeepEqual is not sufficient for this.
// Need to implement equality check on the value type.
c.Modified.Insert(w.path)
}
ruleKeepRHS(w)
}, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
}
})
if err != nil {
return nil, err
}
return c, nil
}
// RemoveItems removes each provided list or map item from the value.
func (tv typedValue) RemoveItems(items *fieldpath.Set) TypedValue {
removeItemsWithSchema(&tv.value, items, tv.schema, tv.typeRef)
return tv
}
func merge(lhs, rhs typedValue, rule, postRule mergeRule) (TypedValue, error) {
if lhs.schema != rhs.schema {
return nil, errorFormatter{}.
errorf("expected objects with types from the same schema")
}
if !reflect.DeepEqual(lhs.typeRef, rhs.typeRef) {
return nil, errorFormatter{}.
errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
mw := mergingWalker{
lhs: &lhs.value,
rhs: &rhs.value,
schema: lhs.schema,
typeRef: lhs.typeRef,
rule: rule,
postItemHook: postRule,
}
errs := mw.merge()
if len(errs) > 0 {
return nil, errs
}
out := typedValue{
schema: lhs.schema,
typeRef: lhs.typeRef,
}
if mw.out == nil {
out.value = value.Value{Null: true}
} else {
out.value = *mw.out
}
return out, nil
}
// Comparison is the return value of a TypedValue.Compare() operation.
@ -125,89 +240,3 @@ func (c *Comparison) String() string {
}
return str
}
// Compare compares the two objects. See the comments on the `Comparison`
// struct for details on the return value.
//
// tv and rhs must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Compare(rhs TypedValue) (c *Comparison, err error) {
c = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
c.Merged, err = merge(tv, rhs, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
} else if !reflect.DeepEqual(w.rhs, w.lhs) {
// TODO: reflect.DeepEqual is not sufficient for this.
// Need to implement equality check on the value type.
c.Modified.Insert(w.path)
}
ruleKeepRHS(w)
}, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
}
})
if err != nil {
return nil, err
}
return c, nil
}
func merge(lhs, rhs TypedValue, rule, postRule mergeRule) (TypedValue, error) {
if lhs.schema != rhs.schema {
return TypedValue{}, errorFormatter{}.
errorf("expected objects with types from the same schema")
}
if !reflect.DeepEqual(lhs.typeRef, rhs.typeRef) {
return TypedValue{}, errorFormatter{}.
errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
mw := mergingWalker{
lhs: &lhs.value,
rhs: &rhs.value,
schema: lhs.schema,
typeRef: lhs.typeRef,
rule: rule,
postItemHook: postRule,
}
errs := mw.merge()
if len(errs) > 0 {
return TypedValue{}, errs
}
out := TypedValue{
schema: lhs.schema,
typeRef: lhs.typeRef,
}
if mw.out == nil {
out.value = value.Value{Null: true}
} else {
out.value = *mw.out
}
return out, nil
}
// AsTypeUnvalidated is just like WithType, but doesn't validate that the type
// conforms to the schema, for cases where that has already been checked or
// where you're going to call a method that validates as a side-effect (like
// ToFieldSet).
func AsTypedUnvalidated(v value.Value, s *schema.Schema, typeName string) TypedValue {
tv := TypedValue{
value: v,
typeRef: schema.TypeRef{NamedType: &typeName},
schema: s,
}
return tv
}

View File

@ -22,7 +22,7 @@ import (
"sigs.k8s.io/structured-merge-diff/value"
)
func (tv TypedValue) walker() *validatingObjectWalker {
func (tv typedValue) walker() *validatingObjectWalker {
return &validatingObjectWalker{
value: tv.value,
schema: tv.schema,

View File

@ -31,7 +31,7 @@ import (
func FromYAML(input []byte) (Value, error) {
var decoded interface{}
if len(input) == 0 || (len(input) == 4 && string(input) == "null") {
if len(input) == 4 && string(input) == "null" {
// Special case since the yaml package doesn't accurately
// preserve this.
return Value{Null: true}, nil