Skip to content

Commit 7a661f6

Browse files
Resolve conflicting repairs for skipping properties (#4991)
* Split repairs into two phases * Update pipeline * Fix tests * Rename method * Fix bug identified by copilot
1 parent 8e36d3b commit 7a661f6

File tree

4 files changed

+119
-41
lines changed

4 files changed

+119
-41
lines changed

v2/tools/generator/internal/codegen/code_generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func createAllPipelineStages(
222222
pipeline.InjectOriginalVersionFunction(idFactory).UsedFor(pipeline.ARMTarget),
223223
pipeline.CreateStorageTypes().UsedFor(pipeline.ARMTarget),
224224
pipeline.CreateConversionGraph(configuration, astmodel.GeneratorVersion).UsedFor(pipeline.ARMTarget),
225-
pipeline.RepairSkippingProperties(configuration.ObjectModelConfiguration).UsedFor(pipeline.ARMTarget),
225+
pipeline.RepairSkippingProperties(configuration.ObjectModelConfiguration, log).UsedFor(pipeline.ARMTarget),
226226

227227
// Need to regenerate the conversion graph in case our repairs for Skipping properties changed things
228228
pipeline.CreateConversionGraph(configuration, astmodel.GeneratorVersion).UsedFor(pipeline.ARMTarget),

v2/tools/generator/internal/codegen/pipeline/repair_skipping_properties.go

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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.
4748
func 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(
154159
func (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+
386462
type structuralComparer struct {
387463
definitions astmodel.TypeDefinitionSet
388464
equalityOverrides astmodel.EqualityOverrides

v2/tools/generator/internal/codegen/pipeline/repair_skipping_properties_golden_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func TestGolden_RepairSkippingProperties_WhenPropertyTypesDiffer_GeneratesExpect
5757
// Act - run the Repairer stage
5858
finalState, err := RunTestPipeline(
5959
initialState,
60-
RepairSkippingProperties(cfg.ObjectModelConfiguration), // and then we get to run the stage we're testing
61-
CreateConversionGraph(cfg, "v"), // Then, RECREATE the conversion graph showing relationships
60+
RepairSkippingProperties(cfg.ObjectModelConfiguration, logr.Discard()), // and then we get to run the stage we're testing
61+
CreateConversionGraph(cfg, "v"), // Then, RECREATE the conversion graph showing relationships
6262
InjectPropertyAssignmentFunctions(cfg, idFactory, logr.Discard()),
6363
)
6464
g.Expect(err).To(BeNil())

v2/tools/generator/internal/codegen/pipeline/repair_skipping_properties_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010

1111
. "github.com/onsi/gomega"
1212

13+
"github.com/go-logr/logr"
14+
1315
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
1416
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/codegen/storage"
1517
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/config"
@@ -42,7 +44,7 @@ func TestSkippingPropertyRepairer_AddProperty_CreatesExpectedChain(t *testing.T)
4244
graph, err := builder.Build()
4345
g.Expect(err).NotTo(HaveOccurred())
4446

45-
repairer := newSkippingPropertyRepairer(defs, graph, cfg)
47+
repairer := newSkippingPropertyRepairer(defs, graph, cfg, logr.Discard())
4648
err = repairer.AddProperties(person2020.Name(), test.FullNameProperty)
4749
g.Expect(err).NotTo(HaveOccurred())
4850

@@ -76,7 +78,7 @@ func TestSkippingPropertyRepairer_findBreak_returnsExpectedResults(t *testing.T)
7678
defs := make(astmodel.TypeDefinitionSet)
7779
cfg := config.NewObjectModelConfiguration()
7880
graph, _ := storage.NewConversionGraphBuilder(cfg, "v").Build()
79-
repairer := newSkippingPropertyRepairer(defs, graph, cfg)
81+
repairer := newSkippingPropertyRepairer(defs, graph, cfg, logr.Discard())
8082

8183
repairer.addLink(alphaSeen, betaSeen)
8284
repairer.addLink(betaSeen, gammaSeen)
@@ -147,7 +149,7 @@ func Test_RepairSkippingProperties_WhenPropertyTypesIdentical_DoesNotChangeDefin
147149
// Act - run the Repairer stage
148150
finalState, err := RunTestPipeline(
149151
initialState,
150-
RepairSkippingProperties(cfg.ObjectModelConfiguration), // and then we get to run the stage we're testing
152+
RepairSkippingProperties(cfg.ObjectModelConfiguration, logr.Discard()), // and then we get to run the stage we're testing
151153
)
152154

153155
// Assert - we expect no error, and no new definitions
@@ -186,7 +188,7 @@ func Test_RepairSkippingProperties_WhenPropertyStructurlyIdentical_DoesNotChange
186188
// Act - run the Repairer stage
187189
finalState, err := RunTestPipeline(
188190
initialState,
189-
RepairSkippingProperties(cfg.ObjectModelConfiguration), // and then we get to run the stage we're testing
191+
RepairSkippingProperties(cfg.ObjectModelConfiguration, logr.Discard()), // and then we get to run the stage we're testing
190192
)
191193
g.Expect(err).To(BeNil())
192194

@@ -226,7 +228,7 @@ func Test_RepairSkippingProperties_WhenPropertyTypesDiffer_InjectsExpectedAdditi
226228
// Act - run the Repairer stage
227229
finalState, err := RunTestPipeline(
228230
initialState,
229-
RepairSkippingProperties(cfg.ObjectModelConfiguration), // and then we get to run the stage we're testing
231+
RepairSkippingProperties(cfg.ObjectModelConfiguration, logr.Discard()), // and then we get to run the stage we're testing
230232
)
231233

232234
// Assert - we expect no error, and one new definition

0 commit comments

Comments
 (0)