terraform-plugin-framework icon indicating copy to clipboard operation
terraform-plugin-framework copied to clipboard

Allow accesing provider configuration in AttributePlanModifier

Open project0 opened this issue 3 years ago • 4 comments

Module version

v0.6.1

Use-cases

I want to be able to access the provider configuration in custom AttributePlanModifier, so i can set global behaviors or feature flags. Also some default values may can be set here to apply or extend.

For example tags or labels which should be added to all resources:

provider "provider" {
  default_labels = {
     owner = "unicorn"
  }
}
resource "resource" "res" {
  labels = {
    app = "tool"
  }
}

// will produce  with help of the plan modifier:
  labels = {
     app = "tool"
     owner = "unicorn"
  }

Attempted Solutions

I do not think it is currently possible at all, at least i was not able to determine how it is supposed to work.

Proposal

ModifyAttributePlanRequest provides some functionality to retrieve global provider configuration.

e.g.

type ModifyAttributePlanRequest struct {
	ProviderConfig ProviderConfig
...

References

project0 avatar Apr 26 '22 23:04 project0

Hi @Project0 👋

You should have the ability to define this type of functionality today. The Provider implementation for your Resources are available via any of the Resource methods, such as ModifyPlan, assuming that Provider type is stored in the Resource type itself. For example:

var _ tfsdk.Provider = exampleProvider{}
var _ tfsdk.ResourceType = exampleResourceType{}
var _ tfsdk.Resource = exampleResource{}
var _ tfsdk.ResourceWithModifyPlan = exampleResource{}

type exampleProvider struct{
  defaultLabels map[string]string
}

// Other Provider methods omitted for brevity
func (p exampleProvider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
  // logic to fetch from req.Config and save into p.defaultLabels
}

type exampleResourceType struct{}

// Other ResourceType methods omitted for brevity
func (t exampleResourceType) NewResource(ctx context.Context, in tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
	return exampleResource{
		provider: in.(exampleProvider), // direct type assertion shown for brevity
	}, nil
}

type exampleResource struct{
  provider exampleProvider
}

// Other Resource methods omitted for brevity
func (r exampleResource) ModifyPlan(ctx context.Context, req tfsdk.ModifyResourcePlanRequest, resp *tfsdk.ModifyResourcePlanResponse) {
  // has access to r.provider.defaultLabels
}

That being said, saving the provider configuration and providing access to it via the ModifyAttributePlanRequest is an interesting idea. There are a few things top of mind that should be considered here:

  • Provider configuration is only available during the ConfigureProvider RPC. Plan modification happens during PlanResourceChange RPCs. The framework RPC server implementation would need to cache the result across RPCs, which would be a first. It's not clear whether this is a design choice that could hurt in the future.
  • Provider configuration happens early in the Terraform operation lifecycle and can come from other sources (such as environment variables or API calls), during the ConfigureProvider RPC. Offering the provider configuration in this manner means there is a strict limitation that the configuration could only come from a Terraform configuration and not other sources.
  • Whether instead of providing the provider configuration, if the request could or should provide the tfsdk.Provider instance. This would help obviate the two above points, but could introduce other issues, such as now each attribute plan modifier implementation needing to convert the type when trying to access it.

None of these feel like immediate "yes we should do this" or "no we shouldn't do this" items, so it'll be helpful to see if others have similar use cases or have particular feelings about potential implementation details.

Thanks for raising this!

bflad avatar Apr 27 '22 20:04 bflad

@bflad I am working on a provider where I want to set the provider configuration with an environment variable if the configuration is not explicitly set. I also want to optionally specify a default value, such that if the config is not set and the env variable is empty or not set, then it uses the default value. This ensures the provider config property can never be null.

I originally implemented in the configuration function and that works as you pointed out. However, I found it less ergonomic then the api provided for a plan modifier. The configure function requires a lot of boilerplate if then else verbosity that would be removed with something like this. Note the planmodifers is a package I wrote.

	PlanModifiers: []tfsdk.AttributePlanModifier{
					planmodifiers.DefaultEnvString("envName", types.String{Value: "defaultValue"}),
				},

And this works fine for a resource. However, during my testing, I discovered that the plan modifier wasn't being called for a provider. I don't have a strong opinion on the implementation or whether a plan modifier API is the right choice here. But I would suggest that something like this would be very helpful for wiring up providers with complex configuration

In the meantime, I would suggest updating the godoc to note that a plan modifiers on a provider schema attribute are not called.

jaloren avatar Oct 17 '22 13:10 jaloren

I found it less ergonomic then the api provided for a plan modifier. The configure function requires a lot of boilerplate if then else verbosity that would be removed with something like this.

That is certainly a good callout. It does require more code currently. We can certainly think of ways to make this type of logic more declarative. We are considering whether to introduce some form of attribute "default" value which could contain this type of logic, but we need to figure out the design and semantics of that system so that it works well with other attribute functionality as well as working properly with the internal value reflection system.

In the meantime, I would suggest updating the godoc to note that a plan modifiers on a provider schema attribute are not called.

Were you looking for this documentation in a particular place outside of tfsdk.Attribute? The PlanModifiers field includes this comment:

	// Plan modification only applies to resources, not data sources or
	// providers. Setting PlanModifiers on a data source or provider attribute
	// will have no effect.

That being said, we are planning on refactoring schema definitions so data source and provider attributes cannot specify that field at all as part of #132.

bflad avatar Oct 17 '22 15:10 bflad

@bflad re: docs, you are completely correct. My apologies, my IDE had apparently truncated the godoc comment with an ellipsis and I thought the first paragraph was the only paragraph.

jaloren avatar Oct 18 '22 10:10 jaloren

Hey @bflad i just tried this approach with the latest versions, but i have some strange error what looks like a framework error to me.

So, i was successfully be able to pass the provider data to the resource with the Configure method and it is also now available in the ModifyPlan method :muscle: .

I can also read the planned value, but i fail to set an updated value as the plugin framework seems to refuse the change with the following error message (I mean, i cannot modify the plan, so what is the purpose here...):

Provider "registry.terraform.io/project0/podman" planned an invalid value for podman_network.name.labels: planned value 
cty.MapVal(map[string]cty.Value{"default":cty.StringVal("yes"), "my":cty.StringVal("value")}) 
does not match config value
cty.MapVal(map[string]cty.Value{"my":cty.StringVal("value")}) 
nor prior value cty.MapValEmpty(cty.String).

This is how i used it:

func (r *resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
	// No default labels set, skip merge
	if r.providerData.DefaultLabels.IsNull() {
		return
	}

	var labels types.Map
	resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("labels"), &labels)...)
	if resp.Diagnostics.HasError() {
		return
	}

        // its not null anymore, at least default labels are set
	labels.Null = false
        // merge labels
	for k, v := range g.providerData.DefaultLabels.Elems {
		if _, exist := labels.Elems[k]; exist {
			continue
		}
		labels.Elems[k] = v
	}
        // update the attribute with the new labels (this part fails)
	resp.Plan.SetAttribute(ctx, path.Root("labels"), &labels)
}

project0 avatar Oct 25 '22 17:10 project0

@project0 does the affected attribute have Computed: true set?

bflad avatar Oct 25 '22 17:10 bflad

@bflad Indeed, but unfortunately changing this to false does not has any effect. I also removed all attribute based modifiers without luck. I pushed into a branch so you can have a look into everything yourself ;-) : https://github.com/project0/terraform-provider-podman/blob/issue/hc/pf/306-planmodifier/internal/provider/resource.go#L44-L66

project0 avatar Oct 25 '22 17:10 project0

Ah, okay. I think I see what you're trying to do -- are you trying to setup provider-level default labels for your resources? If not please let me know, but I'll describe what I think is happening here and what you can potentially do about it.

Terraform has requirements that if an attribute value is configured (in a Terraform configuration) that the planned value must match that exact configuration value so that configuration, plan, and state values all match through the plan and apply workflows. Therefore, the configurable attribute itself cannot contain additional data from the provider-level data without causing Terraform to return that error or something similar.

Not showing provider-level data is certainly less than ideal though, so creating a "sidecar" Computed: true only attribute is a decent way to show the information during plans or drift detection. That attribute's value can be computed by merging together the provider-level data with the resource-level data and then sent with the API requests.

To ensure that the resource captures missing or updated labels to show plan differences, the configurable attribute can take the API response with all labels and subtract out the provider-level data.

To see how this sort of setup works in a sdk/v2 based resource, the AWS provider supports provider-level default tag values across many resources (e.g. search for tags versus tags_all in code such as https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/service/codebuild/project.go).

bflad avatar Oct 25 '22 18:10 bflad

Thanks for the explanation @bflad! Yes, thats exactly my use case :D. I was already aware how the aws plugin is doing it, but thought it will behave different in the framework. I will skip it for now in my setup as it has no real prio, i was just curious if there is a easy way to accomplish this.

As of the original issue, i guess we can close it as there is nowadays a good way to retrieve the provider data via Configure:

data, ok := req.ProviderData.(providerDataModel)

project0 avatar Oct 25 '22 18:10 project0

Yeah, unfortunately the attribute value constraints are there due to Terraform core, not a difference between the provider frameworks. In general, I'd expect more Terraform errors when doing "incorrect" things with framework providers versus sdk providers since Terraform has special flags to workaround some of the errant sdk behaviors since Terraform 0.12 was introduced.

If you feel inclined, you can certainly raise a feature request upstream to discuss potentially making this sort of provider implementation easier for providers. Thanks for raising this!

bflad avatar Oct 25 '22 18:10 bflad

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

github-actions[bot] avatar Nov 25 '22 02:11 github-actions[bot]