diff --git a/hack/generator/pkg/astbuilder/composite_literals.go b/hack/generator/pkg/astbuilder/composite_literals.go new file mode 100644 index 00000000000..fe9ceed8034 --- /dev/null +++ b/hack/generator/pkg/astbuilder/composite_literals.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package astbuilder + +import ( + "github.com/dave/dst" +) + +// CompositeLiteralDetails captures the information required to generate code for an inline struct initialization +type CompositeLiteralDetails struct { + structType dst.Expr + elts []dst.Expr +} + +// NewCompositeLiteralDetails creates a new instance for initialization of the specified struct +// structType is an expression to handle both structs from the current package and imported ones requiring qualification +func NewCompositeLiteralDetails(structType dst.Expr) *CompositeLiteralDetails { + return &CompositeLiteralDetails{ + structType: structType, + } +} + +// AddField adds initialization of another field +// Returns the receiver to allow method chaining when desired +func (details *CompositeLiteralDetails) AddField(name string, value dst.Expr) { + expr := &dst.KeyValueExpr{ + Key: dst.NewIdent(name), + Value: dst.Clone(value).(dst.Expr), + } + + expr.Decs.Before = dst.NewLine + expr.Decs.After = dst.NewLine + + details.elts = append(details.elts, expr) +} + +// Build constructs the actual dst.CompositeLit that's required +func (details CompositeLiteralDetails) Build() *dst.CompositeLit { + return &dst.CompositeLit{ + Type: details.structType, + Elts: details.elts, + } +} diff --git a/hack/generator/pkg/astmodel/errored_type.go b/hack/generator/pkg/astmodel/errored_type.go index 7bc49619080..44f70b81955 100644 --- a/hack/generator/pkg/astmodel/errored_type.go +++ b/hack/generator/pkg/astmodel/errored_type.go @@ -163,7 +163,11 @@ func (e *ErroredType) Unwrap() Type { // types is a dictionary for resolving named types func (e *ErroredType) WriteDebugDescription(builder *strings.Builder, types Types) { builder.WriteString("Error[") - e.inner.WriteDebugDescription(builder, types) + if e.inner != nil { + e.inner.WriteDebugDescription(builder, types) + } else { + builder.WriteString("") + } for _, e := range e.errors { builder.WriteString("|") diff --git a/hack/generator/pkg/astmodel/function_container.go b/hack/generator/pkg/astmodel/function_container.go index 47cdaf66550..01692f659a7 100644 --- a/hack/generator/pkg/astmodel/function_container.go +++ b/hack/generator/pkg/astmodel/function_container.go @@ -6,9 +6,25 @@ package astmodel // FunctionContainer is implemented by Types that contain functions -// Can't include the withers for modification until we have generics +// Provides readonly access as we need to use a TypeVisitor for modifications to preserve type wrapping type FunctionContainer interface { // Functions returns all the function implementations // A sorted slice is returned to preserve immutability and provide determinism Functions() []Function + + // HasFunctionWithName determines if this resource has a function with the given name + HasFunctionWithName(name string) bool +} + +// AsFunctionContainer converts a type into a function container +// Only use this readonly access as we must use a TypeVisitor for modifications to preserve type wrapping +func AsFunctionContainer(theType Type) (FunctionContainer, bool) { + switch t := theType.(type) { + case FunctionContainer: + return t, true + case MetaType: + return AsFunctionContainer(t.Unwrap()) + default: + return nil, false + } } diff --git a/hack/generator/pkg/astmodel/object_type.go b/hack/generator/pkg/astmodel/object_type.go index 1c7e5a98188..9bc14faeced 100644 --- a/hack/generator/pkg/astmodel/object_type.go +++ b/hack/generator/pkg/astmodel/object_type.go @@ -240,7 +240,10 @@ func (objectType *ObjectType) References() TypeNameSet { results.AddAll(property.PropertyType().References()) } - // Not collecting types from functions deliberately. + for _, fn := range objectType.functions { + results.AddAll(fn.References()) + } + return results } diff --git a/hack/generator/pkg/astmodel/property_container.go b/hack/generator/pkg/astmodel/property_container.go index beb54226967..b9be756d9f6 100644 --- a/hack/generator/pkg/astmodel/property_container.go +++ b/hack/generator/pkg/astmodel/property_container.go @@ -6,7 +6,7 @@ package astmodel // PropertyContainer is implemented by Types that contain properties -// Can't include the withers for modification until we have generics +// Provides readonly access as we need to use a TypeVisitor for modifications to preserve type wrapping type PropertyContainer interface { // Properties returns all the properties from this container // A sorted slice is returned to preserve immutability and provide determinism @@ -15,3 +15,16 @@ type PropertyContainer interface { // Property returns the property and true if the named property is found, nil and false otherwise Property(name PropertyName) (*PropertyDefinition, bool) } + +// AsPropertyContainer converts a type into a property container +// Only use this readonly access as we must use a TypeVisitor for modifications to preserve type wrapping +func AsPropertyContainer(theType Type) (PropertyContainer, bool) { + switch t := theType.(type) { + case PropertyContainer: + return t, true + case MetaType: + return AsPropertyContainer(t.Unwrap()) + default: + return nil, false + } +} diff --git a/hack/generator/pkg/astmodel/resource_type.go b/hack/generator/pkg/astmodel/resource_type.go index f2f8e6b9044..1760fbcec64 100644 --- a/hack/generator/pkg/astmodel/resource_type.go +++ b/hack/generator/pkg/astmodel/resource_type.go @@ -393,6 +393,12 @@ func (resource *ResourceType) Functions() []Function { return functions } +// HasFunctionWithName determines if this resource has a function with the given name +func (resource *ResourceType) HasFunctionWithName(name string) bool { + _, ok := resource.functions[name] + return ok +} + // References returns the types referenced by Status or Spec parts of the resource func (resource *ResourceType) References() TypeNameSet { spec := resource.spec.References() diff --git a/hack/generator/pkg/astmodel/std_references.go b/hack/generator/pkg/astmodel/std_references.go index 97a7f683c46..11fcf7aa54b 100644 --- a/hack/generator/pkg/astmodel/std_references.go +++ b/hack/generator/pkg/astmodel/std_references.go @@ -26,9 +26,13 @@ var ( APIExtensionsJSONReference = MakeExternalPackageReference("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/JSON") APIMachineryErrorsReference = MakeExternalPackageReference("k8s.io/apimachinery/pkg/util/errors") APIMachineryRuntimeReference = MakeExternalPackageReference("k8s.io/apimachinery/pkg/runtime") - ClientGoSchemeReference = MakeExternalPackageReference("k8s.io/client-go/kubernetes/scheme") - ControllerRuntimeAdmission = MakeExternalPackageReference("sigs.k8s.io/controller-runtime/pkg/webhook/admission") - GitHubErrorsReference = MakeExternalPackageReference("github.com/pkg/errors") + APIMachinerySchemaReference = MakeExternalPackageReference("k8s.io/apimachinery/pkg/runtime/schema") + + ClientGoSchemeReference = MakeExternalPackageReference("k8s.io/client-go/kubernetes/scheme") + ControllerRuntimeAdmission = MakeExternalPackageReference("sigs.k8s.io/controller-runtime/pkg/webhook/admission") + ControllerRuntimeConversion = MakeExternalPackageReference("sigs.k8s.io/controller-runtime/pkg/conversion") + ControllerSchemeReference = MakeExternalPackageReference("sigs.k8s.io/controller-runtime/pkg/scheme") + GitHubErrorsReference = MakeExternalPackageReference("github.com/pkg/errors") // References to libraries used for testing CmpReference = MakeExternalPackageReference("github.com/google/go-cmp/cmp") @@ -43,8 +47,17 @@ var ( // Imports with specified names GomegaImport = NewPackageImport(GomegaReference).WithName(".") - // Type names + // Type names - GenRuntime ResourceReferenceTypeName = MakeTypeName(GenRuntimeReference, "ResourceReference") KnownResourceReferenceTypeName = MakeTypeName(GenRuntimeReference, "KnownResourceReference") - JSONTypeName = MakeTypeName(APIExtensionsReference, "JSON") + ToARMConverterInterfaceType = MakeTypeName(GenRuntimeReference, "ToARMConverter") + + // Type names - API Machinery + GroupVersionKindTypeName = MakeTypeName(APIMachinerySchemaReference, "GroupVersionKind") + SchemeType = MakeTypeName(APIMachineryRuntimeReference, "Scheme") + JSONTypeName = MakeTypeName(APIExtensionsReference, "JSON") + + // Type names - Controller Runtime + ConvertibleInterface = MakeTypeName(ControllerRuntimeConversion, "Convertible") + HubInterface = MakeTypeName(ControllerRuntimeConversion, "Hub") ) diff --git a/hack/generator/pkg/astmodel/type_walker.go b/hack/generator/pkg/astmodel/type_walker.go index ded84bde501..da64134aa2f 100644 --- a/hack/generator/pkg/astmodel/type_walker.go +++ b/hack/generator/pkg/astmodel/type_walker.go @@ -74,6 +74,7 @@ func (t *TypeWalker) visitTypeName(this *TypeVisitor, it TypeName, ctx interface if err != nil { return nil, errors.Wrapf(err, "visitTypeName failed for name %q", it) } + it, ok := visitedTypeName.(TypeName) if !ok { panic(fmt.Sprintf("TypeWalker visitor visitTypeName must return a TypeName, instead returned %T", visitedTypeName)) diff --git a/hack/generator/pkg/astmodel/types.go b/hack/generator/pkg/astmodel/types.go index 17c22b9a286..8ecf50329d6 100644 --- a/hack/generator/pkg/astmodel/types.go +++ b/hack/generator/pkg/astmodel/types.go @@ -58,7 +58,7 @@ func (types Types) FullyResolve(t Type) (Type, error) { } // AddAll adds multiple definitions to the set, with the same safety check as Add() to panic if a duplicate is included -func (types Types) AddAll(otherDefinitions []TypeDefinition) { +func (types Types) AddAll(otherDefinitions ...TypeDefinition) { for _, t := range otherDefinitions { types.Add(t) } diff --git a/hack/generator/pkg/astmodel/types_test.go b/hack/generator/pkg/astmodel/types_test.go index a03335c31b8..6481085364f 100644 --- a/hack/generator/pkg/astmodel/types_test.go +++ b/hack/generator/pkg/astmodel/types_test.go @@ -50,9 +50,7 @@ func Test_TypesAddAll_GivenTypes_ModifiesSet(t *testing.T) { g := NewGomegaWithT(t) types := createTestTypes(alphaDefinition, betaDefinition) - otherTypes := []TypeDefinition{gammaDefinition, deltaDefinition} - - types.AddAll(otherTypes) + types.AddAll(gammaDefinition, deltaDefinition) g.Expect(types).To(ContainElements(gammaDefinition, deltaDefinition)) } @@ -61,9 +59,7 @@ func Test_TypesAddAll_GivenOverlappingTypes_Panics(t *testing.T) { g := NewGomegaWithT(t) types := createTestTypes(alphaDefinition, betaDefinition) - otherTypes := []TypeDefinition{betaDefinition, deltaDefinition} - - g.Expect(func() { types.AddAll(otherTypes) }).To(Panic()) + g.Expect(func() { types.AddAll(betaDefinition, deltaDefinition) }).To(Panic()) } /* diff --git a/hack/generator/pkg/codegen/code_generator.go b/hack/generator/pkg/codegen/code_generator.go index ca68a5f71d2..558626125aa 100644 --- a/hack/generator/pkg/codegen/code_generator.go +++ b/hack/generator/pkg/codegen/code_generator.go @@ -15,6 +15,7 @@ import ( "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/pipeline" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" "github.com/Azure/azure-service-operator/hack/generator/pkg/config" ) @@ -78,6 +79,10 @@ func NewCodeGeneratorFromConfig(configuration *config.Configuration, idFactory a } func createAllPipelineStages(idFactory astmodel.IdentifierFactory, configuration *config.Configuration) []pipeline.Stage { + + // graph keeps track of the conversions we need between different API & Storage versions + graph := storage.NewConversionGraph() + return []pipeline.Stage{ pipeline.LoadSchemaIntoTypes(idFactory, configuration, pipeline.DefaultSchemaLoader), @@ -151,7 +156,14 @@ func createAllPipelineStages(idFactory astmodel.IdentifierFactory, configuration pipeline.AddCrossplaneEmbeddedResourceSpec(idFactory).UsedFor(pipeline.CrossplaneTarget), pipeline.AddCrossplaneEmbeddedResourceStatus(idFactory).UsedFor(pipeline.CrossplaneTarget), - pipeline.CreateStorageTypes(idFactory).UsedFor(pipeline.ARMTarget), // TODO: For now only used for ARM + // Create Storage types + //TODO: For now only used for ARM + pipeline.InjectOriginalVersionFunction(idFactory).UsedFor(pipeline.ARMTarget), + pipeline.CreateStorageTypes(graph).UsedFor(pipeline.ARMTarget), + pipeline.InjectOriginalVersionProperty().UsedFor(pipeline.ARMTarget), + pipeline.InjectPropertyAssignmentFunctions(graph, idFactory).UsedFor(pipeline.ARMTarget), + pipeline.InjectOriginalGVKFunction(idFactory).UsedFor(pipeline.ARMTarget), + pipeline.SimplifyDefinitions(), pipeline.InjectJsonSerializationTests(idFactory).UsedFor(pipeline.ARMTarget), @@ -218,7 +230,11 @@ func (generator *CodeGenerator) verifyPipeline() error { } for _, postreq := range stage.Postrequisites() { - stagesExpected[postreq] = append(stagesExpected[postreq], stage.Id()) + if _, ok := stagesSeen[postreq]; ok { + errs = append(errs, errors.Errorf("postrequisite %q of stage %q satisfied too early", postreq, stage.Id())) + } else { + stagesExpected[postreq] = append(stagesExpected[postreq], stage.Id()) + } } stagesSeen[stage.Id()] = struct{}{} diff --git a/hack/generator/pkg/codegen/golden_files_test.go b/hack/generator/pkg/codegen/golden_files_test.go index feba1587be7..b39d18d63d0 100644 --- a/hack/generator/pkg/codegen/golden_files_test.go +++ b/hack/generator/pkg/codegen/golden_files_test.go @@ -145,7 +145,15 @@ func NewTestCodeGenerator(testName string, path string, t *testing.T, testConfig // TODO: This isn't as clean as would be liked -- should we remove panic from RemoveStages? switch genPipeline { case config.GenerationPipelineAzure: - codegen.RemoveStages("deleteGenerated", "rogueCheck", "createStorage", "reportTypesAndVersions") + codegen.RemoveStages( + "deleteGenerated", + "rogueCheck", + "createStorageTypes", + "injectOriginalGVKFunction", + "injectOriginalVersionFunction", + "injectOriginalVersionProperty", + "injectPropertyAssignmentFunctions", + "reportTypesAndVersions") if !testConfig.HasARMResources { codegen.RemoveStages("createArmTypes", "applyArmConversionInterface") // These stages treat the collection of types as a graph of types rooted by a resource type. diff --git a/hack/generator/pkg/codegen/pipeline/create_storage_types.go b/hack/generator/pkg/codegen/pipeline/create_storage_types.go index 96ba2a05db0..aca9533ce38 100644 --- a/hack/generator/pkg/codegen/pipeline/create_storage_types.go +++ b/hack/generator/pkg/codegen/pipeline/create_storage_types.go @@ -8,69 +8,56 @@ package pipeline import ( "context" - kerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/klog/v2" + "github.com/pkg/errors" "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" ) +const CreateStorageTypesStageId = "createStorageTypes" + // CreateStorageTypes returns a pipeline stage that creates dedicated storage types for each resource and nested object. // Storage versions are created for *all* API versions to allow users of older versions of the operator to easily // upgrade. This is of course a bit odd for the first release, but defining the approach from day one is useful. -func CreateStorageTypes(idFactory astmodel.IdentifierFactory) Stage { - return MakeStage( - "createStorage", +func CreateStorageTypes(conversionGraph *storage.ConversionGraph) Stage { + result := MakeStage( + CreateStorageTypesStageId, "Create storage versions of CRD types", func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { - // Create a factory for each group (aka service) and divvy up the types - factories := make(map[string]*storage.StorageTypeFactory) - for name, def := range types { + // Predicate to isolate both resources and complex objects + isPropertyContainer := func(def astmodel.TypeDefinition) bool { + _, ok := astmodel.AsPropertyContainer(def.Type()) + return ok + } - ref, ok := name.PackageReference.AsLocalPackage() - if !ok { - // Skip definitions from non-local packages - // (should never happen) - klog.Warningf("Skipping storage type generation for unexpected non-local package reference %q", name.PackageReference) - continue - } + // Predicate to filter out ARM types + isNotARMType := func(def astmodel.TypeDefinition) bool { + return !astmodel.ARMFlag.IsOn(def.Type()) + } - factory, ok := factories[ref.Group()] - if !ok { - klog.V(3).Infof("Creating storage factory for %s", ref.Group()) - factory = storage.NewStorageTypeFactory(ref.Group(), idFactory) - factories[ref.Group()] = factory - } + // Filter to the types we want to process + typesToConvert := types.Where(isPropertyContainer).Where(isNotARMType) - if astmodel.ARMFlag.IsOn(def.Type()) { - // skip ARM types as they don't need storage variants - continue - } + storageTypes := make(astmodel.Types) + typeConverter := storage.NewTypeConverter(types) - factory.Add(def) - } - - // Collect up all the results - result := make(astmodel.Types) - var errs []error - for _, factory := range factories { - t, err := factory.Types() + // Create storage variants + for name, def := range typesToConvert { + storageDef, err := typeConverter.ConvertDefinition(def) if err != nil { - errs = append(errs, err) - continue + return nil, errors.Wrapf(err, "creating storage variant of %q", name) } - result.AddTypes(t) + storageTypes.Add(storageDef) + conversionGraph.AddLink(name.PackageReference, storageDef.Name().PackageReference) } - err := kerrors.NewAggregate(errs) - if err != nil { - return nil, err - } - - unmodified := types.Except(result) - result.AddTypes(unmodified) + result := types.Copy() + result.AddTypes(storageTypes) return result, nil }) + + result.RequiresPrerequisiteStages(injectOriginalVersionFunctionStageId) + return result } diff --git a/hack/generator/pkg/codegen/pipeline/create_storage_types_test.go b/hack/generator/pkg/codegen/pipeline/create_storage_types_test.go new file mode 100644 index 00000000000..6bd50f3484e --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/create_storage_types_test.go @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestCreateStorageTypes(t *testing.T) { + g := NewGomegaWithT(t) + + // Test Resource V1 + + specV1 := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + statusV1 := test.CreateStatus(pkg2020, "Person") + resourceV1 := test.CreateResource(pkg2020, "Person", specV1, statusV1) + + // Test Resource V2 + + specV2 := test.CreateSpec( + pkg2021, + "Person", + fullNameProperty, + familyNameProperty, + knownAsProperty, + residentialAddress2021, + postalAddress2021) + statusV2 := test.CreateStatus(pkg2021, "Person") + resourceV2 := test.CreateResource(pkg2021, "Person", specV2, statusV2) + + types := make(astmodel.Types) + types.AddAll(resourceV1, specV1, statusV1, resourceV2, specV2, statusV2, address2021) + + // Run the stage + + graph := storage.NewConversionGraph() + createStorageTypes := CreateStorageTypes(graph) + + // Don't need a context when testing + finalTypes, err := createStorageTypes.Run(context.TODO(), types) + + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, finalTypes, t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/crossplane_add_for_provider.go b/hack/generator/pkg/codegen/pipeline/crossplane_add_for_provider.go index fc14dc501fb..067aeb8372f 100644 --- a/hack/generator/pkg/codegen/pipeline/crossplane_add_for_provider.go +++ b/hack/generator/pkg/codegen/pipeline/crossplane_add_for_provider.go @@ -32,7 +32,7 @@ func AddCrossplaneForProvider(idFactory astmodel.IdentifierFactory) Stage { return nil, errors.Wrapf(err, "creating 'ForProvider' types") } - result.AddAll(forProviderTypes) + result.AddAll(forProviderTypes...) } } diff --git a/hack/generator/pkg/codegen/pipeline/determine_resource_ownership.go b/hack/generator/pkg/codegen/pipeline/determine_resource_ownership.go index 5568d7bc09e..af9da624115 100644 --- a/hack/generator/pkg/codegen/pipeline/determine_resource_ownership.go +++ b/hack/generator/pkg/codegen/pipeline/determine_resource_ownership.go @@ -73,7 +73,7 @@ func determineOwnership(definitions astmodel.Types, configuration *config.Config setResourceGroupOwnerForResourcesWithNoOwner(configuration, definitions, updatedDefs) - return astmodel.TypesDisjointUnion(definitions.Except(updatedDefs), updatedDefs), nil + return definitions.OverlayWith(updatedDefs), nil } func resourceSpecTypeAsObject(resourceSpecDef astmodel.TypeDefinition) (*astmodel.ObjectType, error) { diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go new file mode 100644 index 00000000000..d8afe0b45e5 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/functions" +) + +// injectOriginalGVKFunctionId is the unique identifier for this pipeline stage +const injectOriginalGVKFunctionId = "injectOriginalGVKFunction" + +// InjectOriginalGVKFunction injects the function OriginalGVK() into each Resource type +// This function allows us to recover the original version used to create each custom resource, giving the operator the +// information needed to interact with ARM using the correct API version. +func InjectOriginalGVKFunction(idFactory astmodel.IdentifierFactory) Stage { + + stage := MakeStage( + injectOriginalGVKFunctionId, + "Inject the function OriginalGVK() into each Resource type", + func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { + injector := storage.NewFunctionInjector() + result := types.Copy() + + resources := storage.FindResourceTypes(types) + for name, def := range resources { + var fn *functions.OriginalGVKFunction + if astmodel.IsStoragePackageReference(name.PackageReference) { + fn = functions.NewOriginalGVKFunction(functions.ReadProperty, idFactory) + } else { + fn = functions.NewOriginalGVKFunction(functions.ReadFunction, idFactory) + } + + defWithFn, err := injector.Inject(def, fn) + if err != nil { + return nil, errors.Wrapf(err, "injecting OriginalGVK() into %s", name) + } + + result[defWithFn.Name()] = defWithFn + } + + return result, nil + }) + + stage.RequiresPrerequisiteStages(injectOriginalVersionFunctionStageId) + return stage +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function_test.go b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function_test.go new file mode 100644 index 00000000000..36b0beec63f --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function_test.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestInjectOriginalGVKFunction(t *testing.T) { + g := NewGomegaWithT(t) + + idFactory := astmodel.NewIdentifierFactory() + + // Define a test resource + spec := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(pkg2020, "Person") + resource := test.CreateResource(pkg2020, "Person", spec, status) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + injectOriginalVersion := InjectOriginalGVKFunction(idFactory) + + // Don't need a context when testing + finalTypes, err := injectOriginalVersion.Run(context.TODO(), types) + + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, finalTypes, t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go new file mode 100644 index 00000000000..fea37b6d2c4 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/functions" +) + +// injectOriginalVersionFunctionStageId is the unique identifier for this pipeline stage +const injectOriginalVersionFunctionStageId = "injectOriginalVersionFunction" + +// InjectOriginalVersionFunction injects the function OriginalVersion() into each Spec type +// This function allows us to recover the original version used to create each custom resource, giving the operator the +// information needed to interact with ARM using the correct API version. +// We run this stage before we create any storage types, ensuring only API versions get the function. +func InjectOriginalVersionFunction(idFactory astmodel.IdentifierFactory) Stage { + + stage := MakeStage( + injectOriginalVersionFunctionStageId, + "Inject the function OriginalVersion() into each Spec type", + func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { + injector := storage.NewFunctionInjector() + result := types.Copy() + + specs := storage.FindSpecTypes(types) + for name, def := range specs { + fn := functions.NewOriginalVersionFunction(idFactory) + defWithFn, err := injector.Inject(def, fn) + if err != nil { + return nil, errors.Wrapf(err, "injecting OriginalVersion() into %s", name) + } + + result[defWithFn.Name()] = defWithFn + } + + return result, nil + }) + + stage.RequiresPostrequisiteStages(CreateStorageTypesStageId, InjectOriginalVersionPropertyId) + return stage +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_function_test.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_function_test.go new file mode 100644 index 00000000000..ee95a417ec0 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_function_test.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestInjectOriginalVersionFunction(t *testing.T) { + g := NewGomegaWithT(t) + + idFactory := astmodel.NewIdentifierFactory() + + // Define a test resource + spec := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(pkg2020, "Person") + resource := test.CreateResource(pkg2020, "Person", spec, status) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + injectOriginalVersion := InjectOriginalVersionFunction(idFactory) + + // Don't need a context when testing + finalTypes, err := injectOriginalVersion.Run(context.TODO(), types) + + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, finalTypes, t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go new file mode 100644 index 00000000000..a7f4c4482cb --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go @@ -0,0 +1,61 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" +) + +// InjectOriginalVersionPropertyId is the unique identifier for this pipeline stage +const InjectOriginalVersionPropertyId = "injectOriginalVersionProperty" + +// InjectOriginalVersionProperty injects the property OriginalVersion into each Storage Spec type +// This property gets populated by reading from the OriginalVersion() function previously injected into the API Spec +// types, allowing us to recover the original version used to create each custom resource, and giving the operator the +// information needed to interact with ARM using the correct API version. +func InjectOriginalVersionProperty() Stage { + + stage := MakeStage( + InjectOriginalVersionPropertyId, + "Inject the property OriginalVersion into each Storage Spec type", + func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { + injector := storage.NewPropertyInjector() + result := types.Copy() + + doesNotHaveOriginalVersionFunction := func(definition astmodel.TypeDefinition) bool { + ot, ok := astmodel.AsObjectType(definition.Type()) + if !ok { + // Not an object + return false + } + + // Skip objects that have OriginalVersion() functions + return !ot.HasFunctionWithName("OriginalVersion") + } + + storageSpecs := storage.FindSpecTypes(types).Where(doesNotHaveOriginalVersionFunction) + + for name, def := range storageSpecs { + prop := astmodel.NewPropertyDefinition("OriginalVersion", "originalVersion", astmodel.StringType) + defWithProp, err := injector.Inject(def, prop) + if err != nil { + return nil, errors.Wrapf(err, "injecting OriginalVersion into %s", name) + } + + result[defWithProp.Name()] = defWithProp + } + + return result, nil + }) + + stage.RequiresPrerequisiteStages(injectOriginalVersionFunctionStageId) + return stage +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go new file mode 100644 index 00000000000..d005ab6c0eb --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go @@ -0,0 +1,66 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/functions" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestInjectOriginalVersionProperty_InjectsIntoSpec(t *testing.T) { + g := NewGomegaWithT(t) + + // Define a test resource + spec := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(pkg2020, "Person") + resource := test.CreateResource(pkg2020, "Person", spec, status) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + injectOriginalProperty := InjectOriginalVersionProperty() + + // Don't need a context when testing + finalTypes, err := injectOriginalProperty.Run(context.TODO(), types) + + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, finalTypes, t.Name()) +} + +func TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotInjectIntoSpec(t *testing.T) { + g := NewGomegaWithT(t) + + idFactory := astmodel.NewIdentifierFactory() + fnInjector := storage.NewFunctionInjector() + + // Define a test resource + spec := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(pkg2020, "Person") + resource := test.CreateResource(pkg2020, "Person", spec, status) + + spec, err := fnInjector.Inject(spec, functions.NewOriginalVersionFunction(idFactory)) + g.Expect(err).To(Succeed()) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + injectOriginalProperty := InjectOriginalVersionProperty() + + // Don't need a context when testing + finalTypes, err := injectOriginalProperty.Run(context.TODO(), types) + + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, finalTypes, t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go new file mode 100644 index 00000000000..b62fcf5a2bc --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go @@ -0,0 +1,127 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + + "github.com/pkg/errors" + "k8s.io/klog/v2" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/conversions" +) + +// injectPropertyAssignmentFunctionsStageId is the unique identifier for this pipeline stage +const injectPropertyAssignmentFunctionsStageId = "injectPropertyAssignmentFunctions" + +// InjectPropertyAssignmentFunctions injects property assignment functions AssignTo*() and AssignFrom*() into both +// resources and object types. These functions do the heavy lifting of the conversions between versions of each type and +// are the building blocks of the main CovertTo*() and ConvertFrom*() methods. +func InjectPropertyAssignmentFunctions(graph *storage.ConversionGraph, idFactory astmodel.IdentifierFactory) Stage { + + stage := MakeStage( + injectPropertyAssignmentFunctionsStageId, + "Inject property assignment functions AssignFrom() and AssignTo() into resources and objects", + func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { + + result := types.Copy() + factory := NewPropertyAssignmentFunctionsFactory(graph, idFactory, types) + + for name, def := range types { + _, ok := astmodel.AsFunctionContainer(def.Type()) + if !ok { + // just skip it - not a resource nor an object + klog.V(4).Infof("Skipping %s as no conversion functions needed", name) + continue + } + + klog.V(3).Infof("Injecting conversion functions into %s", name) + + // Find the definition we want to convert to/from + nextPackage, ok := graph.LookupTransition(name.PackageReference) + if !ok { + // No next package, so nothing to do + // (this is expected if we have the hub storage package) + continue + } + + nextName := astmodel.MakeTypeName(nextPackage, name.Name()) + nextDef, ok := types[nextName] + if !ok { + // No next type so nothing to do + // (this is expected if the type is discontinued or we're looking at the hub type) + continue + } + + modified, err := factory.injectBetween(def, nextDef) + if err != nil { + return nil, errors.Wrapf(err, "injecting property assignment functions into %s", name) + } + + result[modified.Name()] = modified + } + + return result, nil + }) + + // Needed to populate the conversion graph + stage.RequiresPrerequisiteStages(CreateStorageTypesStageId) + return stage +} + +type propertyAssignmentFunctionsFactory struct { + graph *storage.ConversionGraph + idFactory astmodel.IdentifierFactory + types astmodel.Types + functionInjector *storage.FunctionInjector +} + +func NewPropertyAssignmentFunctionsFactory( + graph *storage.ConversionGraph, + idFactory astmodel.IdentifierFactory, + types astmodel.Types) *propertyAssignmentFunctionsFactory { + return &propertyAssignmentFunctionsFactory{ + graph: graph, + idFactory: idFactory, + types: types, + functionInjector: storage.NewFunctionInjector(), + } +} + +// injectBetween injects conversion methods between the two specified definitions +// upstreamDef is the definition further away from our hub type in our directed conversion graph +// downstreamDef is the definition closer to our hub type in our directed conversion graph +func (f propertyAssignmentFunctionsFactory) injectBetween( + upstreamDef astmodel.TypeDefinition, downstreamDef astmodel.TypeDefinition) (astmodel.TypeDefinition, error) { + + // Create conversion functions + conversionContext := conversions.NewPropertyConversionContext(f.types, f.idFactory) + + assignFromFn, err := conversions.NewPropertyAssignmentFromFunction(upstreamDef, downstreamDef, f.idFactory, conversionContext) + upstreamName := upstreamDef.Name() + if err != nil { + return astmodel.TypeDefinition{}, errors.Wrapf(err, "creating AssignFrom() function for %q", upstreamName) + } + + assignToFn, err := conversions.NewPropertyAssignmentToFunction(upstreamDef, downstreamDef, f.idFactory, conversionContext) + if err != nil { + return astmodel.TypeDefinition{}, errors.Wrapf(err, "creating AssignTo() function for %q", upstreamName) + } + + updatedDefinition, err := f.functionInjector.Inject(upstreamDef, assignFromFn) + if err != nil { + return astmodel.TypeDefinition{}, errors.Wrapf(err, "failed to inject %s function into %q", assignFromFn.Name(), upstreamName) + } + + updatedDefinition, err = f.functionInjector.Inject(updatedDefinition, assignToFn) + if err != nil { + return astmodel.TypeDefinition{}, errors.Wrapf(err, "failed to inject %s function into %q", assignToFn.Name(), upstreamName) + } + + return updatedDefinition, nil +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions_test.go b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions_test.go new file mode 100644 index 00000000000..4224c45bbb2 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions_test.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestInjectPropertyAssignmentFunctions(t *testing.T) { + g := NewGomegaWithT(t) + + idFactory := astmodel.NewIdentifierFactory() + // Test Resource V1 + + specV1 := test.CreateSpec(pkg2020, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + statusV1 := test.CreateStatus(pkg2020, "Person") + resourceV1 := test.CreateResource(pkg2020, "Person", specV1, statusV1) + + // Test Resource V2 + + specV2 := test.CreateSpec( + pkg2021, + "Person", + fullNameProperty, + familyNameProperty, + knownAsProperty, + residentialAddress2021, + postalAddress2021) + statusV2 := test.CreateStatus(pkg2021, "Person") + resourceV2 := test.CreateResource(pkg2021, "Person", specV2, statusV2) + + types := make(astmodel.Types) + types.AddAll(resourceV1, specV1, statusV1, resourceV2, specV2, statusV2, address2021) + + graph := storage.NewConversionGraph() + + // Run CreateStorageTypes first to populate the conversion graph + createStorageTypes := CreateStorageTypes(graph) + types, err := createStorageTypes.Run(context.TODO(), types) + g.Expect(err).To(Succeed()) + + // Now run our stage + injectFunctions := InjectPropertyAssignmentFunctions(graph, idFactory) + types, err = injectFunctions.Run(context.TODO(), types) + g.Expect(err).To(Succeed()) + + test.AssertPackagesGenerateExpectedCode(t, types, t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/shared_for_test.go b/hack/generator/pkg/codegen/pipeline/shared_for_test.go new file mode 100644 index 00000000000..f0324411565 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/shared_for_test.go @@ -0,0 +1,53 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +/* + * Shared building blocks for testing + */ + +var ( + // Common group for testing + testGroup = "microsoft.person" + + // Reusable Properties - any package version + + fullNameProperty = astmodel.NewPropertyDefinition("FullName", "fullName", astmodel.StringType). + WithDescription("As would be used to address mail") + + familyNameProperty = astmodel.NewPropertyDefinition("FamilyName", "familyName", astmodel.StringType). + WithDescription("Shared name of the family") + + knownAsProperty = astmodel.NewPropertyDefinition("KnownAs", "knownAs", astmodel.StringType). + WithDescription("How the person is generally known") + + fullAddressProperty = astmodel.NewPropertyDefinition("FullAddress", "fullAddress", astmodel.StringType). + WithDescription("Full written address for map or postal use") + + cityProperty = astmodel.NewPropertyDefinition("City", "city", astmodel.StringType). + WithDescription("City or town (or nearest)") + + // Reference Package 2020 + pkg2020 = test.MakeLocalPackageReference(testGroup, "v20200101") + + // Reference Package 2021 + pkg2021 = test.MakeLocalPackageReference(testGroup, "v20211231") + + // Objects in pkg2021 + + address2021 = test.CreateObjectDefinition(pkg2021, "Address", fullAddressProperty, cityProperty) + + // Reusable Properties - only in pkg2021 + + residentialAddress2021 = astmodel.NewPropertyDefinition("ResidentialAddress", "residentialAddress", address2021.Name()) + + postalAddress2021 = astmodel.NewPropertyDefinition("PostalAddress", "postalAddress", address2021.Name()) +) diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101.golden new file mode 100644 index 00000000000..07ac0e28703 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101.golden @@ -0,0 +1,41 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +type Person_Status struct { + Status string `json:"status"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101storage.golden new file mode 100644 index 00000000000..b41aee24d7d --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20200101storage.golden @@ -0,0 +1,43 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101storage + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20200101storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20200101.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20200101storage.Person_Spec `json:"spec,omitempty"` + Status v20200101storage.Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +//Storage version of v20200101.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20200101storage.Person `json:"items"` +} + +//Storage version of v20200101.Person_Spec +type Person_Spec struct { + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` +} + +//Storage version of v20200101.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20200101storage.Person{}, &v20200101storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231.golden new file mode 100644 index 00000000000..edf2c62fe10 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231.golden @@ -0,0 +1,51 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` + PostalAddress Address `json:"postalAddress"` + ResidentialAddress Address `json:"residentialAddress"` +} + +type Person_Status struct { + Status string `json:"status"` +} + +type Address struct { + //City: City or town (or nearest) + City string `json:"city"` + + //FullAddress: Full written address for map or postal use + FullAddress string `json:"fullAddress"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231storage.golden new file mode 100644 index 00000000000..0db440d82cb --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestCreateStorageTypes-v20211231storage.golden @@ -0,0 +1,51 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231storage + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20211231.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20211231storage.Person_Spec `json:"spec,omitempty"` + Status v20211231storage.Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +//Storage version of v20211231.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20211231storage.Person `json:"items"` +} + +//Storage version of v20211231.Person_Spec +type Person_Spec struct { + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` + PostalAddress *v20211231storage.Address `json:"postalAddress,omitempty"` + ResidentialAddress *v20211231storage.Address `json:"residentialAddress,omitempty"` +} + +//Storage version of v20211231.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +//Storage version of v20211231.Address +type Address struct { + City *string `json:"city,omitempty"` + FullAddress *string `json:"fullAddress,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20211231storage.Person{}, &v20211231storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalGVKFunction-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalGVKFunction-v20200101.golden new file mode 100644 index 00000000000..de3ff885518 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalGVKFunction-v20200101.golden @@ -0,0 +1,53 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// OriginalGVK returns a GroupValueKind for the original API version used to create the resource +func (person *Person) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: person.Spec.OriginalVersion(), + Kind: "Person", + } +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +type Person_Status struct { + Status string `json:"status"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionFunction-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionFunction-v20200101.golden new file mode 100644 index 00000000000..898ba984ae8 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionFunction-v20200101.golden @@ -0,0 +1,45 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +func (personSpec *Person_Spec) OriginalVersion() string { + return GroupVersion.Version +} + +type Person_Status struct { + Status string `json:"status"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_InjectsIntoSpec-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_InjectsIntoSpec-v20200101.golden new file mode 100644 index 00000000000..5cefb8a611e --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_InjectsIntoSpec-v20200101.golden @@ -0,0 +1,42 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` + OriginalVersion string `json:"originalVersion"` +} + +type Person_Status struct { + Status string `json:"status"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotInjectIntoSpec-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotInjectIntoSpec-v20200101.golden new file mode 100644 index 00000000000..898ba984ae8 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotInjectIntoSpec-v20200101.golden @@ -0,0 +1,45 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +func (personSpec *Person_Spec) OriginalVersion() string { + return GroupVersion.Version +} + +type Person_Status struct { + Status string `json:"status"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101.golden new file mode 100644 index 00000000000..ca537d8e81c --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101.golden @@ -0,0 +1,163 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20200101storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20200101storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20200101storage.Person) error { + + // Spec + var spec v20200101storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20200101storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20200101storage.Person_Spec) error { + + // FamilyName + if source.FamilyName != nil { + personSpec.FamilyName = *source.FamilyName + } else { + personSpec.FamilyName = "" + } + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // KnownAs + if source.KnownAs != nil { + personSpec.KnownAs = *source.KnownAs + } else { + personSpec.KnownAs = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20200101storage.Person_Spec) error { + + // FamilyName + familyName := personSpec.FamilyName + destination.FamilyName = &familyName + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // KnownAs + knownA := personSpec.KnownAs + destination.KnownAs = &knownA + + // No error + return nil +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20200101storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20200101storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101storage.golden new file mode 100644 index 00000000000..b41aee24d7d --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20200101storage.golden @@ -0,0 +1,43 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101storage + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20200101storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20200101.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20200101storage.Person_Spec `json:"spec,omitempty"` + Status v20200101storage.Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +//Storage version of v20200101.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20200101storage.Person `json:"items"` +} + +//Storage version of v20200101.Person_Spec +type Person_Spec struct { + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` +} + +//Storage version of v20200101.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20200101storage.Person{}, &v20200101storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231.golden new file mode 100644 index 00000000000..8ab44f26073 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231.golden @@ -0,0 +1,249 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231 + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20211231storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20211231storage.Person) error { + + // Spec + var spec v20211231storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20211231storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` + PostalAddress Address `json:"postalAddress"` + ResidentialAddress Address `json:"residentialAddress"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20211231storage.Person_Spec) error { + + // FamilyName + if source.FamilyName != nil { + personSpec.FamilyName = *source.FamilyName + } else { + personSpec.FamilyName = "" + } + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // KnownAs + if source.KnownAs != nil { + personSpec.KnownAs = *source.KnownAs + } else { + personSpec.KnownAs = "" + } + + // PostalAddress + if source.PostalAddress != nil { + var postalAddress Address + err := postalAddress.AssignPropertiesFromAddress(source.PostalAddress) + if err != nil { + return errors.Wrap(err, "populating PostalAddress from PostalAddress, calling AssignPropertiesFromAddress()") + } + personSpec.PostalAddress = postalAddress + } else { + personSpec.PostalAddress = Address{} + } + + // ResidentialAddress + if source.ResidentialAddress != nil { + var residentialAddress Address + err := residentialAddress.AssignPropertiesFromAddress(source.ResidentialAddress) + if err != nil { + return errors.Wrap(err, "populating ResidentialAddress from ResidentialAddress, calling AssignPropertiesFromAddress()") + } + personSpec.ResidentialAddress = residentialAddress + } else { + personSpec.ResidentialAddress = Address{} + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20211231storage.Person_Spec) error { + + // FamilyName + familyName := personSpec.FamilyName + destination.FamilyName = &familyName + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // KnownAs + knownA := personSpec.KnownAs + destination.KnownAs = &knownA + + // PostalAddress + var postalAddress v20211231storage.Address + err := personSpec.PostalAddress.AssignPropertiesToAddress(&postalAddress) + if err != nil { + return errors.Wrap(err, "populating PostalAddress from PostalAddress, calling AssignPropertiesToAddress()") + } + destination.PostalAddress = &postalAddress + + // ResidentialAddress + var residentialAddress v20211231storage.Address + err = personSpec.ResidentialAddress.AssignPropertiesToAddress(&residentialAddress) + if err != nil { + return errors.Wrap(err, "populating ResidentialAddress from ResidentialAddress, calling AssignPropertiesToAddress()") + } + destination.ResidentialAddress = &residentialAddress + + // No error + return nil +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20211231storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20211231storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +type Address struct { + //City: City or town (or nearest) + City string `json:"city"` + + //FullAddress: Full written address for map or postal use + FullAddress string `json:"fullAddress"` +} + +// AssignPropertiesFromAddress populates our Address from the provided source Address +func (address *Address) AssignPropertiesFromAddress(source *v20211231storage.Address) error { + + // City + if source.City != nil { + address.City = *source.City + } else { + address.City = "" + } + + // FullAddress + if source.FullAddress != nil { + address.FullAddress = *source.FullAddress + } else { + address.FullAddress = "" + } + + // No error + return nil +} + +// AssignPropertiesToAddress populates the provided destination Address from our Address +func (address *Address) AssignPropertiesToAddress(destination *v20211231storage.Address) error { + + // City + city := address.City + destination.City = &city + + // FullAddress + fullAddress := address.FullAddress + destination.FullAddress = &fullAddress + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231storage.golden new file mode 100644 index 00000000000..0db440d82cb --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectPropertyAssignmentFunctions-v20211231storage.golden @@ -0,0 +1,51 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231storage + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20211231.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20211231storage.Person_Spec `json:"spec,omitempty"` + Status v20211231storage.Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +//Storage version of v20211231.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20211231storage.Person `json:"items"` +} + +//Storage version of v20211231.Person_Spec +type Person_Spec struct { + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` + PostalAddress *v20211231storage.Address `json:"postalAddress,omitempty"` + ResidentialAddress *v20211231storage.Address `json:"residentialAddress,omitempty"` +} + +//Storage version of v20211231.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +//Storage version of v20211231.Address +type Address struct { + City *string `json:"city,omitempty"` + FullAddress *string `json:"fullAddress,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20211231storage.Person{}, &v20211231storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/storage/conversion_graph.go b/hack/generator/pkg/codegen/storage/conversion_graph.go new file mode 100644 index 00000000000..6b633f4a25e --- /dev/null +++ b/hack/generator/pkg/codegen/storage/conversion_graph.go @@ -0,0 +1,56 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "fmt" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// ConversionGraph builds up a set of graphs of the required conversions between versions +// For each group (e.g. microsoft.storage or microsoft.batch) we have a separate subgraph of directed conversions +type ConversionGraph struct { + subgraphs map[string]*GroupConversionGraph +} + +// NewConversionGraph creates a new ConversionGraph +func NewConversionGraph() *ConversionGraph { + return &ConversionGraph{ + subgraphs: make(map[string]*GroupConversionGraph), + } +} + +// AddLink adds a directed link into our graph +func (graph *ConversionGraph) AddLink(source astmodel.PackageReference, destination astmodel.PackageReference) { + subgraph := graph.getSubGraph(source) + subgraph.AddLink(source, destination) +} + +// LookupTransition looks for a link and find out where it ends, given the starting reference. +// Returns the end and true if it's found, or nil and false if not. +func (graph *ConversionGraph) LookupTransition(start astmodel.PackageReference) (astmodel.PackageReference, bool) { + subgraph := graph.getSubGraph(start) + return subgraph.LookupTransition(start) +} + +// getSubGraph finds the relevant subgraph for the group of the provided reference, creating one if necessary +func (graph *ConversionGraph) getSubGraph(ref astmodel.PackageReference) *GroupConversionGraph { + // Expect to get either a local or a storage reference, not an external one + local, ok := ref.AsLocalPackage() + if !ok { + panic(fmt.Sprintf("cannot use external package reference %s with a conversion graph", ref)) + } + + group := local.Group() + subgraph, ok := graph.subgraphs[group] + if !ok { + subgraph = NewGroupConversionGraph(group) + graph.subgraphs[group] = subgraph + } + + return subgraph +} diff --git a/hack/generator/pkg/codegen/storage/conversion_graph_test.go b/hack/generator/pkg/codegen/storage/conversion_graph_test.go new file mode 100644 index 00000000000..9dd80a4f4b0 --- /dev/null +++ b/hack/generator/pkg/codegen/storage/conversion_graph_test.go @@ -0,0 +1,32 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestConversionGraph_GivenLink_ReturnsLink(t *testing.T) { + g := NewGomegaWithT(t) + + personV1 := test.MakeLocalPackageReference("person", "v1") + personV2 := test.MakeLocalPackageReference("person", "v2") + + placeV1 := test.MakeLocalPackageReference("place", "v1") + placeV2 := test.MakeLocalPackageReference("place", "v2") + + graph := NewConversionGraph() + graph.AddLink(personV1, personV2) + graph.AddLink(placeV1, placeV2) + + personActual, ok := graph.LookupTransition(personV1) + g.Expect(ok).To(BeTrue()) + g.Expect(personActual).To(Equal(personV2)) +} diff --git a/hack/generator/pkg/codegen/storage/group_conversion_graph.go b/hack/generator/pkg/codegen/storage/group_conversion_graph.go new file mode 100644 index 00000000000..65e542018d4 --- /dev/null +++ b/hack/generator/pkg/codegen/storage/group_conversion_graph.go @@ -0,0 +1,36 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// GroupConversionGraph represents the directed graph of conversions between versions for a single group +type GroupConversionGraph struct { + group string // Name of the group needing conversions + links map[astmodel.PackageReference]astmodel.PackageReference // All the directed links in our conversion graph +} + +// NewGroupConversionGraph creates a new, empty, GroupConversionGraph ready for use +func NewGroupConversionGraph(group string) *GroupConversionGraph { + return &GroupConversionGraph{ + group: group, + links: make(map[astmodel.PackageReference]astmodel.PackageReference), + } +} + +// AddLink adds a directed link into our graph +func (graph *GroupConversionGraph) AddLink(source astmodel.PackageReference, destination astmodel.PackageReference) { + graph.links[source] = destination +} + +// LookupTransition a link and find out where it ends, given the starting reference. +// Returns the end and true if it's found, or nil and false if not. +func (graph *GroupConversionGraph) LookupTransition(start astmodel.PackageReference) (astmodel.PackageReference, bool) { + end, found := graph.links[start] + return end, found +} diff --git a/hack/generator/pkg/codegen/storage/group_conversion_graph_test.go b/hack/generator/pkg/codegen/storage/group_conversion_graph_test.go new file mode 100644 index 00000000000..65f0f7d3bbb --- /dev/null +++ b/hack/generator/pkg/codegen/storage/group_conversion_graph_test.go @@ -0,0 +1,28 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func TestGroupConversionGraph_GivenLink_ReturnsLink(t *testing.T) { + g := NewGomegaWithT(t) + + start := test.MakeLocalPackageReference("demo", "v1") + finish := test.MakeLocalPackageReference("demo", "v2") + + graph := NewGroupConversionGraph("demo") + graph.AddLink(start, finish) + + actual, ok := graph.LookupTransition(start) + g.Expect(ok).To(BeTrue()) + g.Expect(actual).To(Equal(finish)) +} diff --git a/hack/generator/pkg/codegen/storage/property_converter.go b/hack/generator/pkg/codegen/storage/property_converter.go index 12e8904e296..4f3ccaf8dc1 100644 --- a/hack/generator/pkg/codegen/storage/property_converter.go +++ b/hack/generator/pkg/codegen/storage/property_converter.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" ) -// PropertyConverter is used to convert the properties of object inputTypes as required for storage variants +// PropertyConverter is used to convert the properties of object types as required for storage variants type PropertyConverter struct { // visitor is used to apply the modification visitor astmodel.TypeVisitor @@ -71,7 +71,7 @@ func (p *PropertyConverter) ConvertProperty(property *astmodel.PropertyDefinitio // stripAllValidations removes all validations func (p *PropertyConverter) stripAllValidations( this *astmodel.TypeVisitor, v *astmodel.ValidatedType, ctx interface{}) (astmodel.Type, error) { - // strip all type validations from storage property inputTypes + // strip all type validations from storage properties // act as if they do not exist return this.Visit(v.ElementType(), ctx) } diff --git a/hack/generator/pkg/codegen/storage/property_injector.go b/hack/generator/pkg/codegen/storage/property_injector.go new file mode 100644 index 00000000000..f933bc177d5 --- /dev/null +++ b/hack/generator/pkg/codegen/storage/property_injector.go @@ -0,0 +1,45 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + +// PropertyInjector is a utility for injecting property definitions into resources and objects +type PropertyInjector struct { + // visitor is used to do the actual injection + visitor astmodel.TypeVisitor +} + +// NewPropertyInjector creates a new property injector for modifying resources and objects +func NewPropertyInjector() *PropertyInjector { + result := &PropertyInjector{} + + result.visitor = astmodel.TypeVisitorBuilder{ + VisitObjectType: result.injectPropertyIntoObject, + VisitResourceType: result.injectPropertyIntoResource, + }.Build() + + return result +} + +// Inject modifies the passed type definition by injecting the passed property +func (pi *PropertyInjector) Inject(def astmodel.TypeDefinition, prop *astmodel.PropertyDefinition) (astmodel.TypeDefinition, error) { + return pi.visitor.VisitDefinition(def, prop) +} + +// injectPropertyIntoObject takes the property provided as a context and includes it on the provided object type +func (pi *PropertyInjector) injectPropertyIntoObject( + _ *astmodel.TypeVisitor, ot *astmodel.ObjectType, ctx interface{}) (astmodel.Type, error) { + prop := ctx.(*astmodel.PropertyDefinition) + return ot.WithProperty(prop), nil +} + +// injectPropertyIntoResource takes the property provided as a context and includes it on the provided resource type +func (pi *PropertyInjector) injectPropertyIntoResource( + _ *astmodel.TypeVisitor, rt *astmodel.ResourceType, ctx interface{}) (astmodel.Type, error) { + prop := ctx.(*astmodel.PropertyDefinition) + return rt.WithProperty(prop), nil +} diff --git a/hack/generator/pkg/codegen/storage/storage_type_factory.go b/hack/generator/pkg/codegen/storage/storage_type_factory.go deleted file mode 100644 index ebadd1e9ad8..00000000000 --- a/hack/generator/pkg/codegen/storage/storage_type_factory.go +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - */ - -package storage - -import ( - "github.com/pkg/errors" - "k8s.io/klog/v2" - - "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" - "github.com/Azure/azure-service-operator/hack/generator/pkg/conversions" -) - -// StorageTypeFactory is used to create storage inputTypes for a specific api group -type StorageTypeFactory struct { - group string // Name of the group we're handling (used mostly for logging) - inputTypes astmodel.Types // All the types for this group - outputTypes astmodel.Types // All the types created/modified by this factory - idFactory astmodel.IdentifierFactory // Factory for creating identifiers - typeConverter *TypeConverter // a utility type type visitor used to create storage variants - functionInjector *FunctionInjector // a utility used to inject functions into definitions - resourceHubMarker *HubVersionMarker // a utility used to mark resources as Storage Versions - conversionMap map[astmodel.PackageReference]astmodel.PackageReference // Map of conversion links for creating our conversion graph -} - -// NewStorageTypeFactory creates a new instance of StorageTypeFactory ready for use -func NewStorageTypeFactory(group string, idFactory astmodel.IdentifierFactory) *StorageTypeFactory { - - types := make(astmodel.Types) - - result := &StorageTypeFactory{ - group: group, - inputTypes: types, - idFactory: idFactory, - conversionMap: make(map[astmodel.PackageReference]astmodel.PackageReference), - functionInjector: NewFunctionInjector(), - resourceHubMarker: NewHubVersionMarker(), - typeConverter: NewTypeConverter(types), - } - - return result -} - -// Add the supplied type definition to this factory -func (f *StorageTypeFactory) Add(def astmodel.TypeDefinition) { - f.inputTypes.Add(def) -} - -// Types returns inputTypes contained by the factory, including all new storage variants and modified -// api inputTypes. If any errors occur during processing, they're returned here. -func (f *StorageTypeFactory) Types() (astmodel.Types, error) { - if len(f.outputTypes) == 0 { - err := f.process() - if err != nil { - return nil, err - } - } - - return f.outputTypes, nil -} - -func (f *StorageTypeFactory) process() error { - - f.outputTypes = make(astmodel.Types) - - // Create storage variants and stash them in outputTypes so injectConversions can find them - storageVariants, err := f.inputTypes.Process(f.createStorageVariant) - if err != nil { - return err - } - - f.outputTypes.AddTypes(storageVariants) - - // Inject conversions into our original types and stash them in outputTypes - inputTypesWithConversions, err := f.inputTypes.Process(f.injectConversions) - if err != nil { - return err - } - - f.outputTypes.AddTypes(inputTypesWithConversions) - - // Inject conversions into our storage variants and replace the definitions in outputTypes - storageVariantsWithConversions, err := storageVariants.Process(f.injectConversions) - if err != nil { - return err - } - - for _, d := range storageVariantsWithConversions { - f.outputTypes[d.Name()] = d - } - - // Add anything missing into outputTypes - f.outputTypes.AddTypes(f.inputTypes.Except(f.outputTypes)) - - return nil -} - -// createStorageVariant takes an existing object definition and creates a storage variant in a -// related package. -// def is the api definition on which to base the storage variant -// visitor is a type visitor that will do the creation -func (f *StorageTypeFactory) createStorageVariant(definition astmodel.TypeDefinition) (*astmodel.TypeDefinition, error) { - name := definition.Name() - _, isObject := astmodel.AsObjectType(definition.Type()) - _, isResource := astmodel.AsResourceType(definition.Type()) - if !isObject && !isResource { - // just skip it - klog.V(4).Infof("Skipping %s as no storage variant needed", name) - return nil, nil - } - - klog.V(3).Infof("Creating storage variant of %s", name) - - storageDef, err := f.typeConverter.ConvertDefinition(definition) - if err != nil { - return nil, errors.Wrapf(err, "creating storage variant for %q", name) - } - - // Add API-Package -> Storage-Package link into the conversion map - f.conversionMap[name.PackageReference] = storageDef.Name().PackageReference - - return &storageDef, nil -} - -// injectConversions modifies the named type by injecting the required conversion methods using -// the conversionMap we've previously established -func (f *StorageTypeFactory) injectConversions(definition astmodel.TypeDefinition) (*astmodel.TypeDefinition, error) { - name := definition.Name() - _, isObject := astmodel.AsObjectType(definition.Type()) - _, isResource := astmodel.AsResourceType(definition.Type()) - if !isObject && !isResource { - // just skip it - klog.V(4).Infof("Skipping %s as no conversion functions needed", name) - return nil, nil - } - - klog.V(3).Infof("Injecting conversion functions into %s", name) - - // Find the definition we want to convert to/from - nextPackage, ok := f.conversionMap[name.PackageReference] - if !ok { - // No next package, so nothing to do - // (this is expected if we have the hub storage package) - // Flag the type as needing to be flagged as the storage version - //TODO: Restore this - currently disabled until we get all the conversion functions injected - //hubDefintion, err := f.resourceHubMarker.markResourceAsStorageVersion(definition) - //if err != nil { - // return nil, errors.Wrapf(err, "marking %q as hub version", name) - //} - //return &hubDefintion, nil - return nil, nil - } - - nextName := astmodel.MakeTypeName(nextPackage, name.Name()) - nextDef, ok := f.outputTypes[nextName] - if !ok { - // No next type so nothing to do - // (this is expected if the type is discontinued or we're looking at the hub type) - return nil, nil - } - - // Create conversion functions - knownTypes := make(astmodel.Types) - knownTypes.AddTypes(f.inputTypes) - knownTypes.AddTypes(f.outputTypes) - conversionContext := conversions.NewPropertyConversionContext(knownTypes, f.idFactory) - - convertFromFn, err := conversions.NewPropertyAssignmentFromFunction(definition, nextDef, f.idFactory, conversionContext) - if err != nil { - return nil, errors.Wrapf(err, "creating ConvertFrom() function for %q", name) - } - - convertToFn, err := conversions.NewPropertyAssignmentToFunction(definition, nextDef, f.idFactory, conversionContext) - if err != nil { - return nil, errors.Wrapf(err, "creating ConvertTo() function for %q", name) - } - - updatedDefinition, err := f.functionInjector.Inject(definition, convertFromFn) - if err != nil { - return nil, errors.Wrapf(err, "failed to inject %s function into %q", convertFromFn.Name(), name) - } - - updatedDefinition, err = f.functionInjector.Inject(updatedDefinition, convertToFn) - if err != nil { - return nil, errors.Wrapf(err, "failed to inject %s function into %q", convertToFn.Name(), name) - } - - return &updatedDefinition, nil -} diff --git a/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101.golden b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101.golden new file mode 100644 index 00000000000..c7b533e6a68 --- /dev/null +++ b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101.golden @@ -0,0 +1,170 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + "github.com/Azure/azure-service-operator/hack/generated/microsoft.person/v20200101storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20200101storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20200101storage.Person) error { + + // Spec + var spec v20200101storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20200101storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20200101storage.Person_Spec) error { + + // FamilyName + if source.FamilyName != nil { + personSpec.FamilyName = *source.FamilyName + } else { + personSpec.FamilyName = "" + } + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // KnownAs + if source.KnownAs != nil { + personSpec.KnownAs = *source.KnownAs + } else { + personSpec.KnownAs = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20200101storage.Person_Spec) error { + + // FamilyName + familyName := personSpec.FamilyName + destination.FamilyName = &familyName + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // KnownAs + knownA := personSpec.KnownAs + destination.KnownAs = &knownA + + // OriginalVersion + destination.OriginalVersion = personSpec.OriginalVersion() + + // No error + return nil +} + +func (personSpec *Person_Spec) OriginalVersion() string { + return GroupVersion.Version +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20200101storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20200101storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101storage.golden b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101storage.golden new file mode 100644 index 00000000000..a52cf4621c6 --- /dev/null +++ b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20200101storage.golden @@ -0,0 +1,53 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101storage + +import ( + "github.com/Azure/azure-service-operator/hack/generated/microsoft.person/v20200101storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20200101.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20200101storage.Person_Spec `json:"spec,omitempty"` + Status v20200101storage.Person_Status `json:"status,omitempty"` +} + +func (person *v20200101storage.Person) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: person.Spec.OriginalVersion, + Kind: "Person", + } +} + +// +kubebuilder:object:root=true +//Storage version of v20200101.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20200101storage.Person `json:"items"` +} + +//Storage version of v20200101.Person_Spec +type Person_Spec struct { + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` + OriginalVersion string `json:"original-version"` +} + +//Storage version of v20200101.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20200101storage.Person{}, &v20200101storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231.golden b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231.golden new file mode 100644 index 00000000000..beedb9824bd --- /dev/null +++ b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231.golden @@ -0,0 +1,184 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231 + +import ( + "github.com/Azure/azure-service-operator/hack/generated/microsoft.person/v20211231storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20211231storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20211231storage.Person) error { + + // Spec + var spec v20211231storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20211231storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //CustomerProgram: Level of customer programme (Silver/Gold/Platinum/Diamond) + CustomerProgram string `json:"customerProgram"` + + //FamilyName: Shared name of the family + FamilyName string `json:"familyName"` + + //FullName: As would be used to address mail + FullName string `json:"fullName"` + + //KnownAs: How the person is generally known + KnownAs string `json:"knownAs"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20211231storage.Person_Spec) error { + + // CustomerProgram + if source.CustomerProgram != nil { + personSpec.CustomerProgram = *source.CustomerProgram + } else { + personSpec.CustomerProgram = "" + } + + // FamilyName + if source.FamilyName != nil { + personSpec.FamilyName = *source.FamilyName + } else { + personSpec.FamilyName = "" + } + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // KnownAs + if source.KnownAs != nil { + personSpec.KnownAs = *source.KnownAs + } else { + personSpec.KnownAs = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20211231storage.Person_Spec) error { + + // CustomerProgram + customerProgram := personSpec.CustomerProgram + destination.CustomerProgram = &customerProgram + + // FamilyName + familyName := personSpec.FamilyName + destination.FamilyName = &familyName + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // KnownAs + knownA := personSpec.KnownAs + destination.KnownAs = &knownA + + // OriginalVersion + destination.OriginalVersion = personSpec.OriginalVersion() + + // No error + return nil +} + +func (personSpec *Person_Spec) OriginalVersion() string { + return GroupVersion.Version +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20211231storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20211231storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231storage.golden b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231storage.golden new file mode 100644 index 00000000000..688648a185b --- /dev/null +++ b/hack/generator/pkg/codegen/storage/testdata/Test_StorageTypeFactory_GeneratesExpectedResults-v20211231storage.golden @@ -0,0 +1,54 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231storage + +import ( + "github.com/Azure/azure-service-operator/hack/generated/microsoft.person/v20211231storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20211231.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20211231storage.Person_Spec `json:"spec,omitempty"` + Status v20211231storage.Person_Status `json:"status,omitempty"` +} + +func (person *v20211231storage.Person) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: person.Spec.OriginalVersion, + Kind: "Person", + } +} + +// +kubebuilder:object:root=true +//Storage version of v20211231.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20211231storage.Person `json:"items"` +} + +//Storage version of v20211231.Person_Spec +type Person_Spec struct { + CustomerProgram *string `json:"customerProgram,omitempty"` + FamilyName *string `json:"familyName,omitempty"` + FullName *string `json:"fullName,omitempty"` + KnownAs *string `json:"knownAs,omitempty"` + OriginalVersion string `json:"original-version"` +} + +//Storage version of v20211231.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20211231storage.Person{}, &v20211231storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/storage/type_converter.go b/hack/generator/pkg/codegen/storage/type_converter.go index f4457419d35..5b61f289bd9 100644 --- a/hack/generator/pkg/codegen/storage/type_converter.go +++ b/hack/generator/pkg/codegen/storage/type_converter.go @@ -7,9 +7,11 @@ package storage import ( "fmt" - "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" ) // TypeConverter is used to create a storage variant of an API type @@ -44,7 +46,8 @@ func NewTypeConverter(types astmodel.Types) *TypeConverter { func (t *TypeConverter) ConvertDefinition(def astmodel.TypeDefinition) (astmodel.TypeDefinition, error) { result, err := t.visitor.VisitDefinition(def, nil) if err != nil { - return astmodel.TypeDefinition{}, errors.Wrapf(err, "converting %q for storage variant", def.Name()) + // Don't need to wrap for context because all our callers do that with better precision + return astmodel.TypeDefinition{}, err } description := t.descriptionForStorageVariant(def) @@ -64,10 +67,10 @@ func (t *TypeConverter) convertResourceType( ctx interface{}) (astmodel.Type, error) { // storage resource types do not need defaulter/validator interfaces, they have no webhooks - modifiedResource := resource.WithoutInterface(astmodel.DefaulterInterfaceName). + result := resource.WithoutInterface(astmodel.DefaulterInterfaceName). WithoutInterface(astmodel.ValidatorInterfaceName) - return astmodel.IdentityVisitOfResourceType(tv, modifiedResource, ctx) + return astmodel.IdentityVisitOfResourceType(tv, result, ctx) } // convertObjectType creates a storage variation of an object type @@ -91,6 +94,7 @@ func (t *TypeConverter) convertObjectType( } objectType := astmodel.NewObjectType().WithProperties(properties...) + return astmodel.StorageFlag.ApplyTo(objectType), nil } diff --git a/hack/generator/pkg/codegen/storage/types_tools.go b/hack/generator/pkg/codegen/storage/types_tools.go new file mode 100644 index 00000000000..869322a9fee --- /dev/null +++ b/hack/generator/pkg/codegen/storage/types_tools.go @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// FindResourceTypes walks the provided set of TypeDefinitions and returns all the resource types +func FindResourceTypes(types astmodel.Types) astmodel.Types { + result := make(astmodel.Types) + + // Find all our resources and extract all their Specs + for _, def := range types { + _, ok := astmodel.AsResourceType(def.Type()) + if !ok { + continue + } + + // We have a resource type + result.Add(def) + } + + return result +} + +// FindSpecTypes walks the provided set of TypeDefinitions and returns all the spec types +func FindSpecTypes(types astmodel.Types) astmodel.Types { + result := make(astmodel.Types) + + // Find all our resources and extract all their Specs + for _, def := range types { + rt, ok := astmodel.AsResourceType(def.Type()) + if !ok { + continue + } + + // We have a resource type + tn, ok := astmodel.AsTypeName(rt.SpecType()) + if !ok { + continue + } + + // Add the named spec type to our results + if spec, ok := types.TryGet(tn); ok { + result.Add(spec) + } + } + + return result +} + +// FindStatusTypes walks the provided set of TypeDefinitions and returns all the status types +func FindStatusTypes(types astmodel.Types) astmodel.Types { + result := make(astmodel.Types) + + // Find all our resources and extract all their Statuses + for _, def := range types { + rt, ok := astmodel.AsResourceType(def.Type()) + if !ok { + continue + } + + // We have a resource type + tn, ok := astmodel.AsTypeName(rt.StatusType()) + if !ok { + continue + } + + // Add the named status type to our results + if status, ok := types.TryGet(tn); ok { + result.Add(status) + } + } + + return result +} diff --git a/hack/generator/pkg/codegen/storage/types_tools_test.go b/hack/generator/pkg/codegen/storage/types_tools_test.go new file mode 100644 index 00000000000..00c78e30bcf --- /dev/null +++ b/hack/generator/pkg/codegen/storage/types_tools_test.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package storage + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +var ( + testGroup = "microsoft.person" + + testPackage = test.MakeLocalPackageReference(testGroup, "v20200101") + + fullNameProperty = astmodel.NewPropertyDefinition("FullName", "fullName", astmodel.StringType). + WithDescription("As would be used to address mail") + + familyNameProperty = astmodel.NewPropertyDefinition("FamilyName", "familyName", astmodel.StringType). + WithDescription("Shared name of the family") + + knownAsProperty = astmodel.NewPropertyDefinition("KnownAs", "knownAs", astmodel.StringType). + WithDescription("How the person is generally known") +) + +func TestFindSpecTypes(t *testing.T) { + g := NewGomegaWithT(t) + + // Define a test resource + spec := test.CreateSpec(testPackage, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(testPackage, "Person") + resource := test.CreateResource(testPackage, "Person", spec, status) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + specs := FindSpecTypes(types) + + g.Expect(specs).To(HaveLen(1)) + g.Expect(specs.Contains(spec.Name())).To(BeTrue()) +} + +func TestFindStatusTypes(t *testing.T) { + g := NewGomegaWithT(t) + + // Define a test resource + spec := test.CreateSpec(testPackage, "Person", fullNameProperty, familyNameProperty, knownAsProperty) + status := test.CreateStatus(testPackage, "Person") + resource := test.CreateResource(testPackage, "Person", spec, status) + + types := make(astmodel.Types) + types.AddAll(resource, status, spec) + + statuses := FindStatusTypes(types) + + g.Expect(statuses).To(HaveLen(1)) + g.Expect(statuses.Contains(status.Name())).To(BeTrue()) +} diff --git a/hack/generator/pkg/codegen/testdata/ARMCodeGeneratorPipeline.golden b/hack/generator/pkg/codegen/testdata/ARMCodeGeneratorPipeline.golden index 5b853aed2a1..8c67162bb07 100644 --- a/hack/generator/pkg/codegen/testdata/ARMCodeGeneratorPipeline.golden +++ b/hack/generator/pkg/codegen/testdata/ARMCodeGeneratorPipeline.golden @@ -29,7 +29,11 @@ addCrossplaneForProviderProperty crossplane Add a 'ForProvider' property on e addCrossplaneAtProviderProperty crossplane Add an 'AtProvider' property on every status addCrossplaneEmbeddedResourceSpec crossplane Add an embedded runtimev1alpha1.ResourceSpec to every spec type addCrossplaneEmbeddedResourceStatus crossplane Add an embedded runtimev1alpha1.ResourceStatus to every status type -createStorage azure Create storage versions of CRD types +injectOriginalVersionFunction azure Inject the function OriginalVersion() into each Spec type +createStorageTypes azure Create storage versions of CRD types +injectOriginalVersionProperty azure Inject the property OriginalVersion into each Storage Spec type +injectPropertyAssignmentFunctions azure Inject property assignment functions AssignFrom() and AssignTo() into resources and objects +injectOriginalGVKFunction azure Inject the function OriginalGVK() into each Resource type simplifyDefinitions Flatten definitions by removing wrapper types jsonTestCases azure Add test cases to verify JSON serialization markStorageVersion Mark the latest version of each resource as the storage version diff --git a/hack/generator/pkg/conversions/property_assignment_function.go b/hack/generator/pkg/conversions/property_assignment_function.go index 442c5ea426d..bc19aa8d731 100644 --- a/hack/generator/pkg/conversions/property_assignment_function.go +++ b/hack/generator/pkg/conversions/property_assignment_function.go @@ -312,34 +312,10 @@ func (fn *PropertyAssignmentFunction) createConversions(receiver astmodel.TypeDe return nil } -// asPropertyContainer converts a type into a property container -func (fn *PropertyAssignmentFunction) asPropertyContainer(theType astmodel.Type) (astmodel.PropertyContainer, bool) { - switch t := theType.(type) { - case astmodel.PropertyContainer: - return t, true - case astmodel.MetaType: - return fn.asPropertyContainer(t.Unwrap()) - default: - return nil, false - } -} - -// asFunctionContainer converts a type into a function container -func (fn *PropertyAssignmentFunction) asFunctionContainer(theType astmodel.Type) (astmodel.FunctionContainer, bool) { - switch t := theType.(type) { - case astmodel.FunctionContainer: - return t, true - case astmodel.MetaType: - return fn.asFunctionContainer(t.Unwrap()) - default: - return nil, false - } -} - func (fn *PropertyAssignmentFunction) createReadableEndpoints(instance astmodel.Type) map[string]ReadableConversionEndpoint { result := make(map[string]ReadableConversionEndpoint) - propContainer, ok := fn.asPropertyContainer(instance) + propContainer, ok := astmodel.AsPropertyContainer(instance) if ok { for _, prop := range propContainer.Properties() { endpoint := MakeReadableConversionEndpointForProperty(prop, fn.knownLocals) @@ -347,7 +323,7 @@ func (fn *PropertyAssignmentFunction) createReadableEndpoints(instance astmodel. } } - funcContainer, ok := fn.asFunctionContainer(instance) + funcContainer, ok := astmodel.AsFunctionContainer(instance) if ok { for _, f := range funcContainer.Functions() { valueFn, ok := f.(astmodel.ValueFunction) @@ -364,7 +340,7 @@ func (fn *PropertyAssignmentFunction) createReadableEndpoints(instance astmodel. func (fn *PropertyAssignmentFunction) createWritableEndpoints(instance astmodel.Type) map[string]WritableConversionEndpoint { result := make(map[string]WritableConversionEndpoint) - propContainer, ok := fn.asPropertyContainer(instance) + propContainer, ok := astmodel.AsPropertyContainer(instance) if ok { for _, prop := range propContainer.Properties() { endpoint := MakeWritableConversionEndpointForProperty(prop, fn.knownLocals) diff --git a/hack/generator/pkg/conversions/property_assignment_function_test.go b/hack/generator/pkg/conversions/property_assignment_function_test.go index 9e2d5684a7a..9975e85b00b 100644 --- a/hack/generator/pkg/conversions/property_assignment_function_test.go +++ b/hack/generator/pkg/conversions/property_assignment_function_test.go @@ -116,7 +116,7 @@ func CreatePropertyAssignmentFunctionTestCases() []*StorageConversionPropertyTes types := make(astmodel.Types) types.Add(currentDefinition) types.Add(hubDefinition) - types.AddAll(otherDefinitions) + types.AddAll(otherDefinitions...) return &StorageConversionPropertyTestCase{ name: name, diff --git a/hack/generator/pkg/functions/original_gvk_function.go b/hack/generator/pkg/functions/original_gvk_function.go new file mode 100644 index 00000000000..fd7c18b1187 --- /dev/null +++ b/hack/generator/pkg/functions/original_gvk_function.go @@ -0,0 +1,110 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "github.com/dave/dst" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astbuilder" + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// OriginalGVKFunction implements a function to return the GVK for the type on which it is called +// We put these on our resource types, giving us a way to obtain the right type of instance when the reconciler is +// working with ARM. The code differs slightly depending on whether we're injecting into an API or storage variant. +// +// func (resource *SomeResource) OriginalGVK() scheme.GroupVersionKind { +// return scheme.GroupVersionKind{ +// Group: GroupVersion.Group, +// Version: resource.Spec.OriginalVersion, +// Kind: "SomeResource", +// } +// } +// +type OriginalGVKFunction struct { + idFactory astmodel.IdentifierFactory + hasOriginalVersionFunction bool + hasOriginalVersionProperty bool +} + +// Ensure OriginalGVKFunction properly implements Function +var _ astmodel.Function = &OriginalGVKFunction{} + +type OriginalVersionKind string + +const ( + ReadProperty = OriginalVersionKind("property") + ReadFunction = OriginalVersionKind("function") +) + +// NewOriginalGVKFunction creates a new OriginalGVKFunction +func NewOriginalGVKFunction(originalVersion OriginalVersionKind, idFactory astmodel.IdentifierFactory) *OriginalGVKFunction { + result := &OriginalGVKFunction{ + idFactory: idFactory, + } + + result.hasOriginalVersionFunction = originalVersion == ReadFunction + result.hasOriginalVersionProperty = originalVersion == ReadProperty + + return result +} + +// Name returns the name of this function, which is always OriginalGVK() +func (o *OriginalGVKFunction) Name() string { + return "OriginalGVK" +} + +// RequiredPackageReferences returns the set of packages required by OriginalGVK() +func (o *OriginalGVKFunction) RequiredPackageReferences() *astmodel.PackageReferenceSet { + return astmodel.NewPackageReferenceSet(astmodel.APIMachinerySchemaReference) +} + +// References shows that OriginalGVK() references no other generated types +func (o *OriginalGVKFunction) References() astmodel.TypeNameSet { + return astmodel.NewTypeNameSet() +} + +// AsFunc returns the generated code for the OriginalGVK() function +func (o *OriginalGVKFunction) AsFunc( + generationContext *astmodel.CodeGenerationContext, receiver astmodel.TypeName) *dst.FuncDecl { + gvkType := astmodel.GroupVersionKindTypeName.AsType(generationContext) + groupVersionPackageGlobal := dst.NewIdent("GroupVersion") + + receiverName := o.idFactory.CreateIdentifier(receiver.Name(), astmodel.NotExported) + + spec := astbuilder.Selector(dst.NewIdent(receiverName), "Spec") + + builder := astbuilder.NewCompositeLiteralDetails(gvkType) + builder.AddField("Group", astbuilder.Selector(groupVersionPackageGlobal, "Group")) + + if o.hasOriginalVersionProperty { + builder.AddField("Version", astbuilder.Selector(spec, "OriginalVersion")) + } else if o.hasOriginalVersionFunction { + builder.AddField("Version", astbuilder.CallExpr(spec, "OriginalVersion")) + } + + builder.AddField("Kind", astbuilder.StringLiteral(receiver.Name())) + initGVK := builder.Build() + + funcDetails := &astbuilder.FuncDetails{ + ReceiverIdent: receiverName, + ReceiverType: astbuilder.Dereference(receiver.AsType(generationContext)), + Name: o.Name(), + Body: astbuilder.Statements(astbuilder.Returns(astbuilder.AddrOf(initGVK))), + } + + funcDetails.AddComments("returns a GroupValueKind for the original API version used to create the resource") + funcDetails.AddReturn(astbuilder.Dereference(gvkType)) + + return funcDetails.DefineFunc() +} + +// Equals returns true if the passed function is equal to us, or false otherwise +func (o *OriginalGVKFunction) Equals(f astmodel.Function) bool { + _, ok := f.(*OriginalGVKFunction) + // Equality is just based on Type for now + return ok +} diff --git a/hack/generator/pkg/functions/original_gvk_function_test.go b/hack/generator/pkg/functions/original_gvk_function_test.go new file mode 100644 index 00000000000..c4788425616 --- /dev/null +++ b/hack/generator/pkg/functions/original_gvk_function_test.go @@ -0,0 +1,53 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "testing" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func Test_OriginalGVKFunction_ReadingOriginalVersionFromProperty_GeneratesExpectedCode(t *testing.T) { + idFactory := astmodel.NewIdentifierFactory() + + testGroup := "microsoft.person" + testPackage := test.MakeLocalPackageReference(testGroup, "v20200101") + + fullNameProperty := astmodel.NewPropertyDefinition("FullName", "fullName", astmodel.StringType). + WithDescription("As would be used to address mail") + + originalGVKFunction := NewOriginalGVKFunction(ReadProperty, idFactory) + + // Define a test resource + spec := test.CreateSpec(testPackage, "Person", fullNameProperty) + status := test.CreateStatus(testPackage, "Person") + resource := test.CreateResource(testPackage, "Person", spec, status, originalGVKFunction) + + fileDef := test.CreateFileDefinition(resource) + test.AssertFileGeneratesExpectedCode(t, fileDef, t.Name()) +} + +func Test_OriginalGVKFunction_ReadingOriginalVersionFromFunction_GeneratesExpectedCode(t *testing.T) { + idFactory := astmodel.NewIdentifierFactory() + + testGroup := "microsoft.person" + testPackage := test.MakeLocalPackageReference(testGroup, "v20200101") + + fullNameProperty := astmodel.NewPropertyDefinition("FullName", "fullName", astmodel.StringType). + WithDescription("As would be used to address mail") + + originalGVKFunction := NewOriginalGVKFunction(ReadFunction, idFactory) + + // Define a test resource + spec := test.CreateSpec(testPackage, "Person", fullNameProperty) + status := test.CreateStatus(testPackage, "Person") + resource := test.CreateResource(testPackage, "Person", spec, status, originalGVKFunction) + + fileDef := test.CreateFileDefinition(resource) + test.AssertFileGeneratesExpectedCode(t, fileDef, t.Name()) +} diff --git a/hack/generator/pkg/functions/original_version_function.go b/hack/generator/pkg/functions/original_version_function.go new file mode 100644 index 00000000000..8f211a60e85 --- /dev/null +++ b/hack/generator/pkg/functions/original_version_function.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "github.com/dave/dst" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astbuilder" + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// OriginalVersionFunction implements a function to return the version for the type on which it is called +// We put these on the *API* versions of our spec types, giving us a way to obtain the right type of instance when the +// reconciler is working with ARM. +// +// func (spec *SomeSpec) OriginalVersion() string { +// return GroupVersion.Version +// } +// +type OriginalVersionFunction struct { + idFactory astmodel.IdentifierFactory +} + +// Ensure OriginalVersionFunction properly implements ValueFunction +var _ astmodel.ValueFunction = &OriginalVersionFunction{} + +// NewOriginalVersionFunction creates a new OriginalVersionFunction +func NewOriginalVersionFunction(idFactory astmodel.IdentifierFactory) *OriginalVersionFunction { + return &OriginalVersionFunction{ + idFactory: idFactory, + } +} + +// Name returns the name of this function, which is always OriginalVersion() +func (o *OriginalVersionFunction) Name() string { + return "OriginalVersion" +} + +// RequiredPackageReferences returns the set of packages required by OriginalVersion() +func (o *OriginalVersionFunction) RequiredPackageReferences() *astmodel.PackageReferenceSet { + return astmodel.NewPackageReferenceSet() +} + +// References shows that OriginalVersion() references no other generated types +func (o *OriginalVersionFunction) References() astmodel.TypeNameSet { + return astmodel.NewTypeNameSet() +} + +// AsFunc returns the generated code for the OriginalVersion() function +func (o *OriginalVersionFunction) AsFunc( + generationContext *astmodel.CodeGenerationContext, receiver astmodel.TypeName) *dst.FuncDecl { + groupVersionPackageGlobal := dst.NewIdent("GroupVersion") + + receiverName := o.idFactory.CreateIdentifier(receiver.Name(), astmodel.NotExported) + + returnVersion := astbuilder.Returns( + astbuilder.Selector(groupVersionPackageGlobal, "Version")) + + funcDetails := &astbuilder.FuncDetails{ + ReceiverIdent: receiverName, + ReceiverType: astbuilder.Dereference(receiver.AsType(generationContext)), + Name: o.Name(), + Body: astbuilder.Statements(returnVersion), + } + + funcDetails.AddReturn(dst.NewIdent("string")) + + return funcDetails.DefineFunc() +} + +// Equals returns true if the passed function is equal to us, or false otherwise +func (o *OriginalVersionFunction) Equals(f astmodel.Function) bool { + _, ok := f.(*OriginalVersionFunction) + // Equality is just based on Type for now + return ok +} + +// ReturnType indicates that this function returns a string +func (o *OriginalVersionFunction) ReturnType() astmodel.Type { + return astmodel.StringType +} diff --git a/hack/generator/pkg/functions/original_version_function_test.go b/hack/generator/pkg/functions/original_version_function_test.go new file mode 100644 index 00000000000..14e26121dcb --- /dev/null +++ b/hack/generator/pkg/functions/original_version_function_test.go @@ -0,0 +1,28 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "testing" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +func Test_OriginalVersionFunction_GeneratesExpectedCode(t *testing.T) { + idFactory := astmodel.NewIdentifierFactory() + + originalVersionFunction := NewOriginalVersionFunction(idFactory) + demoType := astmodel.NewObjectType().WithFunction(originalVersionFunction) + + demoPkg := astmodel.MakeLocalPackageReference("github.com/Azure/azure-service-operator/hack/generated/_apis", "Person", "vDemo") + demoName := astmodel.MakeTypeName(demoPkg, "Demo") + + demoDef := astmodel.MakeTypeDefinition(demoName, demoType) + + fileDef := test.CreateFileDefinition(demoDef) + test.AssertFileGeneratesExpectedCode(t, fileDef, t.Name()) +} diff --git a/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..b50460b7236 --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_GeneratesExpectedCode.golden @@ -0,0 +1,17 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package vDemo + +import "k8s.io/apimachinery/pkg/runtime/schema" + +type Demo struct { +} + +func (demo *Demo) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: demo.Spec.OriginalVersion, + Kind: "Demo", + } +} diff --git a/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromFunction_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromFunction_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..db823f2e5c1 --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromFunction_GeneratesExpectedCode.golden @@ -0,0 +1,38 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// OriginalGVK returns a GroupValueKind for the original API version used to create the resource +func (person *Person) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: person.Spec.OriginalVersion(), + Kind: "Person", + } +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromProperty_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromProperty_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..3abac2d40a5 --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_OriginalGVKFunction_ReadingOriginalVersionFromProperty_GeneratesExpectedCode.golden @@ -0,0 +1,38 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// OriginalGVK returns a GroupValueKind for the original API version used to create the resource +func (person *Person) OriginalGVK() *schema.GroupVersionKind { + return &schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: person.Spec.OriginalVersion, + Kind: "Person", + } +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/functions/testdata/Test_OriginalVersionFunction_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_OriginalVersionFunction_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..e14bb31529c --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_OriginalVersionFunction_GeneratesExpectedCode.golden @@ -0,0 +1,11 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package vDemo + +type Demo struct { +} + +func (demo *Demo) OriginalVersion() string { + return GroupVersion.Version +} diff --git a/hack/generator/pkg/test/asserts.go b/hack/generator/pkg/test/asserts.go index 57d28c0a881..881719b5335 100644 --- a/hack/generator/pkg/test/asserts.go +++ b/hack/generator/pkg/test/asserts.go @@ -7,6 +7,7 @@ package test import ( "bytes" + "fmt" "testing" "github.com/sebdah/goldie/v2" @@ -14,7 +15,7 @@ import ( "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" ) -// AssertFileGeneratesExpectedCode serialises the given FileDefintion as a golden file test, checking that the expected +// AssertFileGeneratesExpectedCode serialises the given FileDefinition as a golden file test, checking that the expected // results are generated func AssertFileGeneratesExpectedCode(t *testing.T, fileDef *astmodel.FileDefinition, testName string) { g := goldie.New(t) @@ -28,3 +29,27 @@ func AssertFileGeneratesExpectedCode(t *testing.T, fileDef *astmodel.FileDefinit g.Assert(t, testName, buf.Bytes()) } + +// AssertPackagesGenerateExpectedCode creates a golden file for each package represented in the passed set of type +// definitions, asserting that the generated content is expected +func AssertPackagesGenerateExpectedCode(t *testing.T, types astmodel.Types, prefix string) { + // Group type definitions by package + groups := make(map[astmodel.PackageReference][]astmodel.TypeDefinition) + for _, def := range types { + ref := def.Name().PackageReference + groups[ref] = append(groups[ref], def) + } + + // Write a file for each package + for _, defs := range groups { + ref := defs[0].Name().PackageReference + local, ok := ref.AsLocalPackage() + if !ok { + panic("Must only have types from local packages - fix your test") + } + + fileName := fmt.Sprintf("%s-%s", prefix, local.Version()) + file := CreateFileDefinition(defs...) + AssertFileGeneratesExpectedCode(t, file, fileName) + } +} diff --git a/hack/generator/pkg/test/resource.go b/hack/generator/pkg/test/resource.go new file mode 100644 index 00000000000..9122d497dce --- /dev/null +++ b/hack/generator/pkg/test/resource.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package test + +import ( + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// CreateResource makes a resource for testing +func CreateResource( + pkg astmodel.PackageReference, + name string, + spec astmodel.TypeDefinition, + status astmodel.TypeDefinition, + functions ...astmodel.Function) astmodel.TypeDefinition { + + resourceType := astmodel.NewResourceType(spec.Name(), status.Name()) + for _, fn := range functions { + resourceType = resourceType.WithFunction(fn) + } + + return astmodel.MakeTypeDefinition(astmodel.MakeTypeName(pkg, name), resourceType) +} + +// CreateSpec makes a spec for testing +func CreateSpec( + pkg astmodel.PackageReference, + name string, + properties ...*astmodel.PropertyDefinition) astmodel.TypeDefinition { + specName := astmodel.MakeTypeName(pkg, name+"_Spec") + return astmodel.MakeTypeDefinition( + specName, + astmodel.NewObjectType().WithProperties(properties...)) +} + +// CreateStatus makes a status for testing +func CreateStatus(pkg astmodel.PackageReference, name string) astmodel.TypeDefinition { + statusProperty := astmodel.NewPropertyDefinition("Status", "status", astmodel.StringType) + statusName := astmodel.MakeTypeName(pkg, name+"_Status") + return astmodel.MakeTypeDefinition( + statusName, + astmodel.NewObjectType().WithProperties(statusProperty)) +} + +// CreateObjectDefinition makes an object for testing +func CreateObjectDefinition( + pkg astmodel.PackageReference, + name string, + properties ...*astmodel.PropertyDefinition) astmodel.TypeDefinition { + + typeName := astmodel.MakeTypeName(pkg, name) + return astmodel.MakeTypeDefinition( + typeName, + astmodel.NewObjectType().WithProperties(properties...)) +} diff --git a/hack/generator/pkg/testcases/object_type_serialization_test_case.go b/hack/generator/pkg/testcases/object_type_serialization_test_case.go index 8972d3c4cff..93d3b2248b5 100644 --- a/hack/generator/pkg/testcases/object_type_serialization_test_case.go +++ b/hack/generator/pkg/testcases/object_type_serialization_test_case.go @@ -70,6 +70,10 @@ func (o ObjectSerializationTestCase) AsFuncs(name astmodel.TypeName, genContext // Remove properties from our runtime o.removeByPackage(properties, astmodel.GenRuntimeReference) + // Remove API machinery properties + o.removeByPackage(properties, astmodel.APIMachineryRuntimeReference) + o.removeByPackage(properties, astmodel.APIMachinerySchemaReference) + // Temporarily remove properties related to support for Arbitrary JSON // TODO: Add generators for these properties o.removeByPackage(properties, astmodel.APIExtensionsReference) @@ -147,8 +151,15 @@ func (o ObjectSerializationTestCase) RequiredImports() *astmodel.PackageImportSe } // Equals determines if this TestCase is equal to another one -func (o ObjectSerializationTestCase) Equals(_ astmodel.TestCase) bool { - panic("implement me") +func (o ObjectSerializationTestCase) Equals(other astmodel.TestCase) bool { + otherTC, ok := other.(*ObjectSerializationTestCase) + if !ok { + return false + } + + return o.testName == otherTC.testName && + o.subject.Equals(otherTC.subject) && + o.objectType.Equals(otherTC.objectType) } // createTestRunner generates the AST for the test runner itself