Skip to content

Commit e3fbe42

Browse files
authored
Merge pull request #204 from zqzten/retry_add_back_owned
Fix potential conversion failure in addBackOwnedItems
2 parents 0f3c884 + 41d6b2c commit e3fbe42

File tree

2 files changed

+172
-20
lines changed

2 files changed

+172
-20
lines changed

merge/multiple_appliers_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,3 +1814,134 @@ func BenchmarkMultipleApplierRecursiveRealConversion(b *testing.B) {
18141814
}
18151815
}
18161816
}
1817+
1818+
var multiversionWithReliantFieldsParser = func() Parser {
1819+
parser, err := typed.NewParser(`types:
1820+
- name: v1
1821+
map:
1822+
fields:
1823+
- name: field_foo_rely_on_bar
1824+
type:
1825+
scalar: string
1826+
- name: common_field
1827+
type:
1828+
scalar: string
1829+
- name: v2
1830+
map:
1831+
fields:
1832+
- name: required_field_bar
1833+
type:
1834+
scalar: string
1835+
- name: common_field
1836+
type:
1837+
scalar: string
1838+
`)
1839+
if err != nil {
1840+
panic(err)
1841+
}
1842+
return parser
1843+
}()
1844+
1845+
// reliantFieldsConverter converts v2 obj to v1 relying on the required_field_bar,
1846+
// if required_field_bar is empty, the conversion shall fail.
1847+
// This converter can only be used with multiversionWithReliantFieldsParser.
1848+
type reliantFieldsConverter struct {
1849+
}
1850+
1851+
var _ merge.Converter = reliantFieldsConverter{}
1852+
1853+
func (r reliantFieldsConverter) Convert(v *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
1854+
inVersion := fieldpath.APIVersion(*v.TypeRef().NamedType)
1855+
if inVersion == version {
1856+
return v, nil
1857+
}
1858+
y, err := yaml.Marshal(v.AsValue().Unstructured())
1859+
if err != nil {
1860+
return nil, err
1861+
}
1862+
inStr := string(y)
1863+
var outStr string
1864+
switch version {
1865+
case "v1":
1866+
if !strings.Contains(inStr, "required_field_bar") {
1867+
return v, fmt.Errorf("missing requried field bar")
1868+
}
1869+
outStr = strings.Replace(inStr, "required_field_bar", "field_foo_rely_on_bar", -1)
1870+
case "v2":
1871+
outStr = strings.Replace(inStr, "field_foo_rely_on_bar", "required_field_bar", -1)
1872+
default:
1873+
return nil, missingVersionError
1874+
}
1875+
return multiversionWithReliantFieldsParser.Type(string(version)).FromYAML(typed.YAMLObject(outStr))
1876+
}
1877+
1878+
func (r reliantFieldsConverter) IsMissingVersionError(err error) bool {
1879+
return err == missingVersionError
1880+
}
1881+
1882+
func TestMultipleAppliersReliantFieldsConversions(t *testing.T) {
1883+
tests := map[string]TestCase{
1884+
"multiple_versions_with_reliant_fields": {
1885+
Ops: []Operation{
1886+
Apply{
1887+
Manager: "v2_applier",
1888+
APIVersion: "v2",
1889+
Object: typed.YAMLObject(`
1890+
required_field_bar: a
1891+
`),
1892+
},
1893+
Apply{
1894+
Manager: "v1_applier",
1895+
APIVersion: "v1",
1896+
Object: typed.YAMLObject(`
1897+
common_field: b
1898+
`),
1899+
},
1900+
Apply{
1901+
Manager: "v2_applier",
1902+
APIVersion: "v2",
1903+
Object: typed.YAMLObject(`
1904+
required_field_bar: b
1905+
`),
1906+
},
1907+
},
1908+
Object: typed.YAMLObject(`
1909+
required_field_bar: b
1910+
common_field: b
1911+
`),
1912+
APIVersion: "v2",
1913+
Managed: fieldpath.ManagedFields{
1914+
"v2_applier": fieldpath.NewVersionedSet(
1915+
_NS(
1916+
_P("required_field_bar"),
1917+
),
1918+
"v2",
1919+
true,
1920+
),
1921+
"v1_applier": fieldpath.NewVersionedSet(
1922+
_NS(
1923+
_P("common_field"),
1924+
),
1925+
"v1",
1926+
true,
1927+
),
1928+
},
1929+
},
1930+
}
1931+
1932+
converter := reliantFieldsConverter{}
1933+
for name, test := range tests {
1934+
t.Run(name, func(t *testing.T) {
1935+
runTimes := 1
1936+
if name == "multiple_versions_with_reliant_fields" {
1937+
// run this test for enough times to get as consistent results as possible
1938+
runTimes = 100
1939+
}
1940+
for i := 0; i < runTimes; i++ {
1941+
if err := test.TestWithConverter(multiversionWithReliantFieldsParser, converter); err != nil {
1942+
t.Fatal(err)
1943+
}
1944+
}
1945+
})
1946+
}
1947+
}

merge/update.go

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -250,33 +250,54 @@ func (s *Updater) addBackOwnedItems(merged, pruned *typed.TypedValue, managedFie
250250
}
251251
managedAtVersion[managerSet.APIVersion()] = managedAtVersion[managerSet.APIVersion()].Union(managerSet.Set())
252252
}
253-
for version, managed := range managedAtVersion {
254-
merged, err = s.Converter.Convert(merged, version)
253+
// Add back owned items at pruned version first to avoid conversion failure
254+
// caused by pruned fields which are required for conversion.
255+
prunedVersion := fieldpath.APIVersion(*pruned.TypeRef().NamedType)
256+
if managed, ok := managedAtVersion[prunedVersion]; ok {
257+
merged, pruned, err = s.addBackOwnedItemsForVersion(merged, pruned, prunedVersion, managed)
255258
if err != nil {
256-
if s.Converter.IsMissingVersionError(err) {
257-
continue
258-
}
259-
return nil, fmt.Errorf("failed to convert merged object at version %v: %v", version, err)
259+
return nil, err
260260
}
261-
pruned, err = s.Converter.Convert(pruned, version)
261+
delete(managedAtVersion, prunedVersion)
262+
}
263+
for version, managed := range managedAtVersion {
264+
merged, pruned, err = s.addBackOwnedItemsForVersion(merged, pruned, version, managed)
262265
if err != nil {
263-
if s.Converter.IsMissingVersionError(err) {
264-
continue
265-
}
266-
return nil, fmt.Errorf("failed to convert pruned object at version %v: %v", version, err)
266+
return nil, err
267267
}
268-
mergedSet, err := merged.ToFieldSet()
269-
if err != nil {
270-
return nil, fmt.Errorf("failed to create field set from merged object at version %v: %v", version, err)
268+
}
269+
return pruned, nil
270+
}
271+
272+
// addBackOwnedItemsForVersion adds back any fields, list and map items that were removed by prune with specific managed field path at a version.
273+
// It is an extracted sub-function from addBackOwnedItems for code reuse.
274+
func (s *Updater) addBackOwnedItemsForVersion(merged, pruned *typed.TypedValue, version fieldpath.APIVersion, managed *fieldpath.Set) (*typed.TypedValue, *typed.TypedValue, error) {
275+
var err error
276+
merged, err = s.Converter.Convert(merged, version)
277+
if err != nil {
278+
if s.Converter.IsMissingVersionError(err) {
279+
return merged, pruned, nil
271280
}
272-
prunedSet, err := pruned.ToFieldSet()
273-
if err != nil {
274-
return nil, fmt.Errorf("failed to create field set from pruned object at version %v: %v", version, err)
281+
return nil, nil, fmt.Errorf("failed to convert merged object at version %v: %v", version, err)
282+
}
283+
pruned, err = s.Converter.Convert(pruned, version)
284+
if err != nil {
285+
if s.Converter.IsMissingVersionError(err) {
286+
return merged, pruned, nil
275287
}
276-
sc, tr := merged.Schema(), merged.TypeRef()
277-
pruned = merged.RemoveItems(mergedSet.EnsureNamedFieldsAreMembers(sc, tr).Difference(prunedSet.EnsureNamedFieldsAreMembers(sc, tr).Union(managed.EnsureNamedFieldsAreMembers(sc, tr))))
288+
return nil, nil, fmt.Errorf("failed to convert pruned object at version %v: %v", version, err)
278289
}
279-
return pruned, nil
290+
mergedSet, err := merged.ToFieldSet()
291+
if err != nil {
292+
return nil, nil, fmt.Errorf("failed to create field set from merged object at version %v: %v", version, err)
293+
}
294+
prunedSet, err := pruned.ToFieldSet()
295+
if err != nil {
296+
return nil, nil, fmt.Errorf("failed to create field set from pruned object at version %v: %v", version, err)
297+
}
298+
sc, tr := merged.Schema(), merged.TypeRef()
299+
pruned = merged.RemoveItems(mergedSet.EnsureNamedFieldsAreMembers(sc, tr).Difference(prunedSet.EnsureNamedFieldsAreMembers(sc, tr).Union(managed.EnsureNamedFieldsAreMembers(sc, tr))))
300+
return merged, pruned, nil
280301
}
281302

282303
// addBackDanglingItems makes sure that the fields list and map items removed by prune were

0 commit comments

Comments
 (0)