1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package typed
18
19import (
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
27var fmPool = sync.Pool{
28	New: func() interface{} { return &reconcileWithSchemaWalker{} },
29}
30
31func (v *reconcileWithSchemaWalker) finished() {
32	v.fieldSet = nil
33	v.schema = nil
34	v.value = nil
35	v.typeRef = schema.TypeRef{}
36	v.path = nil
37	v.toRemove = nil
38	v.toAdd = nil
39	fmPool.Put(v)
40}
41
42type reconcileWithSchemaWalker struct {
43	value  *TypedValue    // root of the live object
44	schema *schema.Schema // root of the live schema
45
46	// state of node being visited by walker
47	fieldSet *fieldpath.Set
48	typeRef  schema.TypeRef
49	path     fieldpath.Path
50	isAtomic bool
51
52	// the accumulated diff to perform to apply reconciliation
53	toRemove *fieldpath.Set // paths to remove recursively
54	toAdd    *fieldpath.Set // paths to add after any removals
55
56	// Allocate only as many walkers as needed for the depth by storing them here.
57	spareWalkers *[]*reconcileWithSchemaWalker
58}
59
60func (v *reconcileWithSchemaWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *reconcileWithSchemaWalker {
61	if v.spareWalkers == nil {
62		// first descent.
63		v.spareWalkers = &[]*reconcileWithSchemaWalker{}
64	}
65	var v2 *reconcileWithSchemaWalker
66	if n := len(*v.spareWalkers); n > 0 {
67		v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
68	} else {
69		v2 = &reconcileWithSchemaWalker{}
70	}
71	*v2 = *v
72	v2.typeRef = tr
73	v2.path = append(v.path, pe)
74	v2.value = v.value
75	return v2
76}
77
78func (v *reconcileWithSchemaWalker) finishDescent(v2 *reconcileWithSchemaWalker) {
79	v2.fieldSet = nil
80	v2.schema = nil
81	v2.value = nil
82	v2.typeRef = schema.TypeRef{}
83	if cap(v2.path) < 20 { // recycle slices that do not have unexpectedly high capacity
84		v2.path = v2.path[:0]
85	} else {
86		v2.path = nil
87	}
88
89	// merge any accumulated changes into parent walker
90	if v2.toRemove != nil {
91		if v.toRemove == nil {
92			v.toRemove = v2.toRemove
93		} else {
94			v.toRemove = v.toRemove.Union(v2.toRemove)
95		}
96	}
97	if v2.toAdd != nil {
98		if v.toAdd == nil {
99			v.toAdd = v2.toAdd
100		} else {
101			v.toAdd = v.toAdd.Union(v2.toAdd)
102		}
103	}
104	v2.toRemove = nil
105	v2.toAdd = nil
106
107	// if the descent caused a realloc, ensure that we reuse the buffer
108	// for the next sibling.
109	*v.spareWalkers = append(*v.spareWalkers, v2)
110}
111
112// ReconcileFieldSetWithSchema reconciles the a field set with any changes to the
113//// object's schema since the field set was written. Returns the reconciled field set, or nil of
114// no changes were made to the field set.
115//
116// Supports:
117// - changing types from atomic to granular
118// - changing types from granular to atomic
119func ReconcileFieldSetWithSchema(fieldset *fieldpath.Set, tv *TypedValue) (*fieldpath.Set, error) {
120	v := fmPool.Get().(*reconcileWithSchemaWalker)
121	v.fieldSet = fieldset
122	v.value = tv
123
124	v.schema = tv.schema
125	v.typeRef = tv.typeRef
126
127	defer v.finished()
128	errs := v.reconcile()
129
130	if len(errs) > 0 {
131		return nil, fmt.Errorf("errors reconciling field set with schema: %s", errs.Error())
132	}
133
134	// If there are any accumulated changes, apply them
135	if v.toAdd != nil || v.toRemove != nil {
136		out := v.fieldSet
137		if v.toRemove != nil {
138			out = out.RecursiveDifference(v.toRemove)
139		}
140		if v.toAdd != nil {
141			out = out.Union(v.toAdd)
142		}
143		return out, nil
144	}
145	return nil, nil
146}
147
148func (v *reconcileWithSchemaWalker) reconcile() (errs ValidationErrors) {
149	a, ok := v.schema.Resolve(v.typeRef)
150	if !ok {
151		errs = append(errs, errorf("could not resolve %v", v.typeRef)...)
152		return
153	}
154	return handleAtom(a, v.typeRef, v)
155}
156
157func (v *reconcileWithSchemaWalker) doScalar(_ *schema.Scalar) (errs ValidationErrors) {
158	return errs
159}
160
161func (v *reconcileWithSchemaWalker) visitListItems(t *schema.List, element *fieldpath.Set) (errs ValidationErrors) {
162	handleElement := func(pe fieldpath.PathElement, isMember bool) {
163		var hasChildren bool
164		v2 := v.prepareDescent(pe, t.ElementType)
165		v2.fieldSet, hasChildren = element.Children.Get(pe)
166		v2.isAtomic = isMember && !hasChildren
167		errs = append(errs, v2.reconcile()...)
168		v.finishDescent(v2)
169	}
170	element.Children.Iterate(func(pe fieldpath.PathElement) {
171		if element.Members.Has(pe) {
172			return
173		}
174		handleElement(pe, false)
175	})
176	element.Members.Iterate(func(pe fieldpath.PathElement) {
177		handleElement(pe, true)
178	})
179	return errs
180}
181
182func (v *reconcileWithSchemaWalker) doList(t *schema.List) (errs ValidationErrors) {
183	// reconcile lists changed from granular to atomic.
184	// Note that migrations from atomic to granular are not recommended and will
185	// be treated as if they were always granular.
186	//
187	// In this case, the manager that owned the previously atomic field (and all subfields),
188	// will now own just the top-level field and none of the subfields.
189	if !v.isAtomic && t.ElementRelationship == schema.Atomic {
190		v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
191		v.toAdd = fieldpath.NewSet(v.path)    // add the root of the atomic
192		return errs
193	}
194	if v.fieldSet != nil {
195		errs = v.visitListItems(t, v.fieldSet)
196	}
197	return errs
198}
199
200func (v *reconcileWithSchemaWalker) visitMapItems(t *schema.Map, element *fieldpath.Set) (errs ValidationErrors) {
201	handleElement := func(pe fieldpath.PathElement, isMember bool) {
202		var hasChildren bool
203		if tr, ok := typeRefAtPath(t, pe); ok { // ignore fields not in the schema
204			v2 := v.prepareDescent(pe, tr)
205			v2.fieldSet, hasChildren = element.Children.Get(pe)
206			v2.isAtomic = isMember && !hasChildren
207			errs = append(errs, v2.reconcile()...)
208			v.finishDescent(v2)
209		}
210	}
211	element.Children.Iterate(func(pe fieldpath.PathElement) {
212		if element.Members.Has(pe) {
213			return
214		}
215		handleElement(pe, false)
216	})
217	element.Members.Iterate(func(pe fieldpath.PathElement) {
218		handleElement(pe, true)
219	})
220
221	return errs
222}
223
224func (v *reconcileWithSchemaWalker) doMap(t *schema.Map) (errs ValidationErrors) {
225	// We don't currently reconcile deduced types (unstructured CRDs) or maps that contain only unknown
226	// fields since deduced types do not yet support atomic or granular tags.
227	if isUntypedDeducedMap(t) {
228		return errs
229	}
230
231	// reconcile maps and structs changed from granular to atomic.
232	// Note that migrations from atomic to granular are not recommended and will
233	// be treated as if they were always granular.
234	//
235	// In this case the manager that owned the previously atomic field (and all subfields),
236	// will now own just the top-level field and none of the subfields.
237	if !v.isAtomic && t.ElementRelationship == schema.Atomic {
238		if v.fieldSet != nil && v.fieldSet.Size() > 0 {
239			v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
240			v.toAdd = fieldpath.NewSet(v.path)    // add the root of the atomic
241		}
242		return errs
243	}
244	if v.fieldSet != nil {
245		errs = v.visitMapItems(t, v.fieldSet)
246	}
247	return errs
248}
249
250func fieldSetAtPath(node *fieldpath.Set, path fieldpath.Path) (*fieldpath.Set, bool) {
251	ok := true
252	for _, pe := range path {
253		if node, ok = node.Children.Get(pe); !ok {
254			break
255		}
256	}
257	return node, ok
258}
259
260func descendToPath(node *fieldpath.Set, path fieldpath.Path) *fieldpath.Set {
261	for _, pe := range path {
262		node = node.Children.Descend(pe)
263	}
264	return node
265}
266
267func typeRefAtPath(t *schema.Map, pe fieldpath.PathElement) (schema.TypeRef, bool) {
268	tr := t.ElementType
269	if pe.FieldName != nil {
270		if sf, ok := t.FindField(*pe.FieldName); ok {
271			tr = sf.Type
272		}
273	}
274	return tr, tr != schema.TypeRef{}
275}
276
277// isUntypedDeducedMap returns true if m has no fields defined, but allows untyped elements.
278// This is equivalent to a openAPI object that has x-kubernetes-preserve-unknown-fields=true
279// but does not have any properties defined on the object.
280func isUntypedDeducedMap(m *schema.Map) bool {
281	return isUntypedDeducedRef(m.ElementType) && m.Fields == nil
282}
283
284func isUntypedDeducedRef(t schema.TypeRef) bool {
285	if t.NamedType != nil {
286		return *t.NamedType == "__untyped_deduced_"
287	}
288	atom := t.Inlined
289	return atom.Scalar != nil && *atom.Scalar == "untyped"
290}
291