v2/tools/generator/internal/armconversion/shared.go (315 lines of code) (raw):

/* * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. */ package armconversion import ( "fmt" "strings" "sync" "github.com/dave/dst" "github.com/rotisserie/eris" kerrors "k8s.io/apimachinery/pkg/util/errors" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/astbuilder" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/functions" ) type conversionBuilder struct { receiverIdent string receiverTypeExpr dst.Expr codeGenerationContext *astmodel.CodeGenerationContext idFactory astmodel.IdentifierFactory typeKind TypeKind methodName string destinationType *astmodel.ObjectType destinationTypeName astmodel.InternalTypeName sourceType *astmodel.ObjectType sourceTypeName astmodel.InternalTypeName propertyConversionHandlers []propertyConversionHandler } type TypeKind int const ( TypeKindOrdinary TypeKind = iota TypeKindSpec TypeKindStatus ) // sourceTypeIdent returns a dst.Expr that refers to the source type. func (builder conversionBuilder) sourceTypeIdent() dst.Expr { // If the source type is in this package, return it without qualification sourceTypePkg := builder.sourceTypeName.InternalPackageReference() if sourceTypePkg.Equals(builder.codeGenerationContext.CurrentPackage()) { return dst.NewIdent(builder.sourceTypeName.Name()) } sourceTypeImport := builder.codeGenerationContext.MustGetImportedPackageName(sourceTypePkg) return astbuilder.QualifiedTypeName(sourceTypeImport, builder.sourceTypeName.Name()) } // destinationTypeIdent returns a dst.Expr that refers to the destination type. func (builder conversionBuilder) destinationTypeIdent() dst.Expr { // If the destination type is in this package, return it without qualification destinationTypePkg := builder.destinationTypeName.InternalPackageReference() if destinationTypePkg.Equals(builder.codeGenerationContext.CurrentPackage()) { return dst.NewIdent(builder.destinationTypeName.Name()) } destinationTypeImport := builder.codeGenerationContext.MustGetImportedPackageName(destinationTypePkg) return astbuilder.QualifiedTypeName(destinationTypeImport, builder.destinationTypeName.Name()) } // sourceTypeString returns a string that refers to the source type. func (builder conversionBuilder) sourceTypeString() string { // If the source type is in this package, return it without qualification sourceTypePkg := builder.sourceTypeName.InternalPackageReference() if sourceTypePkg.Equals(builder.codeGenerationContext.CurrentPackage()) { return builder.sourceTypeName.Name() } sourceTypeImport := builder.codeGenerationContext.MustGetImportedPackageName(sourceTypePkg) return fmt.Sprintf("%s.%s", sourceTypeImport, builder.sourceTypeName.Name()) } // destinationTypeString returns a string that refers to the destination type. // //nolint:unused func (builder conversionBuilder) destinationTypeString() string { // If the destination type is in this package, return it without qualification destinationTypePkg := builder.destinationTypeName.InternalPackageReference() if destinationTypePkg.Equals(builder.codeGenerationContext.CurrentPackage()) { return builder.destinationTypeName.Name() } destinationTypeImport := builder.codeGenerationContext.MustGetImportedPackageName(destinationTypePkg) return fmt.Sprintf("%s.%s", destinationTypeImport, builder.destinationTypeName.Name()) } func (builder conversionBuilder) propertyConversionHandler( toProp *astmodel.PropertyDefinition, fromType *astmodel.ObjectType, ) ([]dst.Stmt, error) { var err error for _, conversionHandler := range builder.propertyConversionHandlers { var conversion propertyConversionHandlerResult conversion, err = conversionHandler(toProp, fromType) if err != nil { break } if conversion.matched { return conversion.statements, nil } } var kubeDescription strings.Builder builder.destinationType.WriteDebugDescription(&kubeDescription, nil) var armDescription strings.Builder builder.sourceType.WriteDebugDescription(&armDescription, nil) message := fmt.Sprintf( "no property found for %q in method %s()\nFrom: %s\nTo: %s", toProp.PropertyName(), builder.methodName, kubeDescription.String(), armDescription.String()) if err != nil { return nil, eris.Wrap(err, message) } return nil, eris.New(message) } type propertyConversionHandler = func( toProp *astmodel.PropertyDefinition, fromType *astmodel.ObjectType, ) (propertyConversionHandlerResult, error) type propertyConversionHandlerResult struct { statements []dst.Stmt matched bool } // notHandled is a result to use when a handler declines to handle the requested conversion var notHandled = propertyConversionHandlerResult{ matched: false, } // handledWithNoOp is a result to use when a handler wants to return an empty set of statements for a conversion var handledWithNoOp = propertyConversionHandlerResult{ matched: true, } // handleWith is a result to use when a handler wants to return a set of statements for a conversion func handleWith(statements ...any) propertyConversionHandlerResult { return propertyConversionHandlerResult{ statements: astbuilder.Statements(statements...), matched: true, } } var ( once sync.Once azureNameProperty *astmodel.PropertyDefinition ) func initializeAzureName(idFactory astmodel.IdentifierFactory) { azureNameFieldDescription := "The name of the resource in Azure. This is often the same as" + " the name of the resource in Kubernetes but it doesn't have to be." azureNameProperty = astmodel.NewPropertyDefinition( idFactory.CreatePropertyName(astmodel.AzureNameProperty, astmodel.Exported), idFactory.CreateStringIdentifier(astmodel.AzureNameProperty, astmodel.NotExported), astmodel.StringType).WithDescription(azureNameFieldDescription) } // GetAzureNameProperty returns the special "AzureName" field func GetAzureNameProperty(idFactory astmodel.IdentifierFactory) *astmodel.PropertyDefinition { once.Do(func() { initializeAzureName(idFactory) }) return azureNameProperty } func getReceiverObjectType( codeGenerationContext *astmodel.CodeGenerationContext, receiver astmodel.InternalTypeName, ) *astmodel.ObjectType { // Determine the type we're operating on rt := codeGenerationContext.MustGetDefinition(receiver) receiverType, ok := rt.Type().(*astmodel.ObjectType) if !ok { // Don't expect to have any wrapper types left at this point panic(fmt.Sprintf("receiver for ARMConversionFunction is not of expected type. TypeName: %s, Type %T", receiver, rt.Type())) } return receiverType } func generateTypeConversionAssignments( fromType *astmodel.ObjectType, toType *astmodel.ObjectType, propertyHandler func(toProp *astmodel.PropertyDefinition, fromType *astmodel.ObjectType) ([]dst.Stmt, error), ) ([]dst.Stmt, error) { var result []dst.Stmt var errs []error for _, toField := range toType.Properties().AsSlice() { fieldConversionStmts, err := propertyHandler(toField, fromType) if err != nil { errs = append(errs, err) continue } if len(fieldConversionStmts) > 0 { result = append(result, &dst.EmptyStmt{ Decs: dst.EmptyStmtDecorations{ NodeDecs: dst.NodeDecs{ Before: dst.EmptyLine, End: []string{fmt.Sprintf("// Set property %q:", toField.PropertyName())}, }, }, }) result = append(result, fieldConversionStmts...) } else { result = append(result, &dst.EmptyStmt{ Decs: dst.EmptyStmtDecorations{ NodeDecs: dst.NodeDecs{ Before: dst.EmptyLine, End: []string{fmt.Sprintf("// no assignment for property %q", toField.PropertyName())}, }, }, }) } } return result, kerrors.NewAggregate(errs) } // NewARMConversionImplementation creates an interface implementation with the specified ARM conversion functions func NewARMConversionImplementation( armTypeName astmodel.InternalTypeName, armType *astmodel.ObjectType, kubeTypeName astmodel.InternalTypeName, idFactory astmodel.IdentifierFactory, typeKind TypeKind, ) *astmodel.InterfaceImplementation { var convertToARMFunc *ConvertToARMFunction if typeKind != TypeKindStatus { // status type should not have ConvertToARM convertToARMFunc = &ConvertToARMFunction{ ARMConversionFunction: ARMConversionFunction{ armTypeName: armTypeName, armType: armType, kubeTypeName: kubeTypeName, idFactory: idFactory, typeKind: typeKind, }, } } populateFromARMFunc := &PopulateFromARMFunction{ ARMConversionFunction: ARMConversionFunction{ armTypeName: armTypeName, armType: armType, kubeTypeName: kubeTypeName, idFactory: idFactory, typeKind: typeKind, }, } newEmptyARMValueFunc := functions.NewNewEmptyARMValueFunc(armTypeName, idFactory) if convertToARMFunc != nil { // can convert both to and from ARM = the ARMTransformer interface return astmodel.NewInterfaceImplementation( astmodel.MakeExternalTypeName(astmodel.GenRuntimeReference, "ARMTransformer"), newEmptyARMValueFunc, convertToARMFunc, populateFromARMFunc) } else { // can only convert in one direction with the FromARMConverter interface return astmodel.NewInterfaceImplementation( astmodel.MakeExternalTypeName(astmodel.GenRuntimeReference, "FromARMConverter"), newEmptyARMValueFunc, populateFromARMFunc) } } func removeEmptyStatements(stmts []dst.Stmt) []dst.Stmt { result := make([]dst.Stmt, 0, len(stmts)) for _, stmt := range stmts { if _, ok := stmt.(*dst.EmptyStmt); ok { continue } result = append(result, stmt) } return result } const ( ConversionTag = "conversion" NoARMConversionValue = "noarmconversion" PushToOneOfLeaf = "pushtoleaf" ) func skipPropertiesFlaggedWithNoARMConversion( toProp *astmodel.PropertyDefinition, _ *astmodel.ObjectType, ) (propertyConversionHandlerResult, error) { // If the property has been flagged as not being convertible, skip it if toProp.HasTagValue(ConversionTag, NoARMConversionValue) { return handledWithNoOp, nil } return notHandled, nil } // propertyPair is a struct that holds a pair of properties - one from the CRD and one from the ARM type, // used to track pairs of properties for promotion/demotion. type propertyPair struct { crdProperty string armProperty string } // findPromotions creates a map of promotions - properties from nested ARM types that need to be // promoted to the container API object. Only properties that are properly tagged are candidates, // and only if they exist on the referenced type. func (builder conversionBuilder) findPromotions( crdType *astmodel.ObjectType, armType *astmodel.ObjectType, ) map[string][]propertyPair { // Find all properties that are tagged for promotion, promotable := astmodel.NewPropertySet() for _, prop := range armType.Properties().AsSlice() { if prop.HasTagValue(ConversionTag, PushToOneOfLeaf) { promotable.Add(prop) } } // If there are no properties to promote, early return if promotable.Len() == 0 { // No properties to promote, early return return nil } // Map those properties to the CRD type propMap := make(map[astmodel.PropertyName]astmodel.PropertyName, len(promotable)) crdProperties := crdType.Properties() for _, prop := range armType.Properties().AsSlice() { if crdProperties.ContainsProperty(prop.PropertyName()) { propMap[prop.PropertyName()] = prop.PropertyName() continue } // Special case for the AzureName property if prop.HasName(astmodel.NameProperty) && crdProperties.ContainsProperty(astmodel.AzureNameProperty) { propMap[astmodel.NameProperty] = astmodel.AzureNameProperty } } // Find all source properties that represent leaves from which promotion might occur // and build a set of the promotable properties for each defs := builder.codeGenerationContext.GetAllReachableDefinitions() result := make(map[string][]propertyPair, len(promotable)) armProperties := armType.Properties() for _, prop := range armProperties.AsSlice() { // Check whether the property is a leaf tn, leaf, ok := builder.asLeaf(prop, defs) if !ok { continue } promotableFromLeaf := armProperties.Intersect(leaf.Properties()) if promotableFromLeaf.IsEmpty() { continue } pairs := make([]propertyPair, 0, len(promotableFromLeaf)) for _, p := range promotableFromLeaf.AsSlice() { pair := propertyPair{ crdProperty: string(propMap[p.PropertyName()]), armProperty: string(p.PropertyName()), } pairs = append(pairs, pair) } result[tn.Name()] = pairs } return result } // asLeaf determines if a property is a OneOf leaf property, returning the related leaf type if so. // This requires the property to be an InternalTypeName that refers to an object type. func (builder conversionBuilder) asLeaf( prop *astmodel.PropertyDefinition, defs astmodel.TypeDefinitionSet, ) (astmodel.InternalTypeName, *astmodel.ObjectType, bool) { if prop.HasTagValue(ConversionTag, PushToOneOfLeaf) { // Tagged for promotion itself, so not a leaf return astmodel.InternalTypeName{}, nil, false } tn, ok := astmodel.AsInternalTypeName(prop.PropertyType()) if !ok { return astmodel.InternalTypeName{}, nil, false } def, ok := defs[tn] if !ok { return astmodel.InternalTypeName{}, nil, false } obj, ok := astmodel.AsObjectType(def.Type()) if !ok { return astmodel.InternalTypeName{}, nil, false } return def.Name(), obj, true }