Skip to content

Commit 5bd90b3

Browse files
authored
Merge pull request #60 from jennybuckley/remove-disowned
Remove removed items from maps and lists on apply
2 parents 302c24c + c5df12b commit 5bd90b3

File tree

6 files changed

+170
-26
lines changed

6 files changed

+170
-26
lines changed

fieldpath/set.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ func (s *Set) iteratePrefix(prefix Path, f func(Path)) {
154154
s.Children.iteratePrefix(prefix, f)
155155
}
156156

157+
// WithPrefix returns the subset of paths which begin with the given prefix,
158+
// with the prefix not included.
159+
func (s *Set) WithPrefix(pe PathElement) *Set {
160+
subset, ok := s.Children.Get(pe)
161+
if !ok {
162+
return NewSet()
163+
}
164+
return subset
165+
}
166+
157167
// setNode is a pair of PathElement / Set, for the purpose of expressing
158168
// nested set membership.
159169
type setNode struct {

merge/set_test.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,6 @@ func TestUpdateSet(t *testing.T) {
304304
},
305305
},
306306
},
307-
}
308-
309-
for name, test := range tests {
310-
t.Run(name, func(t *testing.T) {
311-
if err := test.Test(setFieldsParser); err != nil {
312-
t.Fatal(err)
313-
}
314-
})
315-
}
316-
}
317-
318-
func TestUpdateSetBroken(t *testing.T) {
319-
tests := map[string]TestCase{
320307
"apply_twice_remove": {
321308
Ops: []Operation{
322309
Apply{
@@ -359,8 +346,8 @@ func TestUpdateSetBroken(t *testing.T) {
359346

360347
for name, test := range tests {
361348
t.Run(name, func(t *testing.T) {
362-
if test.Test(setFieldsParser) == nil {
363-
t.Fatalf("Broken test passed.")
349+
if err := test.Test(setFieldsParser); err != nil {
350+
t.Fatal(err)
364351
}
365352
})
366353
}

merge/update.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ func (s *Updater) update(oldObject, newObject typed.TypedValue, version fieldpat
9090
}
9191
}
9292

93+
if _, ok := managers[workflow]; !ok {
94+
managers[workflow] = &fieldpath.VersionedSet{
95+
Set: fieldpath.NewSet(),
96+
}
97+
}
98+
9399
return managers, nil
94100
}
95101

@@ -108,11 +114,6 @@ func (s *Updater) Update(liveObject, newObject typed.TypedValue, version fieldpa
108114
if err != nil {
109115
return fieldpath.ManagedFields{}, fmt.Errorf("failed to compare live and new objects: %v", err)
110116
}
111-
if _, ok := managers[manager]; !ok {
112-
managers[manager] = &fieldpath.VersionedSet{
113-
Set: fieldpath.NewSet(),
114-
}
115-
}
116117
managers[manager].Set = managers[manager].Set.Union(compare.Modified).Union(compare.Added).Difference(compare.Removed)
117118
managers[manager].APIVersion = version
118119
if managers[manager].Set.Empty() {
@@ -133,9 +134,10 @@ func (s *Updater) Apply(liveObject, configObject typed.TypedValue, version field
133134
if err != nil {
134135
return nil, fieldpath.ManagedFields{}, err
135136
}
136-
137-
// TODO: Remove unconflicting removed fields
138-
137+
newObject, err = s.removeDisownedItems(newObject, configObject, managers[manager])
138+
if err != nil {
139+
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to remove fields: %v", err)
140+
}
139141
set, err := configObject.ToFieldSet()
140142
if err != nil {
141143
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
@@ -149,3 +151,15 @@ func (s *Updater) Apply(liveObject, configObject typed.TypedValue, version field
149151
}
150152
return newObject, managers, nil
151153
}
154+
155+
func (s *Updater) removeDisownedItems(merged, applied typed.TypedValue, lastSet *fieldpath.VersionedSet) (typed.TypedValue, error) {
156+
convertedApplied, err := s.Converter.Convert(applied, lastSet.APIVersion)
157+
if err != nil {
158+
return nil, fmt.Errorf("failed to convert applied config to last applied version: %v", err)
159+
}
160+
appliedSet, err := convertedApplied.ToFieldSet()
161+
if err != nil {
162+
return nil, fmt.Errorf("failed to create field set from applied config in last applied version: %v", err)
163+
}
164+
return merged.RemoveItems(lastSet.Set.Difference(appliedSet)), nil
165+
}

typed/deduced.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ package typed
1919
import (
2020
"reflect"
2121

22-
"sigs.k8s.io/structured-merge-diff/value"
2322
"sigs.k8s.io/structured-merge-diff/fieldpath"
23+
"sigs.k8s.io/structured-merge-diff/value"
2424
)
2525

2626
// deducedTypedValue holds a value and guesses what it is and what to
@@ -130,7 +130,7 @@ func added(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
130130
} else if lhs.MapValue == nil && rhs.MapValue != nil {
131131
// From leaf to map, add leaf fields of map.
132132
fieldsetDeduced(rhs, path, set)
133-
} else if lhs.MapValue != nil && rhs.MapValue == nil {
133+
} else if lhs.MapValue != nil && rhs.MapValue == nil {
134134
// Went from map to field, add field.
135135
set.Insert(path)
136136
} else {
@@ -150,7 +150,7 @@ func added(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
150150
}
151151

152152
func modified(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
153-
if lhs.MapValue == nil && rhs.MapValue == nil {
153+
if lhs.MapValue == nil && rhs.MapValue == nil {
154154
if !reflect.DeepEqual(lhs, rhs) {
155155
set.Insert(path)
156156
}
@@ -170,3 +170,9 @@ func modified(lhs, rhs value.Value, path fieldpath.Path, set *fieldpath.Set) {
170170
}
171171
}
172172
}
173+
174+
// RemoveItems does nothing because all lists in a deducedTypedValue are considered atomic,
175+
// and there are no maps because it is indistinguishable from a struct.
176+
func (dv deducedTypedValue) RemoveItems(_ *fieldpath.Set) TypedValue {
177+
return dv
178+
}

typed/remove.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package typed
15+
16+
import (
17+
"sigs.k8s.io/structured-merge-diff/fieldpath"
18+
"sigs.k8s.io/structured-merge-diff/schema"
19+
"sigs.k8s.io/structured-merge-diff/value"
20+
)
21+
22+
type removingWalker struct {
23+
value *value.Value
24+
schema *schema.Schema
25+
toRemove *fieldpath.Set
26+
}
27+
28+
func removeItemsWithSchema(value *value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef) {
29+
w := &removingWalker{
30+
value: value,
31+
schema: schema,
32+
toRemove: toRemove,
33+
}
34+
resolveSchema(schema, typeRef, w)
35+
}
36+
37+
// doLeaf should be called on leaves before descending into children, if there
38+
// will be a descent. It modifies w.inLeaf.
39+
func (w *removingWalker) doLeaf() ValidationErrors { return nil }
40+
41+
func (w *removingWalker) doScalar(t schema.Scalar) ValidationErrors { return nil }
42+
43+
func (w *removingWalker) doStruct(t schema.Struct) ValidationErrors {
44+
s := w.value.MapValue
45+
46+
// If struct is null, empty, or atomic just return
47+
if s == nil || len(s.Items) == 0 || t.ElementRelationship == schema.Atomic {
48+
return nil
49+
}
50+
51+
fieldTypes := map[string]schema.TypeRef{}
52+
for _, structField := range t.Fields {
53+
fieldTypes[structField.Name] = structField.Type
54+
}
55+
56+
for i, _ := range s.Items {
57+
item := s.Items[i]
58+
pe := fieldpath.PathElement{FieldName: &item.Name}
59+
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
60+
removeItemsWithSchema(&s.Items[i].Value, subset, w.schema, fieldTypes[item.Name])
61+
}
62+
}
63+
return nil
64+
}
65+
66+
func (w *removingWalker) doList(t schema.List) (errs ValidationErrors) {
67+
l := w.value.ListValue
68+
69+
// If list is null, empty, or atomic just return
70+
if l == nil || len(l.Items) == 0 || t.ElementRelationship == schema.Atomic {
71+
return nil
72+
}
73+
74+
newItems := []value.Value{}
75+
for i, _ := range l.Items {
76+
item := l.Items[i]
77+
// Ignore error because we have already validated this list
78+
pe, _ := listItemToPathElement(t, i, item)
79+
path, _ := fieldpath.MakePath(pe)
80+
if w.toRemove.Has(path) {
81+
continue
82+
}
83+
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
84+
removeItemsWithSchema(&l.Items[i], subset, w.schema, t.ElementType)
85+
}
86+
newItems = append(newItems, l.Items[i])
87+
}
88+
l.Items = newItems
89+
return nil
90+
}
91+
92+
func (w *removingWalker) doMap(t schema.Map) ValidationErrors {
93+
m := w.value.MapValue
94+
95+
// If map is null, empty, or atomic just return
96+
if m == nil || len(m.Items) == 0 || t.ElementRelationship == schema.Atomic {
97+
return nil
98+
}
99+
100+
newMap := &value.Map{}
101+
for i, _ := range m.Items {
102+
item := m.Items[i]
103+
pe := fieldpath.PathElement{FieldName: &item.Name}
104+
path, _ := fieldpath.MakePath(pe)
105+
if w.toRemove.Has(path) {
106+
continue
107+
}
108+
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
109+
removeItemsWithSchema(&m.Items[i].Value, subset, w.schema, t.ElementType)
110+
}
111+
newMap.Set(item.Name, m.Items[i].Value)
112+
}
113+
w.value.MapValue = newMap
114+
return nil
115+
}
116+
117+
func (*removingWalker) doUntyped(_ schema.Untyped) ValidationErrors { return nil }
118+
119+
func (*removingWalker) errorf(_ string, _ ...interface{}) ValidationErrors { return nil }

typed/typed.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type TypedValue interface {
5353
// match), or an error will be returned. Validation errors will be returned if
5454
// the objects don't conform to the schema.
5555
Compare(rhs TypedValue) (c *Comparison, err error)
56+
// RemoveItems removes each provided list or map item from the value.
57+
RemoveItems(items *fieldpath.Set) TypedValue
5658
}
5759

5860
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
@@ -159,6 +161,12 @@ func (tv typedValue) Compare(rhs TypedValue) (c *Comparison, err error) {
159161
return c, nil
160162
}
161163

164+
// RemoveItems removes each provided list or map item from the value.
165+
func (tv typedValue) RemoveItems(items *fieldpath.Set) TypedValue {
166+
removeItemsWithSchema(&tv.value, items, tv.schema, tv.typeRef)
167+
return tv
168+
}
169+
162170
func merge(lhs, rhs typedValue, rule, postRule mergeRule) (TypedValue, error) {
163171
if lhs.schema != rhs.schema {
164172
return nil, errorFormatter{}.

0 commit comments

Comments
 (0)