99 "context"
1010 "fmt"
1111
12+ "github.com/go-logr/logr"
1213 "github.com/rotisserie/eris"
1314 kerrors "k8s.io/apimachinery/pkg/util/errors"
1415
@@ -46,6 +47,7 @@ const RepairSkippingPropertiesStageID = "repairSkippingProperties"
4647// Address: present in (v1, v3); (skipping v2), repair required.
4748func RepairSkippingProperties (
4849 objectModelConfiguration * config.ObjectModelConfiguration ,
50+ log logr.Logger ,
4951) * Stage {
5052 stage := NewStage (
5153 RepairSkippingPropertiesStageID ,
@@ -58,7 +60,7 @@ func RepairSkippingProperties(
5860 graph = g
5961 }
6062
61- repairer := newSkippingPropertyRepairer (state .Definitions (), graph , objectModelConfiguration )
63+ repairer := newSkippingPropertyRepairer (state .Definitions (), graph , objectModelConfiguration , log )
6264
6365 // Add resources and objects to the graph
6466 for _ , def := range state .Definitions () {
@@ -94,6 +96,7 @@ type skippingPropertyRepairer struct {
9496 conversionGraph * storage.ConversionGraph // Graph of conversions between types
9597 comparer * structuralComparer // Helper used to compare types for structural equality
9698 objectModelConfiguration * config.ObjectModelConfiguration // Configuration, used to allow for type renames
99+ log logr.Logger // Logger to use for any messages
97100}
98101
99102// newSkippingPropertyRepairer creates a new graph for tracking chains of properties as they evolve through different
@@ -104,6 +107,7 @@ func newSkippingPropertyRepairer(
104107 definitions astmodel.TypeDefinitionSet ,
105108 conversionGraph * storage.ConversionGraph ,
106109 objectModelConfiguration * config.ObjectModelConfiguration ,
110+ log logr.Logger ,
107111) * skippingPropertyRepairer {
108112 return & skippingPropertyRepairer {
109113 links : make (map [astmodel.PropertyReference ]astmodel.PropertyReference ),
@@ -112,6 +116,7 @@ func newSkippingPropertyRepairer(
112116 conversionGraph : conversionGraph ,
113117 comparer : newStructuralComparer (definitions ),
114118 objectModelConfiguration : objectModelConfiguration ,
119+ log : log ,
115120 }
116121}
117122
@@ -154,18 +159,64 @@ func (repairer *skippingPropertyRepairer) AddProperty(
154159func (repairer * skippingPropertyRepairer ) RepairSkippedProperties () (astmodel.TypeDefinitionSet , error ) {
155160 result := make (astmodel.TypeDefinitionSet )
156161 chains := repairer .findChains ().AsSlice ()
162+
163+ // We start by calculating all the repairs that we need to make.
164+ // Sometimes we'll find a candidate repair and then find a better alternative later on.
157165 var errs []error
166+ allRepairs := make (astmodel.TypeAssociation )
158167 for _ , ref := range chains {
159- defs , err := repairer .repairChain (ref )
168+ repairs , err := repairer .createRepairs (ref )
160169 if err != nil {
161170 errs = append (errs , err )
162171 continue
163172 }
164173
174+ if repairs != nil {
175+ repairer .mergeRepairs (allRepairs , repairs )
176+ }
177+ }
178+
179+ // Now we have a complete list of all the repairs we need to make, we can create the new types
180+ for compatType , originalType := range allRepairs {
181+ // Create a copy of the original type, but with the compatType name
182+ originalDef , ok := repairer .definitions [originalType ]
183+ if ! ok {
184+ return nil , eris .Errorf ("original type %s not found" , originalType )
185+ }
186+
187+ // Find all the type definitions referenced by this definition (e.g. nested types)
188+ defs , err := astmodel .FindConnectedDefinitions (repairer .definitions , astmodel .MakeTypeDefinitionSetFromDefinitions (originalDef ))
189+ if err != nil {
190+ return nil , eris .Wrapf (
191+ err ,
192+ "failed to find connected definitions from %s" ,
193+ originalDef .Name ())
194+ }
195+
196+ // Rename all the types, and their references, to copy them into the compat package
197+ compatPackage := compatType .InternalPackageReference ()
198+ renamer := astmodel .NewRenamingVisitorFromLambda (
199+ func (name astmodel.InternalTypeName ) astmodel.InternalTypeName {
200+ result := name .WithPackageReference (compatPackage )
201+ if newName , ok := repairer .objectModelConfiguration .TypeNameInNextVersion .Lookup (name ); ok {
202+ result = result .WithName (newName )
203+ }
204+
205+ return result
206+ })
207+ newDefs , err := renamer .RenameAll (defs )
208+ if err != nil {
209+ return nil , eris .Wrapf (
210+ err ,
211+ "failed to rename connected definitions from %s into package %s" ,
212+ originalDef .Name (),
213+ compatPackage )
214+ }
215+
165216 // If the repair added any new types (mostly it won't), include them in the result.
166217 // Duplicates are allowed as long as they are structurally identical, as the same type
167218 // may be reached from multiple chains in a given API version.
168- err = result .AddTypesAllowDuplicates (defs )
219+ err = result .AddTypesAllowDuplicates (newDefs )
169220 if err != nil {
170221 errs = append (errs , err )
171222 continue
@@ -233,11 +284,12 @@ func (repairer *skippingPropertyRepairer) hasLinkFrom(ref astmodel.PropertyRefer
233284 return found
234285}
235286
236- // repairChain checks for a gap in the specified property chain.
237- // start is the first property reference in the chain
238- func (repairer * skippingPropertyRepairer ) repairChain (
287+ // createRepairs checks for a gap in the specified property chain and identifies which types need to be created to repair it.
288+ // start is the first property reference in the chain.
289+ // Returns a set of type associations, identifying the required type copies
290+ func (repairer * skippingPropertyRepairer ) createRepairs (
239291 start astmodel.PropertyReference ,
240- ) (astmodel.TypeDefinitionSet , error ) {
292+ ) (astmodel.TypeAssociation , error ) {
241293 lastObserved , firstMissing := repairer .findBreak (start , repairer .wasPropertyObserved )
242294 if firstMissing .IsEmpty () {
243295 // Property was never discontinued
@@ -255,9 +307,14 @@ func (repairer *skippingPropertyRepairer) repairChain(
255307 // reintroduced property intact.)
256308 typesSame := repairer .propertiesHaveStructurallyIdenticalType (lastObserved , reintroduced )
257309 if typesSame {
258- return repairer .repairChain (reintroduced )
310+ return repairer .createRepairs (reintroduced )
259311 }
260312
313+ repairer .log .V (2 ).Info (
314+ "Found skipping property with different shape, constructing compat types to repair" ,
315+ "lastObserved" , lastObserved .String (),
316+ "reintroduced" , reintroduced .String ())
317+
261318 // We've found a skipping property with a different shape. If it's a TypeName we need to repair it.
262319 // We do this by creating a new type that has the same shape as the missing property, injected just prior to
263320 // reintroduction.
@@ -272,41 +329,29 @@ func (repairer *skippingPropertyRepairer) repairChain(
272329 if ! ok {
273330 // If not a type name, defer to our existing property conversion logic
274331 // Continue checking the rest of the chain
275- return repairer .repairChain (reintroduced )
332+ return repairer .createRepairs (reintroduced )
276333 }
277334
278- // Find the type definition for when the object was last observed
279- def := repairer .definitions [tn ]
335+ // Construct the name of the compatibility type we need to create
336+ compatPkg := astmodel .MakeCompatPackageReference (lastMissing .DeclaringType ().InternalPackageReference ())
337+ compatType := tn .WithPackageReference (compatPkg )
280338
281- // Find all the type definitions referenced by this definition (e.g. nested types)
282- defs , err := astmodel .FindConnectedDefinitions (repairer .definitions , astmodel .MakeTypeDefinitionSetFromDefinitions (def ))
339+ result , err := repairer .createRepairs (reintroduced )
283340 if err != nil {
284341 return nil , eris .Wrapf (
285342 err ,
286- "failed to find connected definitions from %s" ,
287- tn )
343+ "failed to repair the rest of the chain after reintroduction at %s" ,
344+ reintroduced )
288345 }
289346
290- // Move all of these types into a compatibility package, respecting any renames specified in the configuration
291- compatPkg := astmodel .MakeCompatPackageReference (lastMissing .DeclaringType ().InternalPackageReference ())
292- renamer := astmodel .NewRenamingVisitorFromLambda (
293- func (name astmodel.InternalTypeName ) astmodel.InternalTypeName {
294- result := name .WithPackageReference (compatPkg )
295- if newName , ok := repairer .objectModelConfiguration .TypeNameInNextVersion .Lookup (name ); ok {
296- result = result .WithName (newName )
297- }
298-
299- return result
300- })
301- newDefs , err := renamer .RenameAll (defs )
302- if err != nil {
303- return nil , eris .Wrapf (
304- err ,
305- "failed to rename definitions from %s" ,
306- tn )
347+ if result == nil {
348+ result = make (astmodel.TypeAssociation )
307349 }
308350
309- return newDefs , nil
351+ // Record the new type we need to create
352+ result [compatType ] = tn
353+
354+ return result , nil
310355}
311356
312357// wasPropertyObserved returns true if the property reference has been observedProperties; false otherwise.
@@ -383,6 +428,37 @@ func (repairer *skippingPropertyRepairer) lookupPropertyType(ref astmodel.Proper
383428 return prop .PropertyType (), true
384429}
385430
431+ // mergeRepairs merges two sets of repairs together.
432+ // If a conflicting repair is found, the newer one is preferred.
433+ func (repairer * skippingPropertyRepairer ) mergeRepairs (
434+ into astmodel.TypeAssociation ,
435+ from astmodel.TypeAssociation ,
436+ ) {
437+ for source , proposedRepair := range from {
438+ existingRepair , found := into [source ]
439+ if ! found {
440+ // New repair, just add it
441+ into [source ] = proposedRepair
442+ continue
443+ }
444+
445+ // This might mean we change the compatibility type with a new release of ASO, but this is safe because
446+ // compatibility types like this are only ever used in a transient fashion.
447+ // Any time we store things using the compatibility type in a property bag, it's promptly removed again
448+ // as a part of the conversion process bewteeen an API version and the storage version.
449+ if astmodel .ComparePathAndVersion (
450+ proposedRepair .InternalPackageReference ().ImportPath (),
451+ existingRepair .InternalPackageReference ().ImportPath ()) > 0 {
452+ repairer .log .V (1 ).Info (
453+ "Conflicting repairs for compatibility type, using newer" ,
454+ "type" , source ,
455+ "using" , proposedRepair ,
456+ "discarding" , existingRepair )
457+ into [source ] = proposedRepair
458+ }
459+ }
460+ }
461+
386462type structuralComparer struct {
387463 definitions astmodel.TypeDefinitionSet
388464 equalityOverrides astmodel.EqualityOverrides
0 commit comments