Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the Convertible interface on non-hub resources #1628

Merged
merged 14 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
* Licensed under the MIT license.
*/

package storage

import "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel"
package astmodel

// FunctionInjector is a utility for injecting function definitions into resources and objects
type FunctionInjector struct {
// visitor is used to do the actual injection
visitor astmodel.TypeVisitor
visitor TypeVisitor
}

// NewFunctionInjector creates a new function injector for modifying resources and objects
func NewFunctionInjector() *FunctionInjector {
result := &FunctionInjector{}

result.visitor = astmodel.TypeVisitorBuilder{
result.visitor = TypeVisitorBuilder{
VisitObjectType: result.injectFunctionIntoObject,
VisitResourceType: result.injectFunctionIntoResource,
}.Build()
Expand All @@ -26,22 +24,32 @@ func NewFunctionInjector() *FunctionInjector {
}

// Inject modifies the passed type definition by injecting the passed function
func (fi *FunctionInjector) Inject(def astmodel.TypeDefinition, fn astmodel.Function) (astmodel.TypeDefinition, error) {
return fi.visitor.VisitDefinition(def, fn)
func (fi *FunctionInjector) Inject(def TypeDefinition, fns ...Function) (TypeDefinition, error) {
result := def

for _, fn := range fns {
var err error
result, err = fi.visitor.VisitDefinition(result, fn)
if err != nil {
return TypeDefinition{}, err
}
}

return result, nil
}

// injectFunctionIntoObject takes the function provided as a context and includes it on the
// provided object type
func (_ *FunctionInjector) injectFunctionIntoObject(
_ *astmodel.TypeVisitor, ot *astmodel.ObjectType, ctx interface{}) (astmodel.Type, error) {
fn := ctx.(astmodel.Function)
_ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) {
fn := ctx.(Function)
return ot.WithFunction(fn), nil
}

// injectFunctionIntoResource takes the function provided as a context and includes it on the
// provided resource type
func (_ *FunctionInjector) injectFunctionIntoResource(
_ *astmodel.TypeVisitor, rt *astmodel.ResourceType, ctx interface{}) (astmodel.Type, error) {
fn := ctx.(astmodel.Function)
_ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) {
fn := ctx.(Function)
return rt.WithFunction(fn), nil
}
5 changes: 5 additions & 0 deletions hack/generator/pkg/astmodel/interface_implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ func (iface *InterfaceImplementation) References() TypeNameSet {
return results
}

// FunctionCount returns the number of included functions
func (iface *InterfaceImplementation) FunctionCount() int {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to just expose this as Functions() like exists on ResourceType? (Can we do that since this is also embedded there?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions returns a copy (by necessity), whereas this can return the count directly. Not that we're too performance sensitive, but that's why I added. it. 😁

return len(iface.functions)
}

// Equals determines if this interface is equal to another interface
func (iface *InterfaceImplementation) Equals(other *InterfaceImplementation) bool {
if len(iface.functions) != len(other.functions) {
Expand Down
49 changes: 49 additions & 0 deletions hack/generator/pkg/astmodel/interface_injector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package astmodel

// InterfaceInjector is a utility for injecting interface implementations into resources and objects
type InterfaceInjector struct {
// visitor is used to do the actual injection
visitor TypeVisitor
}

// NewInterfaceInjector creates a new interface injector for modifying resources and objects
func NewInterfaceInjector() *InterfaceInjector {
result := &InterfaceInjector{}

result.visitor = TypeVisitorBuilder{
VisitObjectType: result.injectInterfaceIntoObject,
VisitResourceType: result.injectInterfaceIntoResource,
}.Build()

return result
}

// Inject modifies the passed type definition by injecting the passed function
func (i *InterfaceInjector) Inject(def TypeDefinition, implementation *InterfaceImplementation) (TypeDefinition, error) {
result, err := i.visitor.VisitDefinition(def, implementation)
if err != nil {
return TypeDefinition{}, err
}
return result, nil
}

// injectFunctionIntoObject takes the function provided as a context and includes it on the
// provided object type
func (i *InterfaceInjector) injectInterfaceIntoObject(
_ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) {
implementation := ctx.(*InterfaceImplementation)
return ot.WithInterface(implementation), nil
}

// injectFunctionIntoResource takes the function provided as a context and includes it on the
// provided resource type
func (i *InterfaceInjector) injectInterfaceIntoResource(
_ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) {
fn := ctx.(*InterfaceImplementation)
return rt.WithInterface(fn), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
* Licensed under the MIT license.
*/

package storage

import "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel"
package 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
visitor TypeVisitor
}

// NewPropertyInjector creates a new property injector for modifying resources and objects
func NewPropertyInjector() *PropertyInjector {
result := &PropertyInjector{}

result.visitor = astmodel.TypeVisitorBuilder{
result.visitor = TypeVisitorBuilder{
VisitObjectType: result.injectPropertyIntoObject,
VisitResourceType: result.injectPropertyIntoResource,
}.Build()
Expand All @@ -26,20 +24,20 @@ func NewPropertyInjector() *PropertyInjector {
}

// Inject modifies the passed type definition by injecting the passed property
func (pi *PropertyInjector) Inject(def astmodel.TypeDefinition, prop *astmodel.PropertyDefinition) (astmodel.TypeDefinition, error) {
func (pi *PropertyInjector) Inject(def TypeDefinition, prop *PropertyDefinition) (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)
_ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) {
prop := ctx.(*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)
_ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) {
prop := ctx.(*PropertyDefinition)
return rt.WithProperty(prop), nil
}
2 changes: 1 addition & 1 deletion hack/generator/pkg/astmodel/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func (types Types) ResolveResourceStatusDefinition(
}

// Process applies a func to transform all members of this set of type definitions, returning a new set of type
// definitions containing the results of the transfomration, or possibly an error
// definitions containing the results of the transformation, or possibly an error
// Only definitions returned by the func will be included in the results of the function. The func may return a nil
// TypeDefinition if it doesn't want to include anything in the output set.
func (types Types) Process(transformation func(definition TypeDefinition) (*TypeDefinition, error)) (Types, error) {
Expand Down
1 change: 1 addition & 0 deletions hack/generator/pkg/codegen/code_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func createAllPipelineStages(idFactory astmodel.IdentifierFactory, configuration
See https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/webhook/conversion/conversion.go#L310

pipeline.InjectHubFunction(idFactory).UsedFor(pipeline.ARMTarget),
pipeline.ImplementConvertibleInterface(idFactory),
*/

// Safety checks at the end:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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"
)

// ImplementConvertibleInterfaceStageId is the unique identifier for this pipeline stage
const ImplementConvertibleInterfaceStageId = "implementConvertibleInterface"

// ImplementConvertibleInterface injects the functions ConvertTo() and ConvertFrom() into each non-hub Resource
// Type, providing the required implementation of the Convertible interface needed by the controller
func ImplementConvertibleInterface(idFactory astmodel.IdentifierFactory) Stage {

stage := MakeStage(
ImplementConvertibleInterfaceStageId,
"Implement the Convertible interface on each non-hub Resource type",
func(ctx context.Context, state *State) (*State, error) {
injector := astmodel.NewInterfaceInjector()

modifiedTypes := make(astmodel.Types)
resources := storage.FindResourceTypes(state.Types())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this method (and the others in types_tools that are generic) should be moved to astmodel along with your interface/function injectors?

Reading this made me think it was finding the storage resource types or something, I had to go look at the impl to understand it was just any old resource type.

Your FindStatusTypes could probably replace my findAllResourceStatusTypes in remover.go as well, although we could leave that for another PR because it'll require changing some types around I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'm sure moved these into astmodel in a later PR, not sure which one off the top of my head.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider pulling it forward? I've got at least one upcoming PR where I'd like to use the Status one.

for name, def := range resources {
resource, ok := astmodel.AsResourceType(def.Type())
if !ok {
// Skip non-resources (though, they should be filtered out, above)
continue
}

if resource.IsStorageVersion() {
// The hub storage version doesn't implement Convertible
continue
}

convertible := createConvertibleInterfaceImplementation(
name, resource, state.ConversionGraph(), idFactory)
if convertible.FunctionCount() > 0 {
modified, err := injector.Inject(def, convertible)
if err != nil {
return nil, errors.Wrapf(err, "injecting Convertible interface into %s", name)
}

modifiedTypes.Add(modified)
}
}

newTypes := state.Types().OverlayWith(modifiedTypes)
return state.WithTypes(newTypes), nil
})

stage.RequiresPrerequisiteStages(InjectPropertyAssignmentFunctionsStageId)
return stage
}

// createConvertibleInterfaceImplementation creates the required implementation of conversion.Convertible, ready for
// injection onto the resource. The ConvertTo() and ConvertFrom() methods chain the required conversion between resource
// versions, but are dependent upon previously injected AssignPropertiesTo() and AssignPropertiesFrom() methods to
// actually copy information across. See resource_conversion_function.go for more information.
func createConvertibleInterfaceImplementation(
name astmodel.TypeName,
resource *astmodel.ResourceType,
conversionGraph *storage.ConversionGraph,
idFactory astmodel.IdentifierFactory) *astmodel.InterfaceImplementation {
var conversionFunctions []astmodel.Function

for _, fn := range resource.Functions() {
if propertyAssignmentFn, ok := fn.(*functions.PropertyAssignmentFunction); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment explaining what the relationship between the PropertyAssignmentFunctions and and Convertible might be useful here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that there are resources without a PropertyAssignmentFunctions, or should that be an error condition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hub resources don't have property assignment functions - conversions are always to/from types closer to the hub; hub resources have nothing to convert with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this function isn't ever invoked with hub resources anyway, because of the

				if resource.IsStorageVersion() {
					// The hub storage version doesn't implement Convertible
					continue
				}

in the loop yes?

That's why I was wondering if you wanted to tolerate types without those functions or if it made sense to just fail fast if you found one since that meant something suspicious was going on.

hub := conversionGraph.FindHubTypeName(name)
conversionFn := functions.NewResourceConversionFunction(hub, propertyAssignmentFn, idFactory)
conversionFunctions = append(conversionFunctions, conversionFn)
}
}

return astmodel.NewInterfaceImplementation(astmodel.ConvertibleInterface, conversionFunctions...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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"
)

// TestInjectConvertibleInterface checks that the pipeline stage does what we expect when run in relative isolation,
// with only a few expected (and closely reated) stages in operation
func TestInjectConvertibleInterface(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this test be replaced with a standard json input file + golden file once the stages are actually enabled in the pipeline?

If so, consider leaving a TODO here to that effect?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the purpose of this test is to test this pipeline stage in isolation (well, as much isolation as possible). I don't want to conflate the results of this test with other things happening to independent stages.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment.

g := NewGomegaWithT(t)

idFactory := astmodel.NewIdentifierFactory()
// Test Resource V1

specV1 := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty)
statusV1 := test.CreateStatus(test.Pkg2020, "Person")
resourceV1 := test.CreateResource(test.Pkg2020, "Person", specV1, statusV1)

// Test Resource V2

specV2 := test.CreateSpec(test.Pkg2021, "Person", test.FullNameProperty)
statusV2 := test.CreateStatus(test.Pkg2021, "Person")
resourceV2 := test.CreateResource(test.Pkg2021, "Person", specV2, statusV2)

types := make(astmodel.Types)
types.AddAll(resourceV1, specV1, statusV1, resourceV2, specV2, statusV2)

state := NewState().WithTypes(types)

// Run CreateConversionGraph first to populate the conversion graph
createConversionGraph := CreateConversionGraph()
state, err := createConversionGraph.Run(context.TODO(), state)
g.Expect(err).To(Succeed())

// Run CreateStorageTypes to create additional resources into which we will inject
createStorageTypes := CreateStorageTypes()
state, err = createStorageTypes.Run(context.TODO(), state)
g.Expect(err).To(Succeed())

// Run InjectPropertyAssignmentFunctions to create those functions
injectPropertyFns := InjectPropertyAssignmentFunctions(idFactory)
state, err = injectPropertyFns.Run(context.TODO(), state)
g.Expect(err).To(Succeed())

// Now run our stage
injectFunctions := ImplementConvertibleInterface(idFactory)
state, err = injectFunctions.Run(context.TODO(), state)
g.Expect(err).To(Succeed())

// When verifying the golden file, check that the implementations of ConvertTo() and ConvertFrom() are what you
// expect - if you don't have expectations, check that they do the right thing.
test.AssertPackagesGenerateExpectedCode(t, state.Types(), t.Name())
}
2 changes: 1 addition & 1 deletion hack/generator/pkg/codegen/pipeline/inject_hub_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func InjectHubFunction(idFactory astmodel.IdentifierFactory) Stage {
InjectHubFunctionStageId,
"Inject the function Hub() into each hub resource",
func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) {
injector := storage.NewFunctionInjector()
injector := astmodel.NewFunctionInjector()
result := types.Copy()

resources := storage.FindResourceTypes(types)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func InjectOriginalGVKFunction(idFactory astmodel.IdentifierFactory) Stage {
injectOriginalGVKFunctionId,
"Inject the function OriginalGVK() into each Resource type",
func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) {
injector := storage.NewFunctionInjector()
injector := astmodel.NewFunctionInjector()
result := types.Copy()

resources := storage.FindResourceTypes(types)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func InjectOriginalVersionFunction(idFactory astmodel.IdentifierFactory) Stage {
InjectOriginalVersionFunctionStageId,
"Inject the function OriginalVersion() into each Spec type",
func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) {
injector := storage.NewFunctionInjector()
injector := astmodel.NewFunctionInjector()
result := types.Copy()

specs := storage.FindSpecTypes(types)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func InjectOriginalVersionProperty() Stage {
InjectOriginalVersionPropertyId,
"Inject the property OriginalVersion into each Storage Spec type",
func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) {
injector := storage.NewPropertyInjector()
injector := astmodel.NewPropertyInjector()
result := types.Copy()

doesNotHaveOriginalVersionFunction := func(definition astmodel.TypeDefinition) bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
. "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"
)
Expand Down Expand Up @@ -43,7 +42,7 @@ func TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotI
g := NewGomegaWithT(t)

idFactory := astmodel.NewIdentifierFactory()
fnInjector := storage.NewFunctionInjector()
fnInjector := astmodel.NewFunctionInjector()

// Define a test resource
spec := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty, test.FamilyNameProperty, test.KnownAsProperty)
Expand Down
Loading