mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> Co-authored-by: Janos <86970079+janosdebugs@users.noreply.github.com> Co-authored-by: James Humphries <James@james-humphries.co.uk> Co-authored-by: Ronny Orot <ronny.orot@gmail.com> Co-authored-by: Oleksandr Levchenkov <ollevche@gmail.com>
This commit is contained in:
parent
874483182b
commit
ed0c761b0e
354
docs/provider-references.md
Normal file
354
docs/provider-references.md
Normal file
@ -0,0 +1,354 @@
|
||||
# Provider References Through the OpenTofu Language, Codebase and State
|
||||
|
||||
The concept of Providers has changed and evolved over the lifetime of OpenTofu, with many of the legacy configuration options still supported today. This document aims to walk through examples and map them to structures within OpenTofu's code.
|
||||
|
||||
## Existing Documentation
|
||||
|
||||
It is recommended that you have the following open when reading through the rest of this document:
|
||||
* https://opentofu.org/docs/language/providers/
|
||||
* https://opentofu.org/docs/language/providers/requirements/
|
||||
* https://opentofu.org/docs/language/providers/configuration/
|
||||
|
||||
## What is a Provider?
|
||||
|
||||
In general terms, a provider is a piece of code which interfaces OpenTofu with resources. For example, the AWS provider describes what resources it is able to read/manage, such as s3 buckets and ec2 instances.
|
||||
|
||||
In most cases providers live in a registry, are downloaded into the local path, and executed to provide a versioned GRPC server to OpenTofu. They could potentially be dynamically loaded directly into the running OpenTofu application, but a distinct process helps with fault tolerance and potential isolation issues.
|
||||
|
||||
Providers also may define functions that can be called from the OpenTofu configuration. See [Provider Functions](#Provider-Functions) below for more information.
|
||||
|
||||
It is HIGHLY recommended to vet all providers you execute locally as they are not sandboxed at all. There are discussions ongoing on how to improve safety in that respect.
|
||||
|
||||
Providers also may be configured with values in a HCL block. This allows the provider have some "global" configuration that does not need to be passed in every resource/data instance, a common example being credentials.
|
||||
|
||||
|
||||
## Language References
|
||||
|
||||
### History and Addressing of Providers
|
||||
|
||||
Provider references and configuration have an interesting history, which leads to the system we have today. Note: some of this history has been summarized or omitted for clarity.
|
||||
|
||||
#### Provider Type
|
||||
|
||||
Prior to v0.10.0, providers were built directly into the binary and not released/versioned separately. They only had a single identifier, which we now call "Provider Type".
|
||||
|
||||
Example:
|
||||
```hcl
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
}
|
||||
```
|
||||
|
||||
This requires the `addrs.Provider{Type = "aws"}` provider, gives it some configuration, and then creates a `s3_bucket` with it due to the type prefix of `aws`. This provider also is referenceable via `addrs.LocalProviderConfig{LocalName = "aws"}`. Note: the Type and LocalName used to be the same field. These are distinct concepts that diverge in later examples.
|
||||
|
||||
|
||||
#### Provider Alias
|
||||
|
||||
You also may need to have multiple configurations of the "aws" provider, perhaps with different credentials or regions. These configurations are distinguished by "Provider Alias".
|
||||
|
||||
|
||||
Example:
|
||||
```hcl
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = aws.default
|
||||
}
|
||||
```
|
||||
|
||||
As with the previous example, this requires the `addrs.Provider{Type = "aws"}` provider, gives it some configuration under the alias "default". The `s3_bucket` resource now refers to the provider explicitly via `addrs.LocalProviderConfig{LocalName = "aws", Alias = "default"}`. Note: the `addrs.Provider{Type = "aws"}` reference is still partially used due to some odd legacy interactions.
|
||||
|
||||
|
||||
#### Provider Versions
|
||||
|
||||
Since v0.10.0, providers are distributed via a registry. This allows provider versions to be decoupled from the main application version. Provider bugfixes and new features can be released independently of the main application. All providers/configs with the same `addrs.Provider` must use the same binary and must have compatible version constraints.
|
||||
|
||||
```hcl
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
version = 0.124 # Deprecated by required_providers
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = aws.default
|
||||
}
|
||||
```
|
||||
|
||||
The result is identical to the previous case, except now the version constraint is tracked in the `config.Module` structure, with `addrs.Provider{Type = "aws"}` as the key. Once all constraints are known, `tofu init` downloads the providers from the registry into a local cache for later execution.
|
||||
|
||||
#### Module Provider References
|
||||
|
||||
Prior to 0.11.0, modules would share/override provider configurations. There was no distinction between configuration of parent or child module's providers. This implicit inheritance caused a variety of issues and limitations. The `module -> providers` map field was introduced to allow explicit passing of provider configurations to child modules.
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
version = 0.124 # Deprecated by required_providers
|
||||
}
|
||||
|
||||
module "my_mod" {
|
||||
source = "./mod"
|
||||
# Only the "unaliased" providers are passed if this is omitted.
|
||||
providers = {
|
||||
aws = aws.default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./mod/mod.tf
|
||||
provider "aws" { # Deprecated by required_providers
|
||||
version = ">= 0.1" # Deprecated by required_providers
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = aws
|
||||
}
|
||||
```
|
||||
|
||||
In the root module (main configuration), we require the `addrs.Provider{Type = "aws"}` with a version constraint of "0.124". A configuration for that provider exists at `addrs.LocalProviderConfig{LocalName = "aws", Alias = "default"}` within the root module and is not automatically accessible from the child module. A new reference is introduced, which can be used globally: `addrs.AbsProviderConfig{Module: Root, Provider: addrs.Provider{Type = "aws"}, Alias = "default"}`.
|
||||
|
||||
The child module is passed the `addrs.AbsProviderConfig` and is internally referenceable within the module under `addrs.LocalProviderConfig{LocalName: "aws"}`. That global configuration is copied and merged with the configuration within that module, which in this case adds an additional version constraint.
|
||||
|
||||
Within that module, `addrs.LocalProviderConfig{LocalName: "aws"}` now refers to `addrs.Provider{Type = "aws"}` and the merged configuration for that provider.
|
||||
|
||||
If multiple instances of the same provider are needed, the alias can be provided in the module's "providers" block
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
version = 0.124 # Deprecated by required_providers
|
||||
}
|
||||
|
||||
module "my_mod" {
|
||||
source = "./mod"
|
||||
# Only the "unaliased" providers are passed if this is omitted.
|
||||
providers = {
|
||||
aws.foo = aws.default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./mod/mod.tf
|
||||
provider "aws" { # Deprecated by required_providers
|
||||
version = ">= 0.1" # Deprecated by required_providers
|
||||
alias = "foo"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = aws.foo
|
||||
}
|
||||
```
|
||||
|
||||
The root module's explanation is nearly identical, the primary change is to the addressing in the child module.
|
||||
|
||||
The child module is passed the `addrs.AbsProviderConfig` and is internally referenceable within the module under `addrs.LocalProviderConfig{LocalName: "aws", Alias = "foo"}`. That global configuration is copied and merged with the configuration within that module, which in this case adds an additional version constraint.
|
||||
|
||||
Within that module, `addrs.LocalProviderConfig{LocalName: "aws", Alias = "foo"}` now refers to `addrs.Provider{Type = "aws"}` and the merged configuration for that provider.
|
||||
|
||||
#### Required Providers (Legacy)
|
||||
|
||||
With the change in 0.11.0 adding the `providers` field, it is still unclear when a child module's provider is "incorrectly configured" or if the parent module has forgotten an entry in the `providers` field.
|
||||
|
||||
To solve this, `terraform -> required_providers` was introduced. The initial version of this feature was a direct mapping between "Provider Type" and "Provider Version Constraint".
|
||||
|
||||
```
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = "0.124"
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
}
|
||||
|
||||
module "my_mod" {
|
||||
source = "./mod"
|
||||
providers = {
|
||||
aws = aws.default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./mod/mod.tf
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = ">= 0.1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = aws
|
||||
}
|
||||
```
|
||||
|
||||
The references are unchanged, except for the dependencies are now more explicit. This form of `required_providers` is no longer supported.
|
||||
|
||||
|
||||
#### Provider Names / Namespaces / Registries
|
||||
|
||||
Other organizations started to create providers over time (along with their own registries) and the concept of referencing a provider needed to be expanded. In v0.13.0 the concept of `addrs.Provider` was expanded to include `Namespace` and `Hostname`.
|
||||
|
||||
Previously, all providers within the registry had global names "aws", "datadog", "gcp", etc... As forks were introduced and the authoring of providers took off, the `Namespace` concept was introduced. It usually maps to the GitHub user/org that owns it, but it is not a strict requirement (especially in third-party registries).
|
||||
|
||||
Organizations also wanted more control over their providers for both development and security purposes. The providers registry hostname was included in the spec.
|
||||
|
||||
Additionally, the previous understanding of "datadog" may refer to "datadog/datadog" or "user/datadog" and is unclear if they are both included in the project. By decoupling `addrs.Provider.Type` and `addrs.LocalProviderConfig.LocalName`, both could be used in the same module under different names. Additionally the same concept can be used to have the LocalName "datadog" refer to "user/datadog-fork" without having to rewrite the whole project's config.
|
||||
|
||||
|
||||
```
|
||||
terraform {
|
||||
required_providers {
|
||||
awsname = { # name added for clarity, usually Type == LocalName
|
||||
#source = "aws"
|
||||
#source = "hashicorp/aws"
|
||||
source = "registry.opentofu.org/hashicorp/aws"
|
||||
version = "0.124"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "awsname" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
}
|
||||
|
||||
module "my_mod" {
|
||||
source = "./mod"
|
||||
providers = {
|
||||
modaws = awsname.default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./mod/mod.tf
|
||||
terraform {
|
||||
required_providers {
|
||||
modaws = {
|
||||
source = "aws"
|
||||
version = ">= 0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "aws_s3_bucket" "foo" {
|
||||
bucket_name = "foo"
|
||||
provider = modaws
|
||||
}
|
||||
```
|
||||
|
||||
The required_providers "source" field in the root module decomposed into `addrs.Provider{Type="aws", Namespace="hashicorp", Hostname="registry.opentofu.org"}`. As the default namespace is "hashicorp" and the default hostname is "registry.opentofu.org", we will continue to use the shorthand `addrs.Provider{Type="aws"}`. Next `addrs.LocalProviderConfig{LocalName: "aws_name"}` is created and within the root module maps to `addrs.Provider{Type="aws"}`. This provider local name is then used in all subsequent references within the root module. The configuration is then mapped to `addrs.AbsProviderConfig{Module: Root, Provider: addrs.Provider{Type = "aws"}, Alias = "default"}` globally.
|
||||
|
||||
The child module is passed the `addrs.AbsProviderConfig` and is internally referenceable within the module under `addrs.LocalProviderConfig{LocalName: "modaws"}`.
|
||||
|
||||
Within that module, `addrs.LocalProviderConfig{LocalName: "modaws"}` now points at `addrs.Provider{Type = "aws"}` and is effectively replaced with `addrs.AbsProviderConfig{Module: Root, Provider: addrs.Provider{Type = "aws"}, Alias = "default"}` at runtime. This optimizes running as few provider instances as possible.
|
||||
|
||||
If a new provider configuration were added to the module:
|
||||
```
|
||||
provider "modaws" {
|
||||
region = "us-west-2"
|
||||
}
|
||||
```
|
||||
This would negate the override / deduplication above and result in `addrs.AbsProviderConfig{Module: MyMod, Provider: addrs.Provider{Type = "aws"}}`.
|
||||
|
||||
#### Multiple Provider Aliases
|
||||
|
||||
Multiple provider aliases can be supplied in required_providers via `configuration_aliases`. This requires that a caller of the module provide the requested aliases explicitly.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
terraform {
|
||||
required_providers {
|
||||
awsname = { # name added for clarity, usually Type == LocalName
|
||||
source = "registry.opentofu.org/hashicorp/aws"
|
||||
version = "0.124"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "awsname" {
|
||||
region = "us-east-1"
|
||||
alias = "default"
|
||||
}
|
||||
|
||||
module "my_mod" {
|
||||
source = "./mod"
|
||||
providers = {
|
||||
modaws.foo = awsname.default
|
||||
modaws.bar = awsname
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./mod/mod.tf
|
||||
terraform {
|
||||
required_providers {
|
||||
modaws = {
|
||||
source = "aws"
|
||||
version = ">= 0.1"
|
||||
configuration_aliases = [ modaws.foo, modaws.bar ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Representation in State
|
||||
|
||||
Resources in the state file note the `addrs.Provider` required to modify them. Due to the structure, all instances (for_each/count) of a resource must use the same provider.
|
||||
|
||||
Note: This section should be expanded with examples.
|
||||
|
||||
Note: `tofu show -json` and the internal statefile format are different and do not always line up one-to-one.
|
||||
|
||||
## Provider Workflow
|
||||
|
||||
When `config.Module` is built from `config.Files`, each module maintains:
|
||||
* ProviderConfigs: map of `provider_name.provider_alias -> config.Provider` from provider config blocks in the parsed config
|
||||
* ProviderRequirements: map of `provider_local_name -> config.RequiredProvider` from `terraform -> required_providers`
|
||||
* ProviderLocalNames: map of `addrs.Provider -> provider_name`
|
||||
|
||||
The full list of required provider types is collated, downloaded, hashed and cached in the .terraform directory during init.
|
||||
|
||||
Providers are then added to the graph in a few transformers:
|
||||
* ProviderConfigTransformer: Adds configured providers to the graph
|
||||
* MissingProviderTransformer: Adds unconfigured but required providers to the graph
|
||||
* ProviderTransformer: Links provider nodes to self reported nodes that require them
|
||||
* ProviderFunctionTransformer: Links provider nodes to other nodes by inspecting their "OpenTofu Function References"
|
||||
* ProviderPruneTransformer: Removes provider nodes that are not in use by other nodes
|
||||
|
||||
Providers are then managed and scoped by the EvalContextBuiltin where the actual `provider.Interface`s are created and attached to resources.
|
||||
|
||||
|
||||
## Provider Functions
|
||||
|
||||
Providers also may supply functions, either unconfigured or configured.
|
||||
* `providers::aws::arn_parse(var.arn)`
|
||||
* `providers::aws::us::arn_parse(var.arn)`
|
356
rfc/20240513-static-evaluation-providers.md
Normal file
356
rfc/20240513-static-evaluation-providers.md
Normal file
@ -0,0 +1,356 @@
|
||||
# Static Evaluation of Provider Iteration
|
||||
|
||||
Issue: https://github.com/opentofu/opentofu/issues/300
|
||||
|
||||
Since the introduction of for_each/count, users have been trying to use each/count in provider configurations and resource/module mappings. Providers are a special case throughout OpenTofu and interacting with them either as a user or developer requires significant care.
|
||||
|
||||
> [!Note]
|
||||
> Please read [Provider References](../docs/provider-references.md) before diving into this section! This document uses the same terminology and formatting.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
The approach proposed in the [Static Evaluation RFC](20240513-static-evaluation.md) can be extended to support provider for_each/count with some clever code and lots of testing. It is assumed that the reader has gone through the Static Evaluation RFC thoroughly before continuing here.
|
||||
|
||||
### User Documentation
|
||||
|
||||
#### Provider Configuration Expansion
|
||||
|
||||
How are multiple configured providers of the same type used today?
|
||||
```hcl
|
||||
provider "aws" {
|
||||
alias = "us"
|
||||
region = "us-east-1"
|
||||
}
|
||||
// Copy pasted from above provider, with minor changes
|
||||
provider "aws" {
|
||||
alias = "eu"
|
||||
region = "eu-west-1"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "primary_us" {
|
||||
provider = aws.us
|
||||
}
|
||||
// Copy pasted from above resource, with minor changes
|
||||
resource "aws_s3_bucket" "primary_eu" {
|
||||
provider = aws.eu
|
||||
}
|
||||
|
||||
module "mod_us" {
|
||||
source = "./mod"
|
||||
providers {
|
||||
aws = aws.us
|
||||
}
|
||||
}
|
||||
// Copy pasted from above module, with minor changes
|
||||
module "mod_eu" {
|
||||
source = "./mod"
|
||||
providers {
|
||||
aws = aws.eu
|
||||
}
|
||||
}
|
||||
```
|
||||
For scenarios where you wish to use the same pattern with multiple providers, copy-paste or multiple workspaces is the only path available. Any copy-paste like this can easily introduce errors and bugs and should be avoided. For this reason, users have been asking to use for_each and count in providers for a long time.
|
||||
|
||||
Let's run through what it would look like to enable this workflow.
|
||||
|
||||
What is expected when a user adds for_each/count to a provider configuration:
|
||||
```hcl
|
||||
locals {
|
||||
regions = {"us": "us-east-1", "eu": "eu-west-1"}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
for_each = local.regions
|
||||
region = each.value
|
||||
}
|
||||
```
|
||||
|
||||
At first glance, this looks fairly straightforward. Following the rules in place with resources, we would expect `aws["us"]` and `aws["eu"]` to be valid.
|
||||
|
||||
What happens if you have another aws provider with the alias of `"us"` (`aws.us`)? That would be incredibly confusing to end users. Therefore, we should consider that provider "indices" and provider aliases are identical concepts. In that previous example both `aws["us"]` and `aws.us` would both be defined.
|
||||
|
||||
|
||||
Providers already have an alias field, how will this interact with for_each/count?
|
||||
```hcl
|
||||
provider "aws" {
|
||||
for_each = local.regions
|
||||
alias = "HARDCODED"
|
||||
region = each.value
|
||||
}
|
||||
```
|
||||
|
||||
This would produce two provider configurations: `aws.HARDCODED` and `aws.HARDCODED`, an obvious conflict. We could try to be smart and try to make sure that the keys are not identical and in some way derived from the iteration. That approach is complex, likely error prone, and confusing to the user. Instead, we should not allow users to provide the alias field when for_each/count are present.
|
||||
|
||||
|
||||
What if a user tries to use a resource output in a provider for_each? In an ideal world, this would be allowed as long as there is not a cyclic dependency. However, due to the way providers are resolved deep in OpenTofu this would require a near rewrite of the core provider logic. Here we rely on the ideas set forth in the [Static Evaluation RFC](20240513-static-evaluation.md) and only allow values known during configuration processing (locals/vars). Anything dynamic like resources/data will be forbidden for now.
|
||||
|
||||
|
||||
With the provider references clarified, we can now use the providers defined above in resources:
|
||||
|
||||
```hcl
|
||||
resource "aws_s3_bucket" "primary" {
|
||||
for_each = local.regions
|
||||
provider = aws.us # Uses the existing reference format
|
||||
}
|
||||
|
||||
locals {
|
||||
region = "eu"
|
||||
}
|
||||
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
providers {
|
||||
aws = aws[local.region] # Uses the new reference format.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Provider Alias Mappings
|
||||
|
||||
Now that we can reference providers via variables, how should this interact with for_each / count in resources and modules?
|
||||
|
||||
```hcl
|
||||
locals {
|
||||
regions = {"us": "us-east-1", "eu": "eu-west-1"}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
for_each = local.regions
|
||||
region = each.value
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "primary" {
|
||||
for_each = local.regions
|
||||
provider = aws[each.key]
|
||||
}
|
||||
|
||||
module "mod" {
|
||||
for_each = local.regions
|
||||
source = "./mod"
|
||||
providers {
|
||||
aws = aws[each.key]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
From a user perspective, this provider mapping is fairly simple to understand. As we will show in the Technical Approach, this will be quite difficult to implement.
|
||||
|
||||
|
||||
#### What's not currently allowed
|
||||
|
||||
There are scenarios that users might think could work, but we don't want to support at this time.
|
||||
|
||||
|
||||
Storing a provider reference in a local or passing it as a variable
|
||||
```hcl
|
||||
locals {
|
||||
my_provider = aws["us"]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "primary" {
|
||||
provider = local.my_provider
|
||||
}
|
||||
```
|
||||
It's not well defined what a "provider reference" is outside of a "provider/providers" block. All of the provider references are built as special cases that are handled beside the code and not as part of it directly.
|
||||
|
||||
|
||||
Using the "splat/*" operator in module providers block:
|
||||
```hcl
|
||||
module "mod" {
|
||||
source = "./mod"
|
||||
providers {
|
||||
aws[*] = aws[*]
|
||||
}
|
||||
}
|
||||
```
|
||||
This implies that all aliased aws providers would be passed into the child module. This is confusing as it conflicts with the existing idea of providers configs passed implicitly to child modules with the same names.
|
||||
|
||||
### Technical Approach
|
||||
|
||||
#### Provider Configuration Expansion
|
||||
|
||||
##### Expansion
|
||||
|
||||
Expanding provider configurations can be done using the StaticContext available in [configs.NewModule()](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L122) as defined in the Static Evaluation RFC.
|
||||
|
||||
At the end of the NewModule constructor, the configured provider's aliases can be expanded using the each/count, similar to how [module.ProviderLocalNames](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L186) is generated. This does not require any special workarounds and will resemble most other applications of the StaticContext.
|
||||
|
||||
```go
|
||||
// Pseudocode
|
||||
func NewModule(files []configs.File, ctx StaticContext) {
|
||||
module = &Module{}
|
||||
for _, file := range files {
|
||||
module.Append(file)
|
||||
}
|
||||
|
||||
ctx.AddVariables(module.Variables)
|
||||
ctx.AddLocals(module.Locals)
|
||||
|
||||
// Re-build the ProviderConfigs map with expansion
|
||||
newProviderConfigs := make(map)
|
||||
for _, cfg := range module.ProviderConfigs {
|
||||
for key, expanded := range cfg.expand(ctx) {
|
||||
newProviderConfigs[key] = expanded
|
||||
}
|
||||
}
|
||||
module.ProviderConfigs = newProviderConfigs
|
||||
|
||||
|
||||
// Generate the FQN -> LocalProviderName map
|
||||
module.ProviderLocalNames()
|
||||
}
|
||||
```
|
||||
|
||||
New validation rules will be added per the User Documentation above, all of which closely resemble existing checks and do not require engineering discussion.
|
||||
|
||||
#### Provider Alias Mappings
|
||||
|
||||
The following understanding of the current OpenTofu code may be incorrect or incomplete. It is a mix of legacy patterns and fallbacks that is hard to reason about. It is based on [Provider References](../docs/provider-references.md#Provider-Workflow)
|
||||
|
||||
Providers and their aliases are:
|
||||
* Fully known at init/config time
|
||||
* Added into the graph via ProviderTransformers
|
||||
* Attached to *Unexpanded* modules/resources in the graph
|
||||
* Linked to *Unexpanded* resources in the graph.
|
||||
|
||||
Let's deconstruct each of these challenges individually:
|
||||
|
||||
##### Providers through Init/Configuration:
|
||||
|
||||
Each `configs.Module` contains fields which define how the module understands it's local provider references and provider configurations. As defined in Expansion above, we can use the StaticContext to fully build out these maps.
|
||||
|
||||
The next piece of data that's important to the config layer is: which `addrs.LocalProviderConfig` (and therefore `addrs.AbsProviderConfig`) a resource or module requires. The entire config system is (currently) blissfully unaware that instances of modules and resources may want different configurations of the same provider.
|
||||
|
||||
Resources and Modules can be queried about which provider they reference. Due to legacy / implicit reasons, it is a bit of a complex question. The Resource and Module structures contain `configs.ProviderConfigRefs`, which is a config-friendly version of `addrs.LocalProviderConfig` (includes ranges).
|
||||
|
||||
Resolving these provider references is a bit tricky and is a split responsibility between the ProviderTransformers in the graph and [validateProviderConfigs](https://github.com/opentofu/opentofu/blob/main/internal/configs/provider_validation.go#L292). This will require surgical changes to how provider configs are tracked between modules. Different approaches will need to be evaluated by the team and prototyped, likely with some refactoring beforehand.
|
||||
|
||||
##### Providers in the graph
|
||||
|
||||
As previously mentioned, the ProviderTransformers are tasked with inspecting the providers defined in configuration and attaching them to resources.
|
||||
|
||||
They inspect all nodes in the graph that say "Hey I require a Provider!" AKA `tofu.GraphNodeProviderConsumer`. This interface allows nodes to specify a single provider that they require to be configured (if applicable) before being evaluated.
|
||||
|
||||
As mentioned before, one of the key issues is that *unexpanded* resources don't currently understand the concept of different provider aliases for the instances they will expand into. With the "Instance Key" -> "Provider Alias" map created in the previous section, the unexpanded resource can now understand and needs to express that each instance depends on a specific aliased `addrs.AbsProviderConfig` (all with the same `addrs.Provider`). By changing the `tofu.GraphNodeProviderConsumer` to allow dependencies on a set of providers instead of a single provider, all of the required configured providers will be initialized before any instances are executed.
|
||||
|
||||
The unexpanded resource depending on all required configured provider nodes is critical for two reasons:
|
||||
* The provider transformers try to identify unused providers and remove them from the graph. This happens pre-expansion before the instanced links are established.
|
||||
* A core assumption of the code is that expanded instances depend on a subset of references/providers that the unexpanded nodes do.
|
||||
|
||||
|
||||
That's a lot to take in, a diagram may be clearer.
|
||||
|
||||
Pre-Expansion:
|
||||
```mermaid
|
||||
graph LR;
|
||||
configs.Resource-.->tofu.Resource;
|
||||
tofu.Resource-->tofu.Provider.A;
|
||||
tofu.Resource-->tofu.Provider.B;
|
||||
```
|
||||
|
||||
Post-Expansion:
|
||||
```mermaid
|
||||
graph LR;
|
||||
configs.Resource-.->tofu.Resource;
|
||||
tofu.Resource-->tofu.ResourceInstance.A;
|
||||
tofu.Resource-->tofu.ResourceInstance.B;
|
||||
tofu.ResourceInstance.A -->tofu.Provider.A;
|
||||
tofu.ResourceInstance.B -->tofu.Provider.B;
|
||||
```
|
||||
That shows that although each tofu.ResourceInstance depends on a single provider alias, to evaluate and expand the tofu.Resource, all of the providers required for the instances must already be initialized and configured.
|
||||
|
||||
It is also worth noting that for "tofu validate" (also run pre plan/apply), the graph is walked *unexpanded* but still uses the providers to validate the config schema. Although we are allowing different "Configured Provider Instances" for each ResourceInstance, the actual "addrs.Provider" must remain the same and therefore will all use the same provider schema.
|
||||
|
||||
Although complex, this approach builds on top of and expands existing structures within the OpenTofu codebase. It is evolutionary, not revolutionary as compared to [Static Module Expansion](20240513-static-evaluation/module-expansion.md).
|
||||
|
||||
##### Providers in the State
|
||||
|
||||
This is the last "tall pole" to knock down for instanced provider alias mapping.
|
||||
|
||||
The state file is currently formatted in such a way that the `addrs.AbsProviderConfig` of an *unexpanded* resource is serialized and attached to the resource itself and not each instance.
|
||||
|
||||
Example (with relevant fields only):
|
||||
```json
|
||||
{
|
||||
"module": "module.dummy",
|
||||
"type": "tfcoremock_simple_resource",
|
||||
"name": "resource",
|
||||
"provider": "module.dummy.provider[\"registry.opentofu.org/hashicorp/tfcoremock\"].alias",
|
||||
"instances": [
|
||||
{
|
||||
"index_key": "first"
|
||||
},
|
||||
{
|
||||
"index_key": "second"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The simplest path forward is to use an un-aliased value for the provider if multiple aliases for the instances are detected. The provider could then be overridden by each instance.
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "module.dummy",
|
||||
"type": "tfcoremock_simple_resource",
|
||||
"name": "resource",
|
||||
"provider": "module.dummy.provider[\"registry.opentofu.org/hashicorp/tfcoremock\"]",
|
||||
"instances": [
|
||||
{
|
||||
"index_key": "first",
|
||||
"provider": "module.dummy.provider[\"registry.opentofu.org/hashicorp/tfcoremock\"].firstalias"
|
||||
},
|
||||
{
|
||||
"index_key": "second",
|
||||
"provider": "module.dummy.provider[\"registry.opentofu.org/hashicorp/tfcoremock\"].secondalias"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Inspecting the history of the state file versioning, this fits well within changes allowed in the same version. This would break compatibility for downgrading to older OpenTofu versions and should be noted in the changelog. It is possible to migrate back to not using provider iteration, but that will need to be applied before any downgrade could occur.
|
||||
|
||||
Note: This only applies to the state file, `tofu state show -json` has it's own format which is not impacted by this change.
|
||||
|
||||
### Open Questions
|
||||
|
||||
Should variables be allowed in required_providers now or in the future? Could help with versioning / swapping out for testing?
|
||||
```hcl
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = var.aws_version
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Initial discussion with Core Team suggests that this should be moved to it's own issue as it would be useful, but does not impact the work done in this issue.
|
||||
|
||||
|
||||
There's also an ongoing discussion on allowing variable names in provider aliases.
|
||||
|
||||
Example:
|
||||
```hcl
|
||||
# Why would you want to do this? It looks like terraform deprecated this some time after 0.11.
|
||||
provider "aws" {
|
||||
alias = var.foo
|
||||
}
|
||||
```
|
||||
Are there any downsides to supporting this?
|
||||
|
||||
### Future Considerations
|
||||
|
||||
If we ever decide to implement Static Module Expansion, how will that interact with the work proposed in this RFC?
|
||||
|
||||
## Potential Alternatives
|
||||
|
||||
Go the route of expanded modules/resources as detailed in [Static Module Expansion](20240513-static-evaluation/module-expansion.md)
|
||||
- Concept has been explored for modules
|
||||
- Not yet explored for resources
|
||||
- Massive development and testing effort!
|
||||
|
543
rfc/20240513-static-evaluation.md
Normal file
543
rfc/20240513-static-evaluation.md
Normal file
@ -0,0 +1,543 @@
|
||||
# Init-time static evaluation of constant variables and locals
|
||||
|
||||
Issue: https://github.com/OpenTofu/OpenTofu/issues/1042
|
||||
|
||||
As initially described in https://github.com/opentofu/opentofu/issues/1042, many users of OpenTofu expect to be able to use variables and locals in a variety of locations that are currently not supported. Common examples include but are not limited to: module sources, provider assignments, backend configuration, encryption configuration.
|
||||
|
||||
All of these examples are pieces of information that need to be known during `tofu init` and can not be integrated into the usual `tofu plan/apply` system. Init sets up a static understanding of the project's configuration that plan/apply can later operate on.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
In simple terms, the proposal is to enhance the configuration processing in OpenTofu to support evaluation of variable and local references that do not depend on any dynamic information (resources/data/providers/etc...).
|
||||
|
||||
### User Documentation
|
||||
|
||||
This proposal in and of itself does not explicitly add support for new user facing functionality in OpenTofu. It is designed to be a support system for the examples above.
|
||||
|
||||
However, this "support system" will have to interact with the user directly. To properly define this support system, we will look at specific examples that will be built on top of this work.
|
||||
|
||||
Note: All errors/warnings are simplified placeholders and would include better wording/formatting as well as source locations.
|
||||
|
||||
#### Configurable Backend Examples
|
||||
|
||||
Let's first look at a project's backend that depends on both variables and locals:
|
||||
|
||||
```hcl
|
||||
variable "key" {
|
||||
type = string
|
||||
}
|
||||
|
||||
locals {
|
||||
region = "us-east-1"
|
||||
key_check = md5sum(var.key)
|
||||
}
|
||||
|
||||
terraform {
|
||||
backend "somebackend" {
|
||||
region = local.region
|
||||
key = var.key
|
||||
key_check = local.key_check
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When first running `tofu init`, two errors will be produced:
|
||||
1. terraform.backend.key requires variable "key" to be provided
|
||||
2. terraform.backend.key_check requires local "key_check", which requires variable "key" to be provided
|
||||
|
||||
The variable "key" can either be provided with via a `terraform.tfvars` file or a cli flag `-var "key=somevalue"`. This implies that the `-var` cli flag will need to be added to most OpenTofu commands.
|
||||
|
||||
> [!NOTE]
|
||||
> Instead of producing an error in this scenario, we could instead ask the user to provide values for the required variables. This already occurs as part of the provider configuration process. We could unify that process as well to reduce code duplication and [odd workarounds](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/context_input.go#L22-L48).
|
||||
|
||||
Let's now consider what happens when `tofu apply` is run. If `terraform.tfvars` or `-var "key="` have changed, the backend configuration will no longer match the configuration during `tofu init` and will return an error to the user. This will require users to be considerate of what vars they are allowing in backends and how they are managed as a team. That said, there are clear guide-rails already in place for most scenarios where configuration does not match expectation.
|
||||
|
||||
As shown by users looking to use values in backends today, someone will eventually try to use a "dynamic" value (resource/data) in a backend configuration:
|
||||
|
||||
```hcl
|
||||
resource "mycloud_account" {
|
||||
}
|
||||
|
||||
locals {
|
||||
account_id = mycloud.account.id
|
||||
}
|
||||
|
||||
terraform {
|
||||
backend "somebackend" {
|
||||
account_id = local.account_id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this scenario, the following error message will be produced:
|
||||
* terraform.backend.account_id is unable to be resolved
|
||||
- The field "account_id" depends on local "account_id", which depends on resource "mycloud.account" which is not allowed here.
|
||||
|
||||
These examples are designed to convey the importance of reference tracking when explaining to users clearly why something they are attempting to do is not allowed.
|
||||
|
||||
#### Module Sources Example
|
||||
|
||||
Modules are a complex concept that static evaluation will need to interact with. We will need to define how users interact with them and how to convey errors and limitations encountered.
|
||||
|
||||
First, let's look at a simple example that does not involve any child modules:
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "helper" {
|
||||
source = "git@github.com:org/my-utils?ref=${var.version}"
|
||||
}
|
||||
```
|
||||
|
||||
This will be subject to the same type of error messages defined above in the backend examples.
|
||||
|
||||
Next, let's look at a more complex example with child module sources:
|
||||
```hcl
|
||||
# main.tf
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "common_first" {
|
||||
source = "./common"
|
||||
version = var.version
|
||||
region = "us-east-1"
|
||||
}
|
||||
module "common_second" {
|
||||
source = "./common"
|
||||
version = var.version
|
||||
region = "us-east-2"
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# ./common/main.tf
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
variable "region" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "helper" {
|
||||
source = "git@github.com:org/my-utils?ref=${var.version}"
|
||||
region = var.region
|
||||
}
|
||||
```
|
||||
|
||||
This now adds an additional dimension to reference errors. Without the variable "version" supplied, the following errors will occur:
|
||||
* Module common_first.helper's source can not be determined
|
||||
- common_first.helper.source depends on variable common_first.version, which depends on variable "version" which has not been supplied
|
||||
* Module common_second.helper's source can not be determined
|
||||
- common_second.helper.source depends on variable common_second.version, which depends on variable "version" which has not been supplied
|
||||
|
||||
Once a value for "version" is provided, everything should work as intended.
|
||||
|
||||
This configuration can be cleaned up a bit further using `for_each`:
|
||||
```hcl
|
||||
# main.tf
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "common" {
|
||||
for_each = {first = "us-east-1", second = "us-east-2"}
|
||||
source = "./common"
|
||||
version = var.version
|
||||
region = each.value
|
||||
}
|
||||
```
|
||||
|
||||
This would produce a single error instead of multiple:
|
||||
* Module common.helper's source can not be determined
|
||||
- common.helper.source depends on variable "common.version", which depends on variable "version" which has not been supplied
|
||||
|
||||
Notice that the for_each does not interact with the static reference system at all.
|
||||
|
||||
This begs the question, what if a user tries to use each.value in a field that must be statically known?
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
variable "version" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "common" {
|
||||
for_each = {first = "us-east-1", second = "us-east-2"}
|
||||
source = "./common"
|
||||
version = "${var.version}_${each.key}"
|
||||
region = each.value
|
||||
}
|
||||
```
|
||||
|
||||
This will produce the following error, regardless of the value of the variable "version":
|
||||
* Module common.helper's source can not be determined
|
||||
- common.helper.source depends on variable "common.version", which depends on a for_each key and is forbidden here!
|
||||
|
||||
There are some significant technical roadblocks to supporting `for_each`/`count` in static expressions. For the purposes of this RFC, we are forbidding it. For more information, see [Static Module Expansion](20240513-static-evaluation/module-expansion.md).
|
||||
|
||||
### Technical Approach
|
||||
|
||||
#### Background
|
||||
|
||||
Although mostly limited in scope to one or two packages in OpenTofu, it is important to understand what complex systems it will be resembling and interacting with.
|
||||
|
||||
> [!NOTE]
|
||||
> It is HIGHLY recommended to read the [Architecture document](../docs/architecture.md) before diving too deep into this document. Below, many of the concepts in the Architecture doc are expanded upon or viewed from a different angle for the purposes of understanding this proposal.
|
||||
|
||||
##### Expressions
|
||||
|
||||
The evaluation of expressions (`1 + var.bar` for example) depends on referenced values and functions used in the expression. In that example, you would need to know the value of `var.bar`. That dependency is known via a concept called "[HCL Traversals](https://pkg.go.dev/github.com/hashicorp/hcl/v2#Traversal)", which represent an attribute access path and can be turned into strongly typed "OpenTofu References". In practice, you would say "the expression depends on an OpenTofu Variable named bar".
|
||||
|
||||
Once you know what the requirements are for an expression ([hcl.Expression](https://pkg.go.dev/github.com/hashicorp/hcl/v2#Expression)), you can build up an evaluation context ([hcl.EvalContext](https://pkg.go.dev/github.com/hashicorp/hcl/v2#EvalContext)) to provide those requirements or return an error. In the above example, the evaluation context must include `{"var": {"bar": <somevalue>}`.
|
||||
|
||||
Expression evaluation is currently split up into two stages: config loading and graph reference evaluation.
|
||||
|
||||
##### Config Loading
|
||||
|
||||
During configuration loading, the HCL or JSON config is pulled apart into Blocks and Attributes by the hcl package. A Block can contain Attributes and nested Blocks. Attributes are simply named expressions (`foo = 1 + var.bar` for example).
|
||||
|
||||
```hcl
|
||||
some_block {
|
||||
some_attribute = "some value"
|
||||
}
|
||||
```
|
||||
|
||||
These Blocks and Attributes are abstract representations of the configuration which have not yet been evaluated into actionable values. When processing a block or attribute, a decision is made to either evaluate it immediately if required or to keep the abstract block/attribute for later processing. If it is kept in the abstract representation, it will later be turned into a value by [Graph Reference Evaluation](#graph-reference-evaluation).
|
||||
|
||||
As a concrete example, the `module -> source` field must be known during configuration loading as it is required to continue the next iteration of the loading process. However, attributes like `module -> for_each` may depend on attribute values from resources or other pieces of information not known during config loading and are therefore stored as an expression for the [Graph Reference Evaluation](#graph-reference-evaluation).
|
||||
|
||||
```hcl
|
||||
resource "aws_instance" "example" {
|
||||
name = "server-${count.index}"
|
||||
count = 5
|
||||
# (other resource arguments...)
|
||||
}
|
||||
|
||||
module "dnsentries" {
|
||||
source = "./dnsentries"
|
||||
hostname = each.value
|
||||
for_each = toset(aws_instance.example.*.name)
|
||||
}
|
||||
```
|
||||
|
||||
No evaluation context is built or provided during the entire config loading process. **Therefore, no functions, locals, or variables may be used during config loading due to the lack of an evaluation context. This limitation is what we wish to resolve.**
|
||||
|
||||
##### Graph Reference Evaluation
|
||||
|
||||
After the config is fully loaded, it is [transformed and processed](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/docs/architecture.md#graph-builder) into nodes in a [graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). These nodes use the "[OpenTofu References](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/addrs/parse_ref.go#L174)" present in their blocks/attributes (the ones not evaluated in config loading) to build both the dependency edges in the graph, and [eventually an evaluation context](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/docs/architecture.md#expression-evaluation) once those references are available.
|
||||
|
||||
This theoretically simple process is deeply complicated by the module dependency tree and [expansion therein](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/docs/architecture.md#sub-graphs). A sub graph is dynamically created due to `for_each` and `count` being evaluated as their required references are made available. The majority of the logic in this process exists within the `tofu` and `lang` packages, which are tightly coupled.
|
||||
|
||||
For example, a module's `for_each` statement may require data from a resource: `for_each = resource.aws_s3_bucket.foo.tags`. Before it can be evaluated, the module must wait for "OpenTofu Resource Reference `aws_s3_bucket.foo`" to be available. This would be represented as a dependency edge between the module node and the specific resource node. The evaluation context would then include `{"resource": {"aws_s3_bucket": {"foo": {"tags": <provided value>}}}}`.
|
||||
|
||||
> [!NOTE]
|
||||
> A common misconception is that modules are "objects". However, modules more closely resemble "namespaces" and can cross-reference each other's vars/outputs as long as there is no reference loop.
|
||||
|
||||
|
||||
|
||||
##### Background Summary
|
||||
|
||||
As you can see above, the lack of evaluation contexts during the config loading stage prevents any expressions with references from being expanded. Only primitive types and expressions are currently allowed during that stage.
|
||||
|
||||
By introducing the ability to build and manage evaluation contexts during config loading, we would open up the ability for *certain* references to be evaluated during the config loading process.
|
||||
|
||||
For example, many users expect to be able to use `local` values within `module -> source` to simplify upgrades and DRY up their configuration. This is not currently possible as the value of `module -> source` *must* be known during the config loading stage and can not be deferred until graph evaluation.
|
||||
|
||||
```hcl
|
||||
local {
|
||||
gitrepo = "git://..."
|
||||
}
|
||||
module "mymodule" {
|
||||
source = locals.gitrepo
|
||||
}
|
||||
```
|
||||
|
||||
By utilizing Traversals/References, we can track what values are statically known throughout the config loading process. This will follow a similar pattern to the graph reference evaluation, with limitations in what can be resolved. It may or may not re-use much of the existing graph reference evaluators code, limited by complex dependency tracing required for error handling.
|
||||
|
||||
When evaluating an Attribute/Block into a value, any missing reference must be properly reported in a way that the user can easily debug and understand. For example, a user may try to use a `local` that depends on a resource's value in a module's source. The user must then be told that the `local` can not be used in the module source field as it depends on a resource which is not available during the config loading process. Variables used through the module tree must also be passed with their associated information, such as their references.
|
||||
|
||||
### Development Approach
|
||||
|
||||
Given the scope of what needs to be changed to build this foundation for static evaluation, we are talking about a significant amount of work, potentially spread across multiple releases.
|
||||
|
||||
We can not take the approach of hacking on a feature branch for months or freezing all related code. It's unrealistic and unfair to other developers.
|
||||
|
||||
Instead, we can break this work into smaller discrete and testable components, some of which may be easy to work on in parallel.
|
||||
|
||||
With this piece by piece approach, we can also add testing before, during, and after each component is added/modified.
|
||||
|
||||
The OpenTofu core team should be the ones to do the majority of the core implementation. If community members are interested, much of the issues blocked by the static evaluation are isolated and well defined enough for them to be worked on independently of the core team.
|
||||
|
||||
### Current Load/Evaluation Flow
|
||||
|
||||
Much of this process is covered by the Architecture document linked above and should be used as reference throughout this section.
|
||||
|
||||
Performing an action in OpenTofu (init/plan/apply/etc...) takes the following steps (simplified):
|
||||
|
||||
A [command](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/command/init.go#L193) in the command package is created based on the CLI arguments and is executed. It performs the following actions:
|
||||
|
||||
#### Parse and Load the configuration modules
|
||||
|
||||
Starting at the root module (current directory), a `config.Config` structure is created. This structure is the root node of a tree representing all of the module calls (`module {}`) that make up the project. Each node in the tree contains a `config.Module` and a `addrs.Module` path.
|
||||
|
||||
The tree is built by: installing a module's source, loading the module, inspecting the module calls, recursing in a depth first pattern.
|
||||
```go
|
||||
// Pseudocode
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/config_build.go#L27
|
||||
func buildConfig(source string) configs.Config {
|
||||
c := configs.Config{}
|
||||
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/initwd/module_install.go#L147
|
||||
path = installModule(source)
|
||||
c.module = loadModule(path) // https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/parser_config_dir.go#L41-L58
|
||||
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/config_build.go#L132
|
||||
for name, call := range c.module.calls {
|
||||
c.children[name] = buildConfig(call.source)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
root = buildConfig(".")
|
||||
```
|
||||
|
||||
|
||||
The [configs.Module](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L22-L63) structure is a representation of the module that is a mixture of fields that are computed during the config process and fields who's evaluation is deferred until later. For example: the `module -> source` must be known for config loading to complete, but a resource's config body can be deferred until during graph node evaluation.
|
||||
|
||||
Module loading takes a directory, turns each hcl/json file into a [configs.File](https://github.com/opentofu/opentofu/blob/290fbd6/internal/configs/module.go#L76) structure, merges them together, and returns a `configs.Module`.
|
||||
|
||||
```go
|
||||
// Pseudocode
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/parser_config_dir.go#L41-L58
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L122
|
||||
func loadModule(path string) configs.Module {
|
||||
var files = []file.File
|
||||
for filepath in range(list_files(path)) {
|
||||
files = append(files, loadFile(filepath))
|
||||
}
|
||||
module := configs.Module{}
|
||||
for _, file in range(files) {
|
||||
module.appendFile(file) // https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L205
|
||||
}
|
||||
return module
|
||||
}
|
||||
|
||||
// https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/parser_config.go#L54
|
||||
func loadFile(filepath string) configs.File {
|
||||
file := configs.File{}
|
||||
|
||||
hclBody = hcl.parse(filepath)
|
||||
for _, hclBlock in range(hclBody) {
|
||||
switch (hclBlock.Type) {
|
||||
case "module":
|
||||
file.ModuleCalls = append(file.ModuleCalls, decodeModuleCall(hclBlock))
|
||||
case "variable":
|
||||
file.Variables = append(file.Variables, decodeVariable(hclBlock))
|
||||
// omitted cases pattern for all remaining supported blocks
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
```
|
||||
|
||||
#### Backend is loaded
|
||||
|
||||
The command [constructs a backend](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/command/apply.go#L111) from the configuration
|
||||
|
||||
The backend is what interfaces with the state storage and is in charge of actually executing and managing state operations (plan/apply)
|
||||
|
||||
#### Operation is executed
|
||||
|
||||
The command executes the [operation](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/command/apply.go#L119) using the [backend and the configuration](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/command/apply.go#L135).
|
||||
|
||||
|
||||
A graph is [built from the loaded configuration](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/transform_config.go#L16-L27) and is transformed such that it can be walked.
|
||||
|
||||
This transformation is a complex process, but the some of the key pieces are:
|
||||
* [Transformation and linking based on references detected between nodes](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/transform_reference.go#L119)
|
||||
- Node dependencies are determined by inspecting [blocks](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/transform_reference.go#L584) and [attributes](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/node_resource_abstract.go#L159)
|
||||
- The blocks and attributes are [turned into references](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/lang/references.go#L56) in the `lang` package
|
||||
* Resources nodes are linked to their required provider nodes
|
||||
* Module variables are mapped from parent to child via their module calls
|
||||
|
||||
The graph is then [evaluated](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/graph.go#L86) by [walking each node](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/graph.go#L43) after it's dependencies have been evaluated.
|
||||
|
||||
When evaluating a node in the graph, the [tofu.EvalContext](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/eval_context.go#L117-L125) (implemented by [tofu.BuiltinEvalContext](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/eval_context_builtin.go#L271-L284)) is used to [build and utilize](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/tofu/eval_context_builtin.go#L504) a `lang.Scope` based on the references that the node specifies, all of which should have already been evaluated due to the dependency structure represented by the transformed graph.
|
||||
|
||||
The `lang.Scope` handles the specific details of [taking OpenTofu references and building](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/lang/eval.go#L295) a `hcl.EvalContext` from the values and functions currently available to the EvaluationContext/State.
|
||||
|
||||
### Proposed Changes
|
||||
|
||||
We will need to modify the above design to track references/values in different scopes during the config loading process. This will be called a Static Evaluation Context, with the requirements and potential implementation paths below.
|
||||
|
||||
When loading a module, a static context must be supplied. When called from an external package like `command`, the static context will contain tfvars from both cli options and .tfvars files. When called from within the `configs.Config` tree building process, it will pass static references to values from the `config.ModuleCall` in as supplied variables. In either case, builtin OpenTofu commands are available.
|
||||
|
||||
```go
|
||||
// Pseudocode
|
||||
func buildConfig(source string, ctx StaticContext) configs.Config {
|
||||
c := configs.Config{}
|
||||
|
||||
path = installModule(source)
|
||||
c.module = loadModule(path, ctx)
|
||||
|
||||
for name, call := range c.module.calls {
|
||||
// Should have the required information at this point to evaluate the child's source field
|
||||
source := ctx.Evaluate(call.source)
|
||||
|
||||
// Build a new StaticContext based on the call's configuration attributes.
|
||||
childCtx = ctx.FromModuleCall(call.Name, call.Config)
|
||||
|
||||
c.children[name] = buildConfig(source, childCtx)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func loadModule(path string, ctx StaticContext) configs.Module {
|
||||
var files = []file.File
|
||||
for filepath in range(list_files(path)) {
|
||||
files = append(files, loadFile(filepath))
|
||||
}
|
||||
module := configs.Module{}
|
||||
for _, file in range(files) {
|
||||
module.appendFile(file)
|
||||
}
|
||||
|
||||
// Link in current variables and locals
|
||||
ctx.AddVariables(module.Variables)
|
||||
ctx.AddLocals(module.Locals)
|
||||
|
||||
// Additional processing of module's fields can be done here using the StaticContext
|
||||
|
||||
return module
|
||||
}
|
||||
|
||||
|
||||
root = buildConfig(".", StaticContextFromTFVars(command.TFVars))
|
||||
```
|
||||
|
||||
#### Static Context Design
|
||||
At the heart of the project lies an evaluation context, similar to what currently exist in the `tofu` and `lang` packages. It must serve a similar purpose, but has some differing requirements.
|
||||
|
||||
Any static evaluator must be able to:
|
||||
* Evaluate a hcl expression or block into a single cty value
|
||||
- Provide detailed insight into why a given expression or block can not be turned into a cty value
|
||||
* Be constructed with variables derived from a parent static context corresponding to parent modules
|
||||
- This is primarily for passing values down the module call stack, while maintaining references
|
||||
* Understand current locals and their dependencies
|
||||
|
||||
There are three potential paths in implementing a static evaluator:
|
||||
* Build a custom solution for this specific problem and it's current use cases
|
||||
- This will not be shared with the existing evaluation context
|
||||
- This approach was taken in the prototypes
|
||||
- Can be flexible during development
|
||||
- Does not break other packages
|
||||
- Tests must be written from scratch
|
||||
* Re-use existing components of the tofu and lang packages with new plumbing
|
||||
- Can build on top of existing tested logic
|
||||
- Somewhat flexible as components can be swapped out as needed
|
||||
- May require refactoring existing components we wish to use
|
||||
- May accidentally break other packages due to poor existing testing
|
||||
* Re-use current evaluator/scope constructs in tofu and lang packages
|
||||
- Would require re-designing these components to function in either mode
|
||||
- Would come with most of the core logic already implemented
|
||||
- High likelihood of breaking other packages due to poor existing testing
|
||||
- Would likely require some ergonomic gymnastics depending on scale of refactoring
|
||||
|
||||
This will need to be investigated and roughly prototyped, but all solutions should fit a similar enough interface to not block development of dependent tasks. We should design the interface first, based on the requirements of the initial prototype. Alternatively this could be a more iterative approach where the interface is designed at the same time as being implemented by multiple team members.
|
||||
|
||||
We are deferring this decision to the actual implementation of this work. It is a deeply technical investigation and discussion that does not significantly impact the proposed solution in this RFC.
|
||||
|
||||
#### Overview of dependent issues
|
||||
|
||||
To better understand the exact solution we are trying to solve, a limited overview of problems that could be solved using the static evaluation context are provided.
|
||||
|
||||
##### Backend Configuration
|
||||
|
||||
Once the core is implemented, this is probably the easiest solution to implement.
|
||||
|
||||
Notes from initial prototyping:
|
||||
* The configs.Backend saves the config body during the config load and does not evaluate it
|
||||
* backendInitFromConfig() in command/meta_backend.go is what evaluates the body
|
||||
- This happens before the graph is constructed / evaluated and can be considered an extension of the config loading stage.
|
||||
* We can stash a copy of the StaticContext in the configs.Backend and use it in backendInitFromConfig() to provide an evaluation context for decoding into the given backend schema.
|
||||
- There are a few ways to do this, stashing it there was a simple way to get it working in the prototype.
|
||||
* Don't forget to update the configs.Backend.Hash() function as that's used to detect any changes
|
||||
|
||||
##### Module Sources
|
||||
|
||||
Module sources must be known at init time as they are downloaded and collated into .terraform/modules. This can be implemented by inspecting the source hcl.Expression using the static evaluator scoped to the current module.
|
||||
|
||||
Notes from initial prototyping:
|
||||
* Create a SourceExpression field in config.ModuleCall and don't set the "config.ModuleCall.Source" field initially
|
||||
* Use the static context available during NewModule constructor to evaluate all of the config.ModuleCall source fields and check for bad references and other errors.
|
||||
|
||||
Many of the other blocked issues follow an extremely similar pattern of "store the expression in the first part of config loading and evaluate when needed" and are therefore omitted.
|
||||
|
||||
##### Encryption
|
||||
|
||||
Encryption attempts to be a standalone package that tries hard to limit dependence on OpenTofu code, potentially allowing it to be used independently at some point.
|
||||
|
||||
It uses the hcl libraries directly and does not follow the same patterns as the rest of OpenTofu codebase. This may have a significant impact on the design of the Static Evaluation Context interface.
|
||||
|
||||
##### Provider Iteration
|
||||
|
||||
This will be described in the [Provider Evaluation RFC](20240513-static-evaluation-providers.md) due to expansion complexity.
|
||||
|
||||
#### Blockers
|
||||
|
||||
Existing testing within OpenTofu is fragmented and more sparse than we would like. Additional test coverage will be needed before, during and after each stage of implementation.
|
||||
|
||||
Code coverage should be inspected before refactoring of a component is undertaken to guide the additional test coverage required. We are not aiming for 100%, but should use it as a tool to understand our current testing.
|
||||
|
||||
A comprehensive guide on e2e testing should be written, see https://github.com/opentofu/opentofu/issues/1536.
|
||||
|
||||
#### Performance Considerations
|
||||
|
||||
##### Multiple calls to parse config
|
||||
Due to the partially refactored command package, the configuration in the pwd is loaded, parsed, and evaluated multiple times during many steps. We will be adding more overhead to that action and may wish to focus some effort on easy places to cut out multiple configuration loads. An issue should be created or updated to track the cleanup of the command package.
|
||||
##### Static evaluator overhead
|
||||
We should keep performance in mind for which solution we choose for the static evaluator above
|
||||
|
||||
### Open Questions
|
||||
|
||||
Do we want to support asking for variable values when required but not provided? This is already an established pattern, but may require additional work. It may be prudent to defer this until a later iteration. See above note on provider configuration in the first user error example.
|
||||
|
||||
Do we want to support the core OpenTofu functions in the static evaluation context? Probably as it would be fairly trivial to hook in.
|
||||
|
||||
Do we want to support provider functions during this static evaluation phase? I suspect not, without a good reason as the development costs may be significant with minimal benefit. It is trivial to detect someone attempting to use a provider function in an expression/body and to mark the expression result as dynamic.
|
||||
|
||||
### Future Considerations
|
||||
|
||||
[Static Module Expansion](20240513-static-evaluation/module-expansion.md) is currently forbidden due to the significant architectural changes required. The linked document serves as an exploration into what that architectural change could look like if the need arises.
|
||||
|
||||
#### Static Module Outputs
|
||||
It would be quite useful to pull in a single module which defined sources and versions of dependencies across multiple projects within an organization. This would enable the following example:
|
||||
```hcl
|
||||
module "mycompany" {
|
||||
source = "git::.../sources"
|
||||
}
|
||||
|
||||
module "capability" {
|
||||
source = ${module.mycompany.some_component}
|
||||
}
|
||||
|
||||
module "other_capability" {
|
||||
source = ${module.mycompany.other_component}
|
||||
}
|
||||
```
|
||||
|
||||
All modules referenced by a parent module are downloaded and added to the config graph without any understanding of inter-dependencies. To implement this, we would need to rewrite the config builder to be aware of the state evaluator and increase the complexity of that component.
|
||||
|
||||
I am not sure the engineering effort here is warranted, but it should at least be investigated
|
||||
|
||||
## Potential Alternatives
|
||||
|
||||
Tools like terragrunt offer an abstraction layer on top of OpenTofu, which many users find beneficial. Building some of these features into OpenTofu means that out of the box you do not need additional tooling. Additionally, terragrunt can focus more on more complex problems that occur when orchestrating complex infrastructure among multiple OpenTofu projects instead of patching around OpenTofu limitations.
|
||||
|
||||
A distinct pre-processor is another option, but that would require a completely distinct language to be designed and implemented to pre-process the configuration. Additionally, it would not integrate easily with any existing OpenTofu constructs.
|
254
rfc/20240513-static-evaluation/module-expansion.md
Normal file
254
rfc/20240513-static-evaluation/module-expansion.md
Normal file
@ -0,0 +1,254 @@
|
||||
This is an ancillary document to the [Static Evaluation RFC](20240513-static-evaluation.md) and is not planned on being implemented. It serves to document the reasoning behind why we are deciding to defer implementation of this complex functionality. For now, we have decided to implement a limited version of this that allows provider aliases *only* to be specified via for_each/count.
|
||||
|
||||
This document should be used as a reference for anyone considering implementing this in the future. It is not designed as a comprehensive guide, but instead as documenting the previous exploration of this concept during the prototyping phase of the Static Evaluation RFC.
|
||||
|
||||
# Static Module Expansion
|
||||
|
||||
Modules may be expanded using for_each and count. This poses a problem for the static evaluation step.
|
||||
|
||||
For example:
|
||||
```hcl
|
||||
# main.tf
|
||||
module "mod" {
|
||||
for_each = {"us" = "first", "eu" = "second"}
|
||||
source = "./my-mod-${each.vakue}"
|
||||
name = each.key
|
||||
}
|
||||
```
|
||||
|
||||
Each instance of "mod" will have a different source. This is a complex situation that must have intense validation, inputs and outputs must be identical between the two modules.
|
||||
|
||||
The example is a bit contrived, but is a simpler representation of why it's difficult to have different module sources for different instaces down a configuration tree.
|
||||
|
||||
If we want to allow this, modules which have static for_each and count expressions must be expanded at the config layer. This must happen before the graph building, transformers, and walking.
|
||||
|
||||
This document assumes you have read the [Static Evaluation RFC](20240513-static-evaluation.md) and understand the concepts in there.
|
||||
|
||||
## Current structure and paths
|
||||
|
||||
Over half of OpenTofu does not understand module/resource instances. They have a simplified view of the world that is called "pre-expansion".
|
||||
|
||||
Relevant components for this document:
|
||||
|
||||
Pre-expansion:
|
||||
* Module structure in configs package
|
||||
* ModuleCalls structure in configs package
|
||||
* Config tree in configs package
|
||||
* Module cache file/filetree
|
||||
* Graph structure and transformers in tofu package (mixed)
|
||||
* EvaluationContext (mixed)
|
||||
|
||||
Post-expansion
|
||||
* Graph structure and transformers in tofu package (mixed)
|
||||
* EvaluationContext (mixed)
|
||||
|
||||
|
||||
## Example represenations:
|
||||
|
||||
Variables and providers have been excluded for this example.
|
||||
|
||||
**HCL:**
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
module "test" {
|
||||
for_each = {"a": "first", "b": "second" }
|
||||
source = "./mod"
|
||||
name = each.key
|
||||
description = each.value
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# mod/mod.tf
|
||||
variable "name" {}
|
||||
variable "description" {}
|
||||
resource "tfcoremock_resource" { string = var.name, other = var.description }
|
||||
```
|
||||
|
||||
**configs.Config**:
|
||||
|
||||
```
|
||||
root = Config {
|
||||
Root = root
|
||||
Parent = nil
|
||||
Module = Module{
|
||||
ModuleCalls = {
|
||||
"test" = { source = "./mod", for_each = hcl.Expression, ... }
|
||||
}
|
||||
}
|
||||
Path = addrs.Module[]
|
||||
Children = { "test" = test }
|
||||
}
|
||||
test = {
|
||||
Root = root
|
||||
Parent = root
|
||||
Module = { ... }
|
||||
Path = addrs.Module["test"]
|
||||
Children = {}
|
||||
}
|
||||
```
|
||||
|
||||
**tofu.Graph (simplified)**
|
||||
|
||||
|
||||
Before Expansion:
|
||||
```
|
||||
rootExpand = NodeExpandModule {
|
||||
Addr = addrs.Module[]
|
||||
Config = root
|
||||
ModuleCall = nil
|
||||
}
|
||||
testExpand = NodeExpandModule {
|
||||
Addr = addrs.Module["test"]
|
||||
Config = test
|
||||
ModuleCall = root.Module.ModuleCalls["test"]
|
||||
}
|
||||
testExpandResource = NodeExpandResource {
|
||||
NodeResource {
|
||||
Addr = addrs.Module["test", "resource"]
|
||||
Config = test.Module.Resources["resource"]
|
||||
}
|
||||
}
|
||||
|
||||
testExpand -> rootExpand
|
||||
testExpandResource -> testExpand
|
||||
```
|
||||
|
||||
With Expansion:
|
||||
```
|
||||
testExpandResourceA = NodeResourceInstance {
|
||||
NodeResource = testExpandResource.NodeResource
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"a"}, {"resource", NoKey}]
|
||||
}
|
||||
testExpandResourceB = NodeResourceInstance {
|
||||
NodeResource = testExpandResource.NodeResource
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"b"}, {"resource", NoKey}]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Proposed structure and paths
|
||||
|
||||
To implement a fully fledged static evaluator which supports for_each and count on modules/providers, the concept of module instances must be brought to all components in the previous section.
|
||||
|
||||
One approach is to remove the concept of a "non-instanced" module path and simply deleted addrs.Module entirely and changed all references to addrs.ModuleInstance (among a number of other changes). This is a incredibly complex change with many ramifications.
|
||||
|
||||
addrs.Module is simply a []string, while addrs.ModuleInstance is a pair of {string, key} where key is:
|
||||
* nil/NoKey representing no instances
|
||||
* CountKey for int count
|
||||
* ForEachKey for string for_each
|
||||
|
||||
## Example represenations for Module -> ModuleInstance:
|
||||
**HCL (identical):**
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
module "test" {
|
||||
for_each = {"a": "first", "b": "second" }
|
||||
source = "./mod"
|
||||
key = each.key
|
||||
value = each.value
|
||||
}
|
||||
```
|
||||
|
||||
```hcl
|
||||
# mod/mod.tf
|
||||
variable "key" {}
|
||||
variable "value" {}
|
||||
resource "tfcoremock_resource" { string = var.key, other = var.value }
|
||||
```
|
||||
|
||||
**configs.Config**
|
||||
|
||||
Changes:
|
||||
* All addresses are instanced.
|
||||
* ModuleCalls is expanded into ExpandedModuleCalls using the static evaluator
|
||||
* The root Children map points to distinct instances of `test["a"]` and `test["b"]`
|
||||
|
||||
```
|
||||
root = Config {
|
||||
Root = root
|
||||
Parent = nil
|
||||
Module = Module{
|
||||
ModuleCalls = {
|
||||
"test" = { source = "./mod", for_each = hcl.Expression, ... }
|
||||
}
|
||||
ExpandedModuleCalls = {
|
||||
{"test", Key{"a"}} = { source = "./mod", for_each = nil, ... }
|
||||
{"test", Key{"b"}} = { source = "./mod", for_each = nil, ... }
|
||||
}
|
||||
}
|
||||
Path = addrs.ModuleInstance[]
|
||||
Children = { "test" = { "a": testA, "b": testB } }
|
||||
}
|
||||
testA = {
|
||||
Root = root
|
||||
Parent = root
|
||||
Module = { ... }
|
||||
Path = addrs.ModuleInstance[{"test", "a"}]
|
||||
Children = {}
|
||||
}
|
||||
testB = {
|
||||
Root = root
|
||||
Parent = root
|
||||
Module = { ... }
|
||||
Path = addrs.ModuleInstance[{"test", "a"}]
|
||||
Children = {}
|
||||
}
|
||||
```
|
||||
**tofu.Graph (simplified)**
|
||||
|
||||
Changes:
|
||||
* All addresses are instanced.
|
||||
* Pre-expanded modules are present in the graph and linked to single instances post-expansion.
|
||||
|
||||
Before Expansion:
|
||||
```
|
||||
rootExpand = NodeExpandModule {
|
||||
Addr = addrs.ModuleInstance[]
|
||||
Config = root
|
||||
ModuleCall = nil
|
||||
}
|
||||
testExpandA = NodeExpandModule {
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"a"}}]
|
||||
Config = testA
|
||||
ModuleCall = root.Module.ExpandedModuleCalls["test"]["a"]
|
||||
}
|
||||
testExpandB = NodeExpandModule {
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"b"}}]
|
||||
Config = testB
|
||||
ModuleCall = root.Module.ExpandedModuleCalls["test"]["b"]
|
||||
}
|
||||
testExpandResourceA = NodeExpandResource {
|
||||
NodeResource {
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"a"}}, {"resource", NoKey}]
|
||||
Config = testA.Module.Resources["resource"]
|
||||
}
|
||||
}
|
||||
testExpandResourceB = NodeExpandResource {
|
||||
NodeResource {
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"b"}}, {"resource", NoKey}]
|
||||
Config = testB.Module.Resources["resource"]
|
||||
}
|
||||
}
|
||||
|
||||
testExpandA -> rootExpand
|
||||
testExpandB -> rootExpand
|
||||
testExpandResourceA -> testExpandA
|
||||
testExpandResourceB -> testExpandB
|
||||
```
|
||||
|
||||
With Expansion:
|
||||
```
|
||||
testExpandResourceA = NodeResourceInstance {
|
||||
NodeResource = testExpandResourceA.NodeResource
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"a"}, {"resource", NoKey}]
|
||||
}
|
||||
testExpandResourceB = NodeResourceInstance {
|
||||
NodeResource = testExpandResourceB.NodeResource
|
||||
Addr = addrs.ModuleInstance[{"test", Key{"b"}, {"resource", NoKey}]
|
||||
}
|
||||
```
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user