Access privatestate from Configure

Since there’s no ConfigureResponse.Private, I seem to be stuck in my migration to the framework. Essentially I’m trying to allow either:

provider "my" {
    account_id = "blah"
}

or:

provider "my" {
}

resource "my_account" {
}

(where it’s either created or imported.)

But I can’t find any way of setting the account ID in resourceMyAccount.Read such that some other resourceMyOther will be able to access it - since if nothing for my_account exists in the plan then it won’t be read again on apply (so private state alone won’t cut it), and I can’t see how to get it back to ProviderData or Configure?

Am I missing something?

Hi @OJFord :wave:

In the first example you provide, account_id is a configuration value for the provider:

provider "my" {
    account_id = "blah"
}

You can make such values available to resources and data sources using the Configure() functions on the provider and resource (or data source).

For example, if you want to make “blah” available to a resource, you could configure the provider as follows:

type providerData struct {
	AccountID types.String `tfsdk:"account_id"`
}

func (p *exampleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
	var data providerData
	diags := req.Config.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	resp.ResourceData = data.AccountID
}

Then using the Configure() func within the resource can make this value available to the resource:

type exampleResource struct {
	accountID basetypes.StringValue
}

func (e *exampleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
	if accountID, ok := req.ProviderData.(basetypes.StringValue); ok {
		e.accountID = accountID
	}
}

The provider account_id should now be available to the CRUD functions within the resource as e.AccountID.

The Configure() funcs are typically used in this way in order to make provider-level data or clients available within resources and data sources. An example of setting-up a client is available in the Configure Provider Client tutorial and it’s usage can be seen in Implement Data Source and Implement Resource Create and Read. Hope this helps.

Thanks for your reply -

Sorry, I wasn’t very clear, that’s the configuration (optional account_id on provider is set) that I do have working. (When I said ‘trying to allow either x or y’, I suppose I meant ‘both x and y’ in a sense - I was thinking from the end user perspective, i.e. it should be possible to do either.)

What I’m having trouble with is when it’s not set on the provider. In this case I require a my_account resource, so the user can create the account in the first place, or import one, and then the resource, not the provider is the source of truth as it were for the account ID.

This is no problem in terraform refresh, or if the my_account resource is created in the same apply as the resources that need it, but the problem comes every subsequent apply (or a -targetted refresh) - essentially how can I propagate the ID from resourceMyAccount.Read (where it originates) to resourceMyOther.Create?

Can you refer to the account_id that is generated in the resourceMyAccount in the other resource(s) that require this account_id?

Perhaps something along the following lines:

resource "account" "example" {
  // this resource creates the account and stores the id in the state
}

resource "other" "example" {
  account_id = account.example.id
}

Could do, I’d just rather not need to (and it would be a breaking change from released versions that use sdkv2).

It did just occur to me though (actually what brought me here - to make a note for myself to try later :sweat_smile:) that perhaps I could to almost exactly that, but make it computed? Then it can be set by Read during refresh (probably no need for private state even - it will be set on the shared client aleady), but nothing else (since the resource doesn’t truly exist) so there will be a diff in the plan, then hopefully Create/Update/Delete have access to it via the req.Plan/req.State?

Although, if Read sets something, even if not the id, it’s going to be considered either a replacement or update of an existing resource isn’t it…?

Is this a publicly published provider? If so, could you share its actual identity? I think it would clarify things a lot.

From what you’ve shared so far, I get the feeling that you may be doing something that Terraform did not intend to support.

You should definitely not depend on resources being read, because -refresh=false exists to turn that off.

It actually doesn’t need read - I was getting muddled about how privatestate works - but the issue is that create/import (where it will be set) don’t have a way to pass it back to other resources’ configure (or to the provider to do so).

Sure, it’s OJFord/mullvad, I have it working at the moment (on the branch linked) with a global, just obviously I’d rather avoid that:

and then I call r.configureAccount(/*...*/) at the start of each resource CRUD function, except resourceMullvadAccount.Create and resourceMullvadAccount.ImportState which call r.setAccount directly with the ID.

I’m not sure if this even works if there are two instances of the provider declared in HCL - does terraform start two processes, or is it the same one with provider new/configure/etc. called multiple times (in which case they’d overwrite each others’ global _account)?

Crikey… so you want part of your provider configuration to be optionally sourced from the details of a resource created by that same provider.

My impression of Terraform’s architecture makes me think that doing so isn’t meant to be supported.

Looking at your master branch code:

  • Suppose a user writes a mullvad_account and a mullvad_wireguard resource, and triggers Terraform apply - what guarantees the account will be created before the wireguard resource? Isn’t this a race condition?

  • Suppose a user uses -refresh=false, which turns off resource reads. Doesn’t this break the retrieval of the credential from state?

Yes exactly. It is a bit weird, but I think both options make sense (as a user) so I wanted to support either.

I don’t particularly like the model of setting an account-level setting on every resource (as used for example by Cloudflare now) since I think that’s a property of the provider, and if you want resources across different accounts (or similar concept) then you can use differently-configured provider aliases.

I did consider having it only set on the provider, just the data source for reading the additional information, so it would always be initially created outside of Terraform. Just since it is an easy API call away it seemed neat to support having it fully contained. (Although, setting for_each or count would never work, so I might be talking myself out of this…)

It doesn’t race because it simply waits for it to be set or an auth token acquired with it (afk don’t remember the details on master exactly).

I don’t think I’ve ever used or tested -refresh=false, so yes that probably doesn’t work.

I think perhaps I just shouldn’t allow it any more. Either (as in, I need to decide which one to support):

  1. Set account on provider only, remove mullvad_account resource, but still have the data source for additional info.
  2. Account attr on every resource, set however you like, but potentially from mullvad_account.name.account_id

Or maybe both is possible actually - the attr in (2) could be computed optional, coming from the ProviderData if not explicitly set, but error if not in either - no magic ‘there is an account resource’ behaviour.

Hm. I’ll think about it some more but that seems to make more sense to me today, I was just getting hung up on trying to switch to the framework without behavioural change really - but it’s not a big deal, the behaviour is weird and it’s not like it’s a popular provider widely used in production - it was my first or second outing with terraform plugins (= also with Go) and I made a questionable decision that stuck!

I think you can still switch to framework without behavioural change.

Framework still supports the notion of setting an arbitrary interface{} Go value (that you use for the client object) in the provider and accessing it from each resource.

Framework’s way of doing that is to set the values in the provider’s Configure method, and retrieve the values in each resource or datasource’s Configure method.

func (m *maxbProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
	resp.DataSourceData = something
	resp.ResourceData = something
}

func (d *datasourceExample) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
	if req.ProviderData != nil {
		d.theThing = req.ProviderData.(*whatevertype)
	}
}

(Apparently the resource/datasource Configure methods get called passing nil sometimes… possible framework bug/design issue.)

I already do that for the client itself, but this hack is for the account ID (d.client.Config.AccountToken) configured on it, which otherwise can’t that I can see be persisted between plan and apply.

Uncanny you should mention that, the subject of my previous question on this forum!