Skip to content

Commit 2e98b16

Browse files
committed
prototype showing that granular->atomic changes can be fixed up by a pre-apply traversal
1 parent bc70e0d commit 2e98b16

File tree

3 files changed

+385
-1
lines changed

3 files changed

+385
-1
lines changed

merge/update.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ func (s *Updater) update(oldObject, newObject *typed.TypedValue, version fieldpa
122122
// this is a CREATE call).
123123
func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string) (*typed.TypedValue, fieldpath.ManagedFields, error) {
124124
var err error
125+
managers, err = s.fixupManagedFields(liveObject, managers)
126+
if err != nil {
127+
return nil, fieldpath.ManagedFields{}, err
128+
}
125129
if s.enableUnions {
126130
newObject, err = liveObject.NormalizeUnions(newObject)
127131
if err != nil {
@@ -152,13 +156,48 @@ func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldp
152156
return newObject, managers, nil
153157
}
154158

159+
// fixupManagedFields attempts to fix up the managed fields to be compatible with the live state of
160+
// the schema.
161+
// This is needed when an "unversioned" change are made to a schema that impact the managedFields.
162+
// By "unversionsed" we mean that a specific version of a schema was directly modified (introduction
163+
// of new versions does not require fixup).
164+
//
165+
// Supported schema changes:
166+
// - Changing a type to "atomic": All field manager that managed any granular fields become managers
167+
// of the atomic to ensure a conflict if any of the fields they previously managed are changed.
168+
// Granular managed fields of the struct are removed.
169+
//
170+
// TODO: Changes to add support for:
171+
// - Change a type from "atomic" to "granular", "set" or "map". All field manager that managed the
172+
// atomic become managers of all child fields to ensure a conflict if any child field is changed.
173+
//
174+
// Potential future optimization: Write a SHA-256 hash to managedFields that can be compared
175+
// with the current hash of the live schemas to determine if any fixup is needed.
176+
func (s *Updater) fixupManagedFields(liveObject *typed.TypedValue, managers fieldpath.ManagedFields) (fieldpath.ManagedFields, error) {
177+
// TODO(jpbetz): I will use Converter to get schemas at different versions when prototyping, but might need to swap that out with something else for production? Is mixed version apply is quite rare?
178+
result := fieldpath.ManagedFields{}
179+
for manager, versionedSet := range managers {
180+
tv, err := s.Converter.Convert(liveObject, versionedSet.APIVersion())
181+
if err != nil {
182+
return nil, err
183+
}
184+
fieldSet := versionedSet.Set()
185+
fixedFieldSet, err := typed.FixupFieldManagers(fieldSet, tv)
186+
result[manager] = fieldpath.NewVersionedSet(fixedFieldSet, versionedSet.APIVersion(), versionedSet.Applied())
187+
}
188+
return result, nil
189+
}
190+
155191
// Apply should be called when Apply is run, given the current object as
156192
// well as the configuration that is applied. This will merge the object
157193
// and return it. If the object hasn't changed, nil is returned (the
158194
// managers can still have changed though).
159195
func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (*typed.TypedValue, fieldpath.ManagedFields, error) {
160-
managers = shallowCopyManagers(managers)
161196
var err error
197+
managers, err = s.fixupManagedFields(liveObject, managers)
198+
if err != nil {
199+
return nil, fieldpath.ManagedFields{}, err
200+
}
162201
if s.enableUnions {
163202
configObject, err = configObject.NormalizeUnionsApply(configObject)
164203
if err != nil {

typed/field_manager_fixup.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package typed
18+
19+
import (
20+
"fmt"
21+
"sync"
22+
23+
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
24+
"sigs.k8s.io/structured-merge-diff/v4/schema"
25+
)
26+
27+
var fmPool = sync.Pool{
28+
New: func() interface{} { return &fixupObjectWalker{} },
29+
}
30+
31+
func (v *fixupObjectWalker) finished() {
32+
v.fieldSet = nil
33+
v.schema = nil
34+
v.typeRef = schema.TypeRef{}
35+
fmPool.Put(v)
36+
}
37+
38+
type fixupObjectWalker struct {
39+
fieldSet *fieldpath.Set // the field set to fixup
40+
schema *schema.Schema // the schema, must be the same APIVersion as the fieldSet
41+
typeRef schema.TypeRef // current type
42+
path fieldpath.Path // current path
43+
44+
out *fieldpath.Set // the resulting fixed up field set, TODO if out is nil, interpret it to mean that there is no change
45+
46+
// Allocate only as many walkers as needed for the depth by storing them here.
47+
spareWalkers *[]*fixupObjectWalker
48+
}
49+
50+
func (v *fixupObjectWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *fixupObjectWalker {
51+
if v.spareWalkers == nil {
52+
// first descent.
53+
v.spareWalkers = &[]*fixupObjectWalker{}
54+
}
55+
var v2 *fixupObjectWalker
56+
if n := len(*v.spareWalkers); n > 0 {
57+
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
58+
} else {
59+
v2 = &fixupObjectWalker{}
60+
}
61+
*v2 = *v
62+
v2.typeRef = tr
63+
v2.path = append(v.path.Copy(), pe) // TODO: avoid copy?
64+
return v2
65+
}
66+
67+
func (v *fixupObjectWalker) finishDescent(v2 *fixupObjectWalker) {
68+
v.fieldSet = nil
69+
v.schema = nil
70+
v.path = nil
71+
72+
v.typeRef = schema.TypeRef{}
73+
74+
// if the descent caused a realloc, ensure that we reuse the buffer
75+
// for the next sibling.
76+
*v.spareWalkers = append(*v.spareWalkers, v2)
77+
}
78+
79+
// FixupFieldManagers attempts to fix up the managed fields to be compatible with the live state of
80+
// the schema.
81+
// This is needed when an "unversioned" change are made to a schema that impact the managedFields.
82+
// By "unversionsed" we mean that a specific version of a schema was directly modified (introduction
83+
// of new versions does not require fixup).
84+
//
85+
// Supported schema changes:
86+
// - Changing a type to "atomic": All field manager that managed any granular fields become managers
87+
// of the atomic to ensure a conflict if any of the fields they previously managed are changed.
88+
// Granular managed fields of the struct are removed.
89+
func FixupFieldManagers(fieldSet *fieldpath.Set, tv *TypedValue) (*fieldpath.Set, error) {
90+
v := fmPool.Get().(*fixupObjectWalker)
91+
v.fieldSet = fieldSet
92+
v.schema = tv.schema
93+
v.typeRef = tv.typeRef
94+
95+
defer v.finished()
96+
errs := v.fixupFieldManagers()
97+
if len(errs) > 0 {
98+
return nil, fmt.Errorf("fixup errors: %s", errs.Error())
99+
}
100+
return v.out, nil
101+
}
102+
103+
func (v *fixupObjectWalker) fixupFieldManagers() (errs ValidationErrors) {
104+
a, ok := v.schema.Resolve(v.typeRef)
105+
if !ok {
106+
errs = append(errs, errorf("could not resolve %v", v.typeRef)...)
107+
return
108+
}
109+
return handleAtom(a, v.typeRef, v)
110+
}
111+
112+
func (v *fixupObjectWalker) doScalar(t *schema.Scalar) (errs ValidationErrors) {
113+
v.out = v.fieldSet
114+
return errs
115+
}
116+
117+
func (v *fixupObjectWalker) visitListItems(t *schema.List, element *fieldpath.Set) (errs ValidationErrors) {
118+
out := fieldpath.NewSet()
119+
element.Children.Iterate(func(pe fieldpath.PathElement) {
120+
v2 := v.prepareDescent(pe, t.ElementType)
121+
v2.fieldSet = element.Children.Descend(pe)
122+
errs = append(errs, v2.fixupFieldManagers()...)
123+
out = out.Union(v2.out) // TODO(jpbetz): If nothing has changed, return null (and optimize the code paths for this)
124+
v.finishDescent(v2)
125+
})
126+
element.Members.Iterate(func(pe fieldpath.PathElement) {
127+
path := append(v.path.Copy(), pe) // TODO: avoid copy?
128+
out.Insert(path)
129+
})
130+
v.out = out
131+
return nil
132+
}
133+
134+
func (v *fixupObjectWalker) doList(t *schema.List) (errs ValidationErrors) {
135+
// fix up lists changed to 'atomic' by:
136+
// - including the list in the managed fields if any of the child field elements are present
137+
// - removing all child field elements from the managed fields
138+
if t.ElementRelationship == schema.Atomic { // TODO: is this right for both Separable->Atomic and Associative->Atomic
139+
if v.fieldSet.Size() > 0 {
140+
v.out = fieldpath.NewSet(v.path)
141+
}
142+
return errs
143+
}
144+
// TODO: Support Atomic->Separable, Atomic->Associative
145+
146+
errs = v.visitListItems(t, v.fieldSet)
147+
return errs
148+
}
149+
150+
func (v *fixupObjectWalker) visitMapItems(t *schema.Map, element *fieldpath.Set) (errs ValidationErrors) {
151+
out := fieldpath.NewSet()
152+
element.Children.Iterate(func(pe fieldpath.PathElement) {
153+
tr := t.ElementType
154+
if pe.FieldName != nil {
155+
if sf, ok := t.FindField(*pe.FieldName); ok {
156+
tr = sf.Type
157+
}
158+
}
159+
if (tr == schema.TypeRef{}) {
160+
errs = append(errs, errorf("field not declared in schema").WithPrefix(pe.String())...)
161+
return
162+
}
163+
v2 := v.prepareDescent(pe, tr)
164+
v2.fieldSet = element.Children.Descend(pe)
165+
errs = append(errs, v2.fixupFieldManagers()...)
166+
out = out.Union(v2.out) // TODO(jpbetz): If nothing has changed, return null (and optimize the code paths for this)
167+
v.finishDescent(v2)
168+
})
169+
element.Members.Iterate(func(pe fieldpath.PathElement) {
170+
path := append(v.path.Copy(), pe) // TODO: avoid copy?
171+
out.Insert(path)
172+
})
173+
v.out = out
174+
return nil
175+
}
176+
177+
func (v *fixupObjectWalker) doMap(t *schema.Map) (errs ValidationErrors) {
178+
// fix up maps/structs changed to 'atomic' by:
179+
// - including the map in the managed fields if any of the child field elements are present
180+
// - removing all child field elements from the managed fields
181+
if t.ElementRelationship == schema.Atomic {
182+
if v.fieldSet.Size() > 0 {
183+
v.out = fieldpath.NewSet(v.path)
184+
}
185+
return errs
186+
}
187+
// TODO: Support Atomic->Separable
188+
189+
errs = v.visitMapItems(t, v.fieldSet)
190+
return errs
191+
}

0 commit comments

Comments
 (0)