crossplane / crossplane-tools

Experimental code generators for Crossplane controllers.
https://crossplane.io
Apache License 2.0
34 stars 26 forks source link

Generator for reference resolver functions #35

Closed muvaf closed 3 years ago

muvaf commented 3 years ago

Description of your changes

During Terrajet work, we discovered that we can actually generate the reference resolvers inALL Crossplane providers if enough information is supplied. This PR introduces a generator that will allow provider authors to add a comment to referencer field and let angryjet generate the zz_generated.resolvers.go file. It is able to handle pointers, arrays with both single and multi resolution calls and normal string references. Additionally, it allows you to override the extractor function, ref field name and selector field name with optional comment markers. By default, it uses external name extractor and appends Ref/Refs and Selector to the field name to find those fields.

Note that it doesn't make any change to CRD struct; authors still need to add FieldNameRef and FieldNameSelector fields on their own for the generated code to compile.

Example usage:

type ModelParameters struct {
    // +crossplane:generate:reference:type=Apigatewayv2Api
    APIID string `json:"APIID"`

    // +crossplane:generate:reference:type=SecurityGroup
    SecurityGroupID *string `json:"SecurityGroupID"`

    Network *NetworkSpec `json:"Network"`

    OtherSetting []OtherSpec `json:"OtherSetting"`

    // +crossplane:generate:reference:type=Subnet
    SubnetIDs []string `json:"SubnetIDs"`

    // +crossplane:generate:reference:type=RouteTable
    RouteTableIDs []*string `json:"RouteTableIDs"`
}

type OtherSpec struct {
    // +crossplane:generate:reference:type=Cluster
    OtherID string `json:"OtherID"`
}

type NetworkSpec struct {
    // +crossplane:generate:reference:type=github.com/crossplane/provider-aws/apis/ec2/v1beta1.VPC
    VPCID string `json:"vpcId"`
}

The generated output:

// ResolveReferences of this Model.
func (mg *Model) ResolveReferences(ctx context.Context, c client.Reader) error {
    r := reference.NewAPIResolver(c, mg)

    var rsp reference.ResolutionResponse
    var mrsp reference.MultiResolutionResponse
    var err error

    rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
        CurrentValue: mg.Spec.ForProvider.APIID,
        Extract:      reference.ExternalName(),
        Reference:    mg.Spec.ForProvider.APIIDRef,
        Selector:     mg.Spec.ForProvider.APIIDSelector,
        To: reference.To{
            List:    &Apigatewayv2ApiList{},
            Managed: &Apigatewayv2Api{},
        },
    })
    if err != nil {
        return errors.Wrap(err, "mg.Spec.ForProvider.APIID")
    }
    mg.Spec.ForProvider.APIID = rsp.ResolvedValue
    mg.Spec.ForProvider.APIIDRef = rsp.ResolvedReference

    rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
        CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.SecurityGroupID),
        Extract:      reference.ExternalName(),
        Reference:    mg.Spec.ForProvider.SecurityGroupIDRef,
        Selector:     mg.Spec.ForProvider.SecurityGroupIDSelector,
        To: reference.To{
            List:    &SecurityGroupList{},
            Managed: &SecurityGroup{},
        },
    })
    if err != nil {
        return errors.Wrap(err, "mg.Spec.ForProvider.SecurityGroupID")
    }
    mg.Spec.ForProvider.SecurityGroupID = reference.ToPtrValue(rsp.ResolvedValue)
    mg.Spec.ForProvider.SecurityGroupIDRef = rsp.ResolvedReference

    rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
        CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.IAMRoleARN),
        Extract:      v1beta1.IAMRoleARN(),
        Reference:    mg.Spec.ForProvider.IAMRoleARNRef,
        Selector:     mg.Spec.ForProvider.IAMRoleARNSelector,
        To: reference.To{
            List:    &v1beta1.IAMList{},
            Managed: &v1beta1.IAM{},
        },
    })
    if err != nil {
        return errors.Wrap(err, "mg.Spec.ForProvider.IAMRoleARN")
    }
    mg.Spec.ForProvider.IAMRoleARN = reference.ToPtrValue(rsp.ResolvedValue)
    mg.Spec.ForProvider.IAMRoleARNRef = rsp.ResolvedReference

    if mg.Spec.ForProvider.Network != nil {
        rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
            CurrentValue: mg.Spec.ForProvider.Network.VPCID,
            Extract:      reference.ExternalName(),
            Reference:    mg.Spec.ForProvider.Network.VPCIDRef,
            Selector:     mg.Spec.ForProvider.Network.VPCIDSelector,
            To: reference.To{
                List:    &v1beta11.VPCList{},
                Managed: &v1beta11.VPC{},
            },
        })
        if err != nil {
            return errors.Wrap(err, "mg.Spec.ForProvider.Network.VPCID")
        }
        mg.Spec.ForProvider.Network.VPCID = rsp.ResolvedValue
        mg.Spec.ForProvider.Network.VPCIDRef = rsp.ResolvedReference

    }
    for i3 := 0; i3 < len(mg.Spec.ForProvider.OtherSetting); i3++ {
        rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
            CurrentValue: mg.Spec.ForProvider.OtherSetting[i3].OtherID,
            Extract:      reference.ExternalName(),
            Reference:    mg.Spec.ForProvider.OtherSetting[i3].OtherIDRef,
            Selector:     mg.Spec.ForProvider.OtherSetting[i3].OtherIDSelector,
            To: reference.To{
                List:    &ClusterList{},
                Managed: &Cluster{},
            },
        })
        if err != nil {
            return errors.Wrap(err, "mg.Spec.ForProvider.OtherSetting[i3].OtherID")
        }
        mg.Spec.ForProvider.OtherSetting[i3].OtherID = rsp.ResolvedValue
        mg.Spec.ForProvider.OtherSetting[i3].OtherIDRef = rsp.ResolvedReference

    }
    mrsp, err = r.ResolveMultiple(ctx, reference.MultiResolutionRequest{
        CurrentValues: mg.Spec.ForProvider.SubnetIDs,
        Extract:       reference.ExternalName(),
        References:    mg.Spec.ForProvider.SubnetIDRefs,
        Selector:      mg.Spec.ForProvider.SubnetIDSelector,
        To: reference.To{
            List:    &SubnetList{},
            Managed: &Subnet{},
        },
    })
    if err != nil {
        return errors.Wrap(err, "mg.Spec.ForProvider.SubnetIDs")
    }
    mg.Spec.ForProvider.SubnetIDs = mrsp.ResolvedValues
    mg.Spec.ForProvider.SubnetIDRefs = mrsp.ResolvedReferences

    mrsp, err = r.ResolveMultiple(ctx, reference.MultiResolutionRequest{
        CurrentValues: reference.FromPtrValues(mg.Spec.ForProvider.RouteTableIDs),
        Extract:       reference.ExternalName(),
        References:    mg.Spec.ForProvider.RouteTableIDsRefs,
        Selector:      mg.Spec.ForProvider.RouteTableIDsSelector,
        To: reference.To{
            List:    &RouteTableList{},
            Managed: &RouteTable{},
        },
    })
    if err != nil {
        return errors.Wrap(err, "mg.Spec.ForProvider.RouteTableIDs")
    }
    mg.Spec.ForProvider.RouteTableIDs = reference.ToPtrValues(mrsp.ResolvedValues)
    mg.Spec.ForProvider.RouteTableIDsRefs = mrsp.ResolvedReferences

    return nil
}

I have:

How has this code been tested

Added a comprehensive unit tests and also tested the code with Go compiler using a provider-aws resource.

muvaf commented 3 years ago

@negz could you take a look at this since you wrote most of angryjet?

muvaf commented 3 years ago

How did you find jen for this task? I feel like this kind of complex code generation where the code you're generating is a lot less fixed is where it shines.

Honestly, the number of look up I had to do for very simple stuff felt too many, so I did feel like it's getting in the way. But handling import statements was indeed helpful. What I'd prefer is go template with no conditionals or loops, since it looks very similar to the actual code, where all conditional/loop logic is handled in actual Go code with string manipulation.