Skip to content

Commit eb03fad

Browse files
committed
Add atomic<->granular schema change fixup
1 parent b84068c commit eb03fad

File tree

6 files changed

+731
-15
lines changed

6 files changed

+731
-15
lines changed

internal/fixture/state.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,33 @@ func (f UpdateObject) preprocess(parser Parser) (Operation, error) {
385385
return f, nil
386386
}
387387

388+
// ChangeParser is a type of operation. It simulates making changes a schema without versioning
389+
// the schema. This can be used to test the behavior of making backward compatible schema changes,
390+
// e.g. setting "elementRelationship: atomic" on an existing struct. It also may be used to ensure
391+
// that backward incompatible changes are detected appropriately.
392+
type ChangeParser struct {
393+
Parser *typed.Parser
394+
}
395+
396+
var _ Operation = &ChangeParser{}
397+
398+
func (cs ChangeParser) run(state *State) error {
399+
state.Parser = cs.Parser
400+
// Swap the schema in for use with the live object so it merges.
401+
// If the schema is incompatible, this will fail validation.
402+
403+
liveWithNewSchema, err := typed.AsTyped(state.Live.AsValue(), &cs.Parser.Schema, state.Live.TypeRef())
404+
if err != nil {
405+
return err
406+
}
407+
state.Live = liveWithNewSchema
408+
return nil
409+
}
410+
411+
func (cs ChangeParser) preprocess(_ Parser) (Operation, error) {
412+
return cs, nil
413+
}
414+
388415
// TestCase is the list of operations that need to be run, as well as
389416
// the object/managedfields as they are supposed to look like after all
390417
// the operations have been successfully performed. If Object/Managed is

merge/schema_change_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 merge_test
18+
19+
import (
20+
"testing"
21+
22+
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
23+
. "sigs.k8s.io/structured-merge-diff/v4/internal/fixture"
24+
"sigs.k8s.io/structured-merge-diff/v4/merge"
25+
"sigs.k8s.io/structured-merge-diff/v4/typed"
26+
)
27+
28+
29+
var structParser = func() *typed.Parser {
30+
oldParser, err := typed.NewParser(`types:
31+
- name: v1
32+
map:
33+
fields:
34+
- name: struct
35+
type:
36+
namedType: struct
37+
- name: struct
38+
map:
39+
fields:
40+
- name: numeric
41+
type:
42+
scalar: numeric
43+
- name: string
44+
type:
45+
scalar: string`)
46+
if err != nil {
47+
panic(err)
48+
}
49+
return oldParser
50+
}()
51+
52+
var structWithAtomicParser = func() *typed.Parser {
53+
newParser, err := typed.NewParser( `types:
54+
- name: v1
55+
map:
56+
fields:
57+
- name: struct
58+
type:
59+
namedType: struct
60+
- name: struct
61+
map:
62+
fields:
63+
- name: numeric
64+
type:
65+
scalar: numeric
66+
- name: string
67+
type:
68+
scalar: string
69+
elementRelationship: atomic`)
70+
if err != nil {
71+
panic(err)
72+
}
73+
return newParser
74+
}()
75+
76+
func TestSchemaChanges(t *testing.T) {
77+
tests := map[string]TestCase{
78+
"change-struct-to-atomic": {
79+
Ops: []Operation{
80+
Apply{
81+
Manager: "one",
82+
Object: `
83+
struct:
84+
numeric: 1
85+
`,
86+
APIVersion: "v1",
87+
},
88+
ChangeParser{Parser: structWithAtomicParser},
89+
Apply{
90+
Manager: "two",
91+
Object: `
92+
struct:
93+
string: "string"
94+
`,
95+
APIVersion: "v1",
96+
Conflicts:merge.Conflicts{
97+
merge.Conflict{Manager: "one", Path: _P("struct")},
98+
},
99+
},
100+
ForceApply{
101+
Manager: "two",
102+
Object: `
103+
struct:
104+
string: "string"
105+
`,
106+
APIVersion: "v1",
107+
},
108+
},
109+
Object: `
110+
struct:
111+
string: "string"
112+
`,
113+
APIVersion: "v1",
114+
Managed: fieldpath.ManagedFields{
115+
"two": fieldpath.NewVersionedSet(_NS(
116+
_P("struct"),
117+
), "v1", true),
118+
},
119+
},
120+
}
121+
122+
for name, test := range tests {
123+
t.Run(name, func(t *testing.T) {
124+
if err := test.Test(structParser); err != nil {
125+
t.Fatal(err)
126+
}
127+
})
128+
}
129+
}

merge/update.go

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,16 @@ 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 {
128132
return nil, fieldpath.ManagedFields{}, err
129133
}
130134
}
131-
managers = shallowCopyManagers(managers)
132135
managers, compare, err := s.update(liveObject, newObject, version, managers, manager, true)
133136
if err != nil {
134137
return nil, fieldpath.ManagedFields{}, err
@@ -141,24 +144,63 @@ func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldp
141144
if ignored == nil {
142145
ignored = fieldpath.NewSet()
143146
}
144-
managers[manager] = fieldpath.NewVersionedSet(
145-
managers[manager].Set().Union(compare.Modified).Union(compare.Added).Difference(compare.Removed).RecursiveDifference(ignored),
146-
version,
147-
false,
148-
)
147+
managers[manager] = fieldpath.NewVersionedSet(managers[manager].Set().Union(compare.Modified).Union(compare.Added).Difference(compare.Removed).RecursiveDifference(ignored), version, false)
149148
if managers[manager].Set().Empty() {
150149
delete(managers, manager)
151150
}
152151
return newObject, managers, nil
153152
}
154153

154+
// fixupManagedFields attempts to fix up the managed fields to be compatible with the live state of
155+
// the schema.
156+
// This is needed when an "unversioned" change are made to a schema that impact the managedFields.
157+
// By "unversionsed" we mean that a specific version of a schema was directly modified (introduction
158+
// of new versions does not require fixup).
159+
//
160+
// Supported schema changes:
161+
// - Changing a type to "atomic": All field manager that managed any granular fields become managers
162+
// of the atomic to ensure a conflict if any of the fields they previously managed are changed.
163+
// Granular managed fields of the struct are removed.
164+
//
165+
// TODO: Changes to add support for:
166+
// - Change a type from "atomic" to "granular", "set" or "map". All field manager that managed the
167+
// atomic become managers of all child fields to ensure a conflict if any child field is changed.
168+
//
169+
// Potential future optimization: Write a SHA-256 hash to managedFields that can be compared
170+
// with the current hash of the live schemas to determine if any fixup is needed.
171+
func (s *Updater) fixupManagedFields(liveObject *typed.TypedValue, managers fieldpath.ManagedFields) (fieldpath.ManagedFields, error) {
172+
result := fieldpath.ManagedFields{}
173+
for manager, versionedSet := range managers {
174+
tv, err := s.Converter.Convert(liveObject, versionedSet.APIVersion())
175+
if s.Converter.IsMissingVersionError(err) { // okay to skip, obsolete versions will be deleted automatically anyway
176+
continue
177+
}
178+
if err != nil {
179+
return nil, err
180+
}
181+
fixedFieldSet, err := typed.FixupFieldManagers(versionedSet.Set(), tv)
182+
if err != nil {
183+
return nil, err
184+
}
185+
if fixedFieldSet != nil {
186+
result[manager] = fieldpath.NewVersionedSet(fixedFieldSet, versionedSet.APIVersion(), versionedSet.Applied())
187+
} else {
188+
result[manager] = versionedSet
189+
}
190+
}
191+
return result, nil
192+
}
193+
155194
// Apply should be called when Apply is run, given the current object as
156195
// well as the configuration that is applied. This will merge the object
157196
// and return it. If the object hasn't changed, nil is returned (the
158197
// managers can still have changed though).
159198
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)
161199
var err error
200+
managers, err = s.fixupManagedFields(liveObject, managers)
201+
if err != nil {
202+
return nil, fieldpath.ManagedFields{}, err
203+
}
162204
if s.enableUnions {
163205
configObject, err = configObject.NormalizeUnionsApply(configObject)
164206
if err != nil {
@@ -204,14 +246,6 @@ func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fiel
204246
return newObject, managers, nil
205247
}
206248

207-
func shallowCopyManagers(managers fieldpath.ManagedFields) fieldpath.ManagedFields {
208-
newManagers := fieldpath.ManagedFields{}
209-
for manager, set := range managers {
210-
newManagers[manager] = set
211-
}
212-
return newManagers
213-
}
214-
215249
// prune will remove a field, list or map item, iff:
216250
// * applyingManager applied it last time
217251
// * applyingManager didn't apply it this time

0 commit comments

Comments
 (0)