Multiple packages in provider causes import cycle during acceptance testing

If a provider consists of only one package, then one can invoke constructors/factories from data, resources, and the provider easily within acceptance tests. However, if the provider organization becomes larger and multiple packages are utilized, then this causes an import cycle following the pattern outlined in the scaffolding Github Repo.

For example if there are data and resources in a package category within the provider, then the constructors/factories for these data and resources are within the package category because they return a pointer to the interface and also rely on the func defining its implementations. This is all well and good because we simply import this nested package within the provider for the e.g. resources:

import "category"
...
func (_ *myProvider) Resources(ctx context.Context) []func() resource.Resource {
	return []func() resource.Resource{
		category.NewMyResource,
	}
}

Unfortunately the factory for the provider interface server also relies on the func defining the provider implementations, and therefore must be within the same package as the provider func definitions, or import them also. Since the acceptance testing uses this factory like:

resource.ParallelTest(test, resource.TestCase{
		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
...

then this construes a dependency of the resource and data upon the provider factory, and the factory upon the provider definitions, and the provider definitions upon the resource and data. As expected this causes a circular dependency and import cycle.

Since the organization and pattern in the terraform plugin framework scaffolding repo cannot be used for acceptance testing in a larger provider with multiple nested packages, then what would be the recommended organization and pattern for achieving this?

Hi @mschuchard :wave: Great question. Have you tried moving the implementation of testAccProtoV6ProviderFactories to its own package (and exporting it) instead of leaving it within the provider package? For example in the hashicorp/aws provider codebase, they created an internal/acctest package with their provider factories: https://github.com/hashicorp/terraform-provider-aws/blob/cbdd9f163c2f944cb9a6d427772ba6f73776aa45/internal/acctest/acctest.go#L119 which gets referenced by acceptance tests such as: https://github.com/hashicorp/terraform-provider-aws/blob/cbdd9f163c2f944cb9a6d427772ba6f73776aa45/internal/service/acm/certificate_test.go#L37

1 Like

Coincidentally(?) I had the same idea and also reviewed the same code in the AWS provider, but did not succeed.

I hit a wall because once I moved the factory to its own package and exported it then it errored because it required the provider constructor, which required the provider interface struct, which required the Configure etc. func defining the implementation, and then I had returned to the same import cycle as before.

When I reviewed the AWS code at internal/acctest I thought it was valid for SDKv2 and not the plugin framework, and that was an error on my part. I also reviewed the line of code you referenced and noticed it invokes a function defined at https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/provider/factory.go#L19, and that invokes the provider factory, and so it is unclear to me how this does not also slam into the same import cycle as it has the same dependency hierarchy.

When creating tests in Go, one option you have is to use the _test package convention, which is treated as a new package, but can live in the same code directory. In this sort of setup you then have a package dependency graph such as:

category_test
   |
   V
provider
   |
   V
category

Theoretically, nothing should ever reference the category_test package to cause an import cycle. If you look at the top of the aws_acm_certificate testing file linked above, you can see this subtle change in action: https://github.com/hashicorp/terraform-provider-aws/blob/cbdd9f163c2f944cb9a6d427772ba6f73776aa45/internal/service/acm/certificate_test.go#L4 My apologies, I should have more explicitly mentioned that important detail.

To your original point, we may want to consider having some form of tutorial for “larger” providers, since splitting up Go packages is a common development strategy in those. I’ll ensure there is a documentation request covering this in the framework issue tracker. Otherwise, I’m not sure personally if it would be more or less confusing to newer Go/Terraform Provider developers to change the scaffolding repository and tutorials to using the _test package for testing.

Thanks Brian that totally does resolve the import cycle. I have not seen that pattern for test organization before in Go, and am more familiar with it in high-level/interpretive languages e.g. Python, Ruby, JS, etc. and not so much with low-level languages e.g. C++, Rust, Go, etc. so I did not even think to consider it. I also through it would violate single package definition per directory. I know import cycles can be a pain in Go, but other languages have their own annoyances as well, and definitely none are perfect.

I am also unsure what sort of documentation update would be helpful here. Up to this point referencing the AWS provider source code has been my “goto” for anything requiring complexity (although it is a hybrid of framework and SDKv2), but the scaffolding and documentation were definitely helpful for my first plugin with the new framework a few months ago. I think a software architecture diagram with basic stubbing like this workaround would be helpful.

Something I also found helpful for this current plugin was separating re-used models, schema, and SDK bindings models to plugin models converters (because of the Go type to tftype conversion) into a separate package. I am unsure how that would conform to “best practices” (AWS provider probably does something more clever), but it has been wonderful for me thus far. I also did the SDK for this API though so I guess I had more flexibility over it?

Anyway thanks again for this.