Skip to content

Commit

Permalink
New conditions pipeline stages (#1689)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthchr authored Aug 6, 2021
1 parent 462f59f commit 4c3c12d
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func newConvertFromARMFunctionBuilder(
result.namePropertyHandler,
result.referencePropertyHandler,
result.ownerPropertyHandler,
result.conditionsPropertyHandler,
result.flattenedPropertyHandler,
result.propertiesWithSameNameHandler,
}
Expand Down Expand Up @@ -218,6 +219,24 @@ func (builder *convertFromARMBuilder) ownerPropertyHandler(
return []dst.Stmt{result}
}

// conditionsPropertyHandler generates conversions for the "Conditions" status property. This property is set by the controller
// after each reconcile and so does not need to be preserved.
func (builder *convertFromARMBuilder) conditionsPropertyHandler(
toProp *astmodel.PropertyDefinition,
_ *astmodel.ObjectType) []dst.Stmt {

isPropConditions := toProp.PropertyName() == builder.idFactory.CreatePropertyName(astmodel.ConditionsProperty, astmodel.Exported)
if !isPropConditions || builder.typeKind != TypeKindStatus {
return nil
}

// Returning an empty statement allows us to "consume" this match and not proceed to the next handler.
// There is nothing included in the generated code.
return []dst.Stmt{
&dst.EmptyStmt{},
}
}

// flattenedPropertyHandler generates conversions for properties that
// were flattened out from inside other properties. The code it generates will
// look something like:
Expand Down
107 changes: 107 additions & 0 deletions hack/generator/pkg/astmodel/conditioner_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package astmodel

import (
"go/token"

"github.com/dave/dst"

"github.com/Azure/azure-service-operator/hack/generator/pkg/astbuilder"
)

const (
ConditionsProperty = "Conditions"
)

// NewConditionerInterfaceImpl creates an InterfaceImplementation with GetConditions() and
// SetConditions() methods, implementing the genruntime.Conditioner interface.
func NewConditionerInterfaceImpl(
idFactory IdentifierFactory,
resource *ResourceType) (*InterfaceImplementation, error) {

getConditions := &resourceFunction{
name: "Get" + ConditionsProperty,
resource: resource,
idFactory: idFactory,
asFunc: getConditionsFunction,
requiredPackages: NewPackageReferenceSet(GenRuntimeConditionsReference),
}

setConditions := &resourceFunction{
name: "Set" + ConditionsProperty,
resource: resource,
idFactory: idFactory,
asFunc: setConditionsFunction,
requiredPackages: NewPackageReferenceSet(GenRuntimeConditionsReference),
}

result := NewInterfaceImplementation(
ConditionerTypeName,
getConditions,
setConditions)

return result, nil
}

// getConditionsFunction returns a function declaration containing the implementation of the GetConditions() function.
//
// func (r *<receiver>)GetConditions() genruntime.Conditions {
// return r.Status.Conditions
// }
func getConditionsFunction(k *resourceFunction, codeGenerationContext *CodeGenerationContext, receiver TypeName, methodName string) *dst.FuncDecl {
receiverIdent := k.idFactory.CreateIdentifier(receiver.Name(), NotExported)
receiverType := receiver.AsType(codeGenerationContext)

status := astbuilder.Selector(dst.NewIdent(receiverIdent), "Status")

fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: &dst.StarExpr{
X: receiverType,
},
Body: []dst.Stmt{
astbuilder.Returns(astbuilder.Selector(status, ConditionsProperty)),
},
}

fn.AddComments("returns the conditions of the resource")
fn.AddReturn(ConditionsTypeName.AsType(codeGenerationContext))

return fn.DefineFunc()
}

// setConditionsFunction returns a function declaration containing the implementation of the SetConditions() function.
//
// func (r *<receiver>)SetConditions(conditions genruntime.Conditions) {
// r.Status.Conditions = conditions
// }
func setConditionsFunction(k *resourceFunction, codeGenerationContext *CodeGenerationContext, receiver TypeName, methodName string) *dst.FuncDecl {
conditionsParameterName := k.idFactory.CreateIdentifier(ConditionsProperty, NotExported)

receiverIdent := k.idFactory.CreateIdentifier(receiver.Name(), NotExported)
receiverType := receiver.AsType(codeGenerationContext)
status := astbuilder.Selector(dst.NewIdent(receiverIdent), "Status")

fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: &dst.StarExpr{
X: receiverType,
},
Body: []dst.Stmt{
astbuilder.QualifiedAssignment(status, "Conditions", token.ASSIGN, dst.NewIdent(conditionsParameterName)),
},
}

fn.AddParameter(
conditionsParameterName,
ConditionsTypeName.AsType(codeGenerationContext))
fn.AddComments("sets the conditions on the resource status")

return fn.DefineFunc()
}
19 changes: 18 additions & 1 deletion hack/generator/pkg/astmodel/property_injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package astmodel

import "github.com/pkg/errors"

// PropertyInjector is a utility for injecting property definitions into resources and objects
type PropertyInjector struct {
// visitor is used to do the actual injection
Expand All @@ -25,19 +27,34 @@ func NewPropertyInjector() *PropertyInjector {

// Inject modifies the passed type definition by injecting the passed property
func (pi *PropertyInjector) Inject(def TypeDefinition, prop *PropertyDefinition) (TypeDefinition, error) {
return pi.visitor.VisitDefinition(def, prop)
result, err := pi.visitor.VisitDefinition(def, prop)
if err != nil {
return TypeDefinition{}, errors.Wrapf(err, "failed to inject property %q into %q", prop.PropertyName(), def.Name())
}

return result, nil
}

// injectPropertyIntoObject takes the property provided as a context and includes it on the provided object type
func (pi *PropertyInjector) injectPropertyIntoObject(
_ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) {
prop := ctx.(*PropertyDefinition)
// Ensure that we don't already have a property with the same name
if _, ok := ot.Property(prop.PropertyName()); ok {
return nil, errors.Errorf("already has property named %q", prop.PropertyName())
}

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(
_ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) {
prop := ctx.(*PropertyDefinition)
// Ensure that we don't already have a property with the same name
if _, ok := rt.Property(prop.PropertyName()); ok {
return nil, errors.Errorf("already has property named %q", prop.PropertyName())
}

return rt.WithProperty(prop), nil
}
8 changes: 6 additions & 2 deletions hack/generator/pkg/astmodel/std_references.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ var (
TestingReference = MakeExternalPackageReference("testing")

// References to our Libraries
GenRuntimeReference = MakeExternalPackageReference(genRuntimePathPrefix)
ReflectHelpersReference = MakeExternalPackageReference(reflectHelpersPath)
GenRuntimeReference = MakeExternalPackageReference(genRuntimePathPrefix)
GenRuntimeConditionsReference = MakeExternalPackageReference(genRuntimePathPrefix + "/conditions")
ReflectHelpersReference = MakeExternalPackageReference(reflectHelpersPath)

// References to other libraries
APIExtensionsReference = MakeExternalPackageReference("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1")
Expand Down Expand Up @@ -54,6 +55,9 @@ var (
ResourceReferenceTypeName = MakeTypeName(GenRuntimeReference, "ResourceReference")
KnownResourceReferenceTypeName = MakeTypeName(GenRuntimeReference, "KnownResourceReference")
ToARMConverterInterfaceType = MakeTypeName(GenRuntimeReference, "ToARMConverter")
ConditionTypeName = MakeTypeName(GenRuntimeConditionsReference, "Condition")
ConditionsTypeName = MakeTypeName(GenRuntimeConditionsReference, "Conditions")
ConditionerTypeName = MakeTypeName(GenRuntimeConditionsReference, "Conditioner")

// Type names - API Machinery
GroupVersionKindTypeName = MakeTypeName(APIMachinerySchemaReference, "GroupVersionKind")
Expand Down
3 changes: 2 additions & 1 deletion hack/generator/pkg/codegen/code_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ func createAllPipelineStages(idFactory astmodel.IdentifierFactory, configuration

// Effects the "flatten" property of Properties:
pipeline.FlattenProperties(),

// Remove types which may not be needed after flattening
pipeline.StripUnreferencedTypeDefinitions(),

pipeline.AddStatusConditions(idFactory).UsedFor(pipeline.ARMTarget),

pipeline.AddCrossplaneOwnerProperties(idFactory).UsedFor(pipeline.CrossplaneTarget),
pipeline.AddCrossplaneForProvider(idFactory).UsedFor(pipeline.CrossplaneTarget),
pipeline.AddCrossplaneAtProvider(idFactory).UsedFor(pipeline.CrossplaneTarget),
Expand Down
3 changes: 2 additions & 1 deletion hack/generator/pkg/codegen/golden_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ func NewTestCodeGenerator(testName string, path string, t *testing.T, testConfig
//pipeline.ImplementConvertibleInterfaceStageId,
pipeline.ImplementConvertibleSpecInterfaceStageId,
pipeline.ImplementConvertibleStatusInterfaceStageId,
pipeline.ReportOnTypesAndVersionsStageID)
pipeline.ReportOnTypesAndVersionsStageID,
pipeline.AddStatusConditionsStageID)
if !testConfig.HasARMResources {
codegen.RemoveStages(pipeline.CreateARMTypesStageID, pipeline.ApplyARMConversionInterfaceStageID)

Expand Down
57 changes: 57 additions & 0 deletions hack/generator/pkg/codegen/pipeline/add_status_conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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"
)

const AddStatusConditionsStageID = "addStatusConditions"

func AddStatusConditions(idFactory astmodel.IdentifierFactory) Stage {
return MakeStage(
AddStatusConditionsStageID,
"Adds the property 'Conditions' to all status types and implements genruntime.Conditioner on all resources",
func(ctx context.Context, state *State) (*State, error) {
defs := state.Types()
result := make(astmodel.Types)

propInjector := astmodel.NewPropertyInjector()
statusDefs := astmodel.FindStatusTypes(defs)
for _, def := range statusDefs {
conditionsProp := astmodel.NewPropertyDefinition(
astmodel.ConditionsProperty,
"conditions",
astmodel.NewArrayType(astmodel.ConditionTypeName))
conditionsProp = conditionsProp.WithDescription("The observed state of the resource").MakeOptional()
updatedDef, err := propInjector.Inject(def, conditionsProp)
if err != nil {
return nil, errors.Wrapf(err, "couldn't add Conditions condition to status %q", def.Name())
}
result.Add(updatedDef)
}

resourceDefs := astmodel.FindResourceTypes(defs)
for _, def := range resourceDefs {
resourceType := def.Type().(*astmodel.ResourceType)

conditionerImpl, err := astmodel.NewConditionerInterfaceImpl(idFactory, resourceType)
if err != nil {
return nil, errors.Wrapf(err, "couldn't create genruntime.Conditioner implementation for %q", def.Name())
}
resourceType = resourceType.WithInterface(conditionerImpl)

result.Add(def.WithType(resourceType))
}
result = defs.OverlayWith(result)

return state.WithTypes(result), nil
})
}
40 changes: 40 additions & 0 deletions hack/generator/pkg/codegen/pipeline/add_status_conditions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package pipeline

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"
)

// TestAddStatusConditions checks that the Add Status Conditions pipeline stage does what we expect
func TestAddStatusConditions(t *testing.T) {
g := NewGomegaWithT(t)

idFactory := astmodel.NewIdentifierFactory()

spec := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty)
status := test.CreateStatus(test.Pkg2020, "Person")
resourceV1 := test.CreateResource(test.Pkg2020, "Person", spec, status)

types := make(astmodel.Types)
types.AddAll(resourceV1, spec, status)

initialState := NewState().WithTypes(types)

finalState, err := RunTestPipeline(
initialState,
AddStatusConditions(idFactory))
g.Expect(err).To(Succeed())

// When verifying the golden file, check to ensure that the Conditions property on the Status type looks correct,
// and that the conditions.Conditioner interface is properly implemented on the resource.
test.AssertPackagesGenerateExpectedCode(t, finalState.types)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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/pkg/genruntime/conditions"
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"`
}

var _ conditions.Conditioner = &Person{}

// GetConditions returns the conditions of the resource
func (person *Person) GetConditions() conditions.Conditions {
return person.Status.Conditions
}

// SetConditions sets the conditions on the resource status
func (person *Person) SetConditions(conditions conditions.Conditions) {
person.Status.Conditions = conditions
}

// +kubebuilder:object:root=true
type PersonList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Person `json:"items"`
}

type Person_Spec struct {
//FullName: As would be used to address mail
FullName string `json:"fullName"`
}

type Person_Status struct {
//Conditions: The observed state of the resource
Conditions []conditions.Condition `json:"conditions,omitempty"`
Status string `json:"status"`
}

func init() {
SchemeBuilder.Register(&Person{}, &PersonList{})
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ applyArmConversionInterface azure Add ARM conversion interfac
applyKubernetesResourceInterface azure Add the KubernetesResource interface to every resource
flattenProperties Apply flattening to properties marked for flattening
stripUnreferenced Strip unreferenced types
addStatusConditions azure Adds the property 'Conditions' to all status types and implements genruntime.Conditioner on all resources
createConversionGraph azure Create the graph of conversions between versions of each resource group
injectOriginalVersionFunction azure Inject the function OriginalVersion() into each Spec type
createStorageTypes azure Create storage versions of CRD types
Expand Down
1 change: 1 addition & 0 deletions hack/generator/pkg/conversions/property_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func init() {
// Known types
copyKnownType(astmodel.KnownResourceReferenceTypeName, "Copy", returnsValue),
copyKnownType(astmodel.ResourceReferenceTypeName, "Copy", returnsValue),
copyKnownType(astmodel.ConditionTypeName, "Copy", returnsValue),
copyKnownType(astmodel.JSONTypeName, "DeepCopy", returnsReference),
// Meta-conversions
assignFromOptional, // Must go before assignToOptional so we generate the right zero values
Expand Down
Loading

0 comments on commit 4c3c12d

Please sign in to comment.