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

Add a transformer to remove API group imports for cross-resource references #331

Merged
merged 8 commits into from
Jan 28, 2024

Conversation

ulucinar
Copy link
Collaborator

@ulucinar ulucinar commented Jan 19, 2024

Description of your changes

Fixes #96

While working in the context of the multi-version CRD & conversion function generation pipelines and #321 & crossplane-contrib/provider-upjet-aws#1075, we observed that selectively moving individual resources to different API versions increases the likelihood of #96 because cross-resource references are more common inside an API group. We observed multiple cases of #96 in crossplane-contrib/provider-upjet-aws#1075 when we bumped the API versions of MRs with breaking API changes, such as the RoutingProfile.connect resource.

This PR employs a dynamic type registry in the calls to the APIResolver so that we don't need to import the modules for the individual GVKs in the resolver modules, which effectively eliminates the issue reported in #96.

While the approach taken here can be applied to the other providers in the ecosystem by implementing the logic in https://github.com/crossplane/crossplane-tools, we have decided to initially implement it in upjet as a transformer (as opposed to being a full-fledged generator). The crossplane-tools' angryjet is invoked first to generate the cross-resource reference resolvers (with cross API-group import statements) and then the transformer introduced with this change (resolver) can be applied as part of the build pipeline to get rid of the cross API-group import statements, effectively addressing #96 for upjet-based providers.

An example transformation obtained by running resolver is as follows:

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

	var rsp reference.ResolutionResponse
	var err error

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

is transformed into:

// ResolveReferences of this Subnet.
func (mg *Subnet) ResolveReferences(ctx context.Context, c client.Reader) error {
	var m xpresource.Managed
	var l xpresource.ManagedList
	r := reference.NewAPIResolver(c, mg)

	var rsp reference.ResolutionResponse
	var err error
	{
		m, l, err = apisresolver.GetManagedResource("ec2.aws.upbound.io",
			"v1beta1",
			"VPC", "VPCList",
		)
		if err != nil {
			return errors.Wrap(err, "failed to get the reference target managed resource and its list for reference resolution")
		}

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

TODO:

  • Implement unit tests

I have:

  • Read and followed Upjet's contribution process.
  • Run make reviewable to ensure this PR is ready for review.
  • Added backport release-x.y labels to auto-backport this PR if necessary.

How has this code been tested

All the generated resolver modules for upbound/provider-aws have been transformed using resolver in crossplane-contrib/provider-upjet-aws#1075. We have also tried moving the RoutingProfile.connect resource to version v1beta2, leaving all the other resources in the same API group at version v1beta1, made sure the import cycle exists between connect/v1beta1/zz_generated.resolvers.go & connect/v1beta2/zz_generated.resolvers.go, and applied to the transformer to remove the import cycle, and then successfully provision a RoutingProfile.connect instance using this manifest.

…resource reference resolver modules

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
- Fix the floating comment issue for the very first function declaration

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
… in function `getManagedResourceStatements`

- Use a hard-coded AST while generating the reference source variable declarations in function `addMRVariableDeclarations`

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
@ulucinar ulucinar force-pushed the acyclic-resolvers branch 4 times, most recently from 75e2f03 to db929c4 Compare January 25, 2024 08:25
@ulucinar ulucinar marked this pull request as ready for review January 25, 2024 08:28
- Bump Go version to 1.21
- Bump golangci-lint to v1.55.2
- Add tests for the Resolver.TransformPackages

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
Copy link
Member

@sergenyalcin sergenyalcin left a comment

Choose a reason for hiding this comment

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

Thanks @ulucinar I left a few comments

apiGroupSuffix = app.Flag("apiGroupSuffix", "Resource API group suffix, such as aws.upbound.io. The resource API group names are suffixed with this to get the canonical API group name.").Short('g').Required().String()
pattern = app.Flag("pattern", "List patterns for the packages to process, such as ./...").Short('p').Default("./...").Strings()
resolverFilePattern = app.Flag("resolver", "Name of the generated resolver files to process.").Short('r').Default("zz_generated.resolvers.go").String()
ignorePackageLoadErrors = app.Flag("ignoreLoadErrors", "Ignore errors encountered while loading the packages.").Short('s').Bool()
Copy link
Member

Choose a reason for hiding this comment

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

What about making the default of this option to true?

Copy link
Collaborator Author

@ulucinar ulucinar Jan 26, 2024

Choose a reason for hiding this comment

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

I feel like the default should be the more strict mode of operation (failing on package loading errors). Though I think I see your point: The purpose of this transformer is to prevent/get rid of circular imports, and any provider benefiting from this transformer should have an import cycle caused by the cross-API group references, so in order to run this transformer, they will need to supply the -s option. But still, I'd like the decision to ignore any package loading errors to be on the side of the Crossplane provider repo maintainer who's using this transformer as part of their code generation pipelines. So keeping the default mode strict forces the repo maintainer to show explicit consent while incorporating the transformer.

The package loading issues we expect are the import cycle errors caused by cross-resource references but what if the API folder has some unexpected (by the transformer) compile errors? I believe it's better to give the responsibility of suppressing package loading errors to the repo maintainer, although from a practical perspective, I agree setting the default of this option to true will not make much difference, if this is the reasoning behind the proposal.

@@ -0,0 +1,595 @@
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
Copy link
Member

Choose a reason for hiding this comment

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

In my local tests, I observe that the place of the first resolver function's code comment changes. This is not a very important issue. However, I thought this place could be reviewed in case it is re-visited.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for reporting this unwanted behavior. I've tackled with this issue a bit and found this upstream issue regarding the floating comments when we modify the existing code. Let me open a separate issue to address this as I suspect we would need to properly calculate the position of that single comment in the transformer source code or use a higher level library like this one to fix this and currently, to the best of my knowledge, it does cause any functional issues.

// e.g., v1beta1
version = tokens[len(tokens)-1]
// e.g., ec2.aws.upbound.io
group = fmt.Sprintf("%s.%s", tokens[len(tokens)-2], apiGroupSuffix)
Copy link
Member

@sergenyalcin sergenyalcin Jan 25, 2024

Choose a reason for hiding this comment

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

Will this statement cause any issues with the Azure-ResourceGroup resource? Because the API group of ResourceGroup is the same as the apiGroupSuffix: azure.upbound.io`.

I mean, the split statement will be this: github.com/upbound/provider-azure/apis/azure/v1beta1

Then, the calculated group will be azure.azure.upbound.io.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks @sergenyalcin, good catch indeed! Given this a try with upbound/provider-azure as you suggested and what we generate is:

m, l, err = apisresolver.GetManagedResource("azure.azure.upbound.io", "v1beta1", "ResourceGroup", "ResourceGroupList")
		if err != nil {
			return errors.Wrap(err, "failed to get the reference target managed resource and its list for reference resolution")
		}

Added the --api-group-override command-line option (shorthand -o) so that any API group overrides can be configured. Also added the --api-resolver-package (shorthand -a) command-line option so that we can properly configure the API resolver function's (GetManagedResource) package.

So, when we run the transformer with the following command-line options:

go run github.com/crossplane/upjet/cmd/resolver -g azure.upbound.io -a github.com/upbound/provider-azure/internal/apis -o azure.azure.upbound.io=azure.upbound.io

what gets now generated is:

		m, l, err = apisresolver.GetManagedResource("azure.upbound.io", "v1beta1", "ResourceGroup", "ResourceGroupList")
		if err != nil {
			return errors.Wrap(err, "failed to get the reference target managed resource and its list for reference resolution")
		}

In a future iteration (especially if some other providers also adopt this transformer), we may also generate the GetManagedResource function via upjet for the provider and remove the --api-resolver-package option.

// e.g., v1beta1
version = tokens[len(tokens)-2]
// e.g., cur.aws.upbound.io
group = fmt.Sprintf("%s.%s", tokens[len(tokens)-3], apiGroupSuffix)
Copy link
Member

Choose a reason for hiding this comment

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

Is there the above problem here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, we need to address this also. Please see the above comment explaining group overrides configuration and the test with upbound/provider-azure. Thank you @sergenyalcin.

…sformation

to be able to override the generated API group names.

- Add the --api-resolver-package command-line option to the resolver transformation
  to properly configure the package of the GetManagedResource function.

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
Copy link
Member

@sergenyalcin sergenyalcin left a comment

Choose a reason for hiding this comment

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

Thanks @ulucinar LGTM!

@ulucinar ulucinar merged commit c8c49ae into crossplane:main Jan 28, 2024
7 checks passed
@ulucinar ulucinar deleted the acyclic-resolvers branch January 28, 2024 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Explicit reference causes import cycle error
2 participants