Skip to content

Latest commit

 

History

History
188 lines (147 loc) · 6.71 KB

File metadata and controls

188 lines (147 loc) · 6.71 KB

Registering Resources and Data Sources

There are two major steps to adding a resource or data source to a Terraform provider. First, the resource or data source must be defined. Second, the provider must be told about the resource or data source. This design doc is concerned with that second bit, and is meant to explore the different approaches we could take to allowing resources and data sources to be registered with providers.

The end goal of each of these approaches is that the provider has a complete list of the resources and data sources that are available through the provider, and knows what name they'll be referred to with in HCL.

Approaches

There are fundamentally two different approaches for this problem: the provider can maintain a list of all the resources and data sources available, or the resources and data sources can be held responsible for telling the provider about themselves.

Provider-Owned List

A provider-owned list (technically, map) is the approach that helper/schema took:

type Provider struct {
	Resources map[string]*Resource
	DataSources map[string]*Resource
}

then when defining your provider, typically in provider.go, you'd have something like this:

func New() *schema.Provider {
	// provider schema, etc. goes here
	Resources: map[string]*schema.Resource{
		"foo_compute_instance": resourceComputeInstance(),
		"foo_compute_disk": resourceComputeDisk(),
	},
	DataSources: map[string]*schema.Resource{
		"foo_compute_image": dataComputeImage(),
	},
	// provider_meta schema, etc. goes here
}

To add a new resource or data source, you just add another line to the map definition.

Resource-Owned Registration

Another approach is to have the resources and data sources own their own registration, telling the provider about themselves:

// inside resource_compute_instance.go
func init() {
	framework.RegisterResource("foo_compute_instance", resourceComputeInstance())
}
// inside resource_compute_disk.go
func init() {
	framework.RegisterResource("foo_compute_disk", resourceComputeDisk())
}
// inside data_compute_image.go
func init() {
	framework.RegisterDataSource("foo_compute_image", dataComputeImage())
}

Trade-offs

There are a number of things to consider when evaluating these two approaches:

Maintainability

One possible benefit of the resource-owned registration approach is that it decentralizes all the code changes needed to add a resource. Meaning there's not one chunk of lines that everyone trying to add a resource at the same time is trying to modify. This removes a possibility of merge conflicts on large providers that expand rapidly.

Approachability

The resource-owned registration model is also (arguably) more approachable to new developers; it means all the code changes required to add a resource can be made in a single file, meaning provider developers don't need to remember to modify multiple files, sometimes in multiple packages, to add a resource. The concerns are all centralized in the resource itself.

On the other hand, the use of init() isn't incredibly obvious, and isn't the most common pattern in Go programs, which may prove a barrier to entry.

Global State

A major drawback of the resource-owned registration model is that it has to rely on global mutable state. The resource needs to be registered to an instance of a provider, and the provider instance it's being registered to needs to exist when that registration happens. The only way around this is if a global, mutable resource "registry" private state is kept, which can then be assigned to provider instances as the provider instances are created. But even then, the resource registry is still global mutable state.

This may have unanticipated consequences for the test framework and for any other scenario where multiple provider servers could be running in parallel.

Code Generation Friendliness

It is much easier to use code generation on a resource-owned registration model, as the registration code is isolated per-resource. The init block just needs to be added. Modifying a map is relatively more difficult, especially if you want to order the resources and data sources in any fashion. Doubly so if the values of the map could be defined inline.

Discovery

One drawback of the resource-owned registration model is that there is no longer a single, central list of resources and data sources for the provider; instead, the list is spread out over all the resource files. In theory, the provider-owned list could have registrations spread out over a number of files, as well, but inertia then is at least on the side of having them in a single, neat list.

This only matters for crude, one-off scripts that are looking to e.g. count resources in a provider without compiling it or executing the code. Arguably, the same thing is still possible (perhaps easier?) with resource-owned registration, although it needs to touch many more files.

Notes

In theory, we don't need to choose; if we choose to use the provider-owned list method, provider devs can still choose to use the resource-owned registration method:

// in provider.go
var resources map[string]framework.Resource
var resourcesMu sync.Mutex

func registerResource(name string, r framework.Resource) {
	resourcesMu.Lock()
	defer resourcesMu.Unlock()

	if resources == nil {
		resources = map[string]framework.Resource{}
	}
	resources[name] = r
}

func New() framework.Provider {
	return framework.Provider{
		Resources: resources,
	}
}

// in resource_compute_instance.go
func init() {
	registerResource("foo_compute_instance", resourceComputeInstance())
}

This defaults to the safest option, and lets the provider decide to use the less-safe (global mutable state) option. But that negates some of the benefits of approachability for new developers.

Recommendations

We went back and forth on this a lot, because we really, really like the user experience of the RegisterResource function, but its reliance on global mutable state is really scary. We felt that the RegisterResource experience would most benefit large providers with lots of concurrent contributions happening. In the end, we decided the global mutable state was too risky, and to stick wtih listing resources in a single map. However, we noted that providers could implement their own version of RegisterResource on top of that, or we could even offer a helper for it in the framework. This left us with the ability to help the providers that would benefit most from it and were okay with the global mutable state, while letting providers that weren't comfortable with the global mutable state avoid it.