jsonconfig: Improve provider configuration output

When rendering configuration as JSON, we have a single map of provider
configurations at the top level, since these are globally applicable.
Each resource has an opaque key into this map which points at the
configuration data for the provider.

This commit fixes two bugs in this implementation:

- Resources in non-root modules had an invalid provider config key,
  which meant that there was never a valid reference to the provider
  config block. These keys were prefixed with the local module name
  instead of the path to the module. This is now corrected.

- Modules with passed provider configs would point to either an empty
  provider config block or one which is not present at all. This has
  been fixed so that these resources point to the provider config block
  from the calling module (or wherever up the module tree it was
  originally defined).

We also add a "full_name" key-value pair to the provider config block,
with the entire fully-qualified provider name including hostname and
namespace.
This commit is contained in:
Alisdair McDiarmid 2021-12-10 12:48:32 -05:00
parent d1ac8b71d4
commit f5b90f84a8
12 changed files with 768 additions and 11 deletions

View File

@ -27,10 +27,12 @@ type config struct {
// module boundaries.
type providerConfig struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Alias string `json:"alias,omitempty"`
VersionConstraint string `json:"version_constraint,omitempty"`
ModuleAddress string `json:"module_address,omitempty"`
Expressions map[string]interface{} `json:"expressions,omitempty"`
parentKey string
}
type module struct {
@ -120,7 +122,6 @@ func Marshal(c *configs.Config, schemas *terraform.Schemas) ([]byte, error) {
pcs := make(map[string]providerConfig)
marshalProviderConfigs(c, schemas, pcs)
output.ProviderConfigs = pcs
rootModule, err := marshalModule(c, schemas, "")
if err != nil {
@ -128,6 +129,15 @@ func Marshal(c *configs.Config, schemas *terraform.Schemas) ([]byte, error) {
}
output.RootModule = rootModule
normalizeModuleProviderKeys(&rootModule, pcs)
for name, pc := range pcs {
if pc.parentKey != "" {
delete(pcs, name)
}
}
output.ProviderConfigs = pcs
ret, err := json.Marshal(output)
return ret, err
}
@ -154,6 +164,7 @@ func marshalProviderConfigs(
p := providerConfig{
Name: pc.Name,
FullName: providerFqn.String(),
Alias: pc.Alias,
ModuleAddress: c.Path.String(),
Expressions: marshalExpressions(pc.Config, schema),
@ -176,6 +187,30 @@ func marshalProviderConfigs(
// Ensure that any required providers with no associated configuration
// block are included in the set.
for k, pr := range c.Module.ProviderRequirements.RequiredProviders {
// If a provider has aliases defined, process those first.
for _, alias := range pr.Aliases {
// If there exists a value for this provider, we have nothing to add
// to it, so skip.
key := opaqueProviderKey(alias.StringCompact(), c.Path.String())
if _, exists := m[key]; exists {
continue
}
// Given no provider configuration block exists, the only fields we can
// fill here are the local name, FQN, module address, and version
// constraints.
p := providerConfig{
Name: pr.Name,
FullName: pr.Type.String(),
ModuleAddress: c.Path.String(),
}
if vc, ok := reqs[pr.Type]; ok {
p.VersionConstraint = getproviders.VersionConstraintsString(vc)
}
m[key] = p
}
// If there exists a value for this provider, we have nothing to add
// to it, so skip.
key := opaqueProviderKey(k, c.Path.String())
@ -188,6 +223,7 @@ func marshalProviderConfigs(
// constraints.
p := providerConfig{
Name: pr.Name,
FullName: pr.Type.String(),
ModuleAddress: c.Path.String(),
}
@ -199,7 +235,53 @@ func marshalProviderConfigs(
}
// Must also visit our child modules, recursively.
for _, cc := range c.Children {
for name, mc := range c.Module.ModuleCalls {
// Keys in c.Children are guaranteed to match those in c.Module.ModuleCalls
cc := c.Children[name]
// Add provider config map entries for passed provider configs,
// pointing at the passed configuration
for _, ppc := range mc.Providers {
// These provider names include aliases, if set
moduleProviderName := ppc.InChild.String()
parentProviderName := ppc.InParent.String()
// Look up the provider FQN from the module context, using the non-aliased local name
providerFqn := cc.ProviderForConfigAddr(addrs.LocalProviderConfig{LocalName: ppc.InChild.Name})
// The presence of passed provider configs means that we cannot have
// any configuration expressions or version constraints here
p := providerConfig{
Name: moduleProviderName,
FullName: providerFqn.String(),
ModuleAddress: cc.Path.String(),
}
key := opaqueProviderKey(moduleProviderName, cc.Path.String())
parentKey := opaqueProviderKey(parentProviderName, cc.Parent.Path.String())
// Traverse up the module call tree until we find the provider
// configuration which has no linked parent config. This is then
// the source of the configuration used in this module call, so
// we link to it directly
for {
parent, exists := m[parentKey]
if !exists {
break
}
p.parentKey = parentKey
parentKey = parent.parentKey
if parentKey == "" {
break
}
}
m[key] = p
}
// Finally, marshal any other provider configs within the called module.
// It is safe to do this last because it is invalid to configure a
// provider which has passed provider configs in the module call.
marshalProviderConfigs(cc, schemas, m)
}
}
@ -319,7 +401,9 @@ func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terra
}
ret.Expressions = marshalExpressions(mc.Config, schema)
module, _ := marshalModule(c, schemas, mc.Name)
module, _ := marshalModule(c, schemas, c.Path.String())
ret.Module = module
if len(mc.DependsOn) > 0 {
@ -342,11 +426,12 @@ func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terra
func marshalResources(resources map[string]*configs.Resource, schemas *terraform.Schemas, moduleAddr string) ([]resource, error) {
var rs []resource
for _, v := range resources {
providerConfigKey := opaqueProviderKey(v.ProviderConfigAddr().StringCompact(), moduleAddr)
r := resource{
Address: v.Addr().String(),
Type: v.Type,
Name: v.Name,
ProviderConfigKey: opaqueProviderKey(v.ProviderConfigAddr().StringCompact(), moduleAddr),
ProviderConfigKey: providerConfigKey,
}
switch v.Mode {
@ -416,6 +501,23 @@ func marshalResources(resources map[string]*configs.Resource, schemas *terraform
return rs, nil
}
// Flatten all resource provider keys in a module and its descendents, such
// that any resources from providers using a configuration passed through the
// module call have a direct refernce to that provider configuration.
func normalizeModuleProviderKeys(m *module, pcs map[string]providerConfig) {
for i, r := range m.Resources {
if pc, exists := pcs[r.ProviderConfigKey]; exists {
if _, hasParent := pcs[pc.parentKey]; hasParent {
m.Resources[i].ProviderConfigKey = pc.parentKey
}
}
}
for _, mc := range m.ModuleCalls {
normalizeModuleProviderKeys(&mc.Module, pcs)
}
}
// opaqueProviderKey generates a unique absProviderConfig-like string from the module
// address and provider
func opaqueProviderKey(provider string, addr string) (key string) {

View File

@ -164,6 +164,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"

View File

@ -152,6 +152,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"

View File

@ -224,7 +224,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "module_test_bar:test",
"provider_config_key": "module.module_test_bar:test",
"expressions": {
"ami": {
"references": [
@ -265,7 +265,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "module_test_foo:test",
"provider_config_key": "module.module_test_foo:test",
"expressions": {
"ami": {
"references": [
@ -291,7 +291,8 @@
"provider_config": {
"module.module_test_foo:test": {
"module_address": "module.module_test_foo",
"name": "test"
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test"
}
}
}

View File

@ -68,7 +68,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "more:test",
"provider_config_key": "module.my_module.module.more:test",
"expressions": {
"ami": {
"references": [

View File

@ -0,0 +1,26 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
configuration_aliases = [test, test.second]
}
}
}
resource "test_instance" "test_primary" {
ami = "primary"
provider = test
}
resource "test_instance" "test_secondary" {
ami = "secondary"
provider = test.second
}
module "grandchild" {
source = "./nested"
providers = {
test = test
test.alt = test.second
}
}

View File

@ -0,0 +1,18 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
configuration_aliases = [test, test.alt]
}
}
}
resource "test_instance" "test_main" {
ami = "main"
provider = test
}
resource "test_instance" "test_alternate" {
ami = "secondary"
provider = test.alt
}

View File

@ -0,0 +1,34 @@
provider "test" {
region = "somewhere"
}
provider "test" {
alias = "backup"
region = "elsewhere"
}
resource "test_instance" "test" {
ami = "foo"
provider = test
}
resource "test_instance" "test_backup" {
ami = "foo-backup"
provider = test.backup
}
module "child" {
source = "./child"
providers = {
test = test
test.second = test.backup
}
}
module "sibling" {
source = "./child"
providers = {
test = test
test.second = test
}
}

View File

@ -0,0 +1,567 @@
{
"format_version": "1.0",
"terraform_version": "1.1.0-dev",
"planned_values": {
"root_module": {
"resources": [
{
"address": "test_instance.test",
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "foo"
},
"sensitive_values": {}
},
{
"address": "test_instance.test_backup",
"mode": "managed",
"type": "test_instance",
"name": "test_backup",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "foo-backup"
},
"sensitive_values": {}
}
],
"child_modules": [
{
"resources": [
{
"address": "module.child.test_instance.test_primary",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "primary"
},
"sensitive_values": {}
},
{
"address": "module.child.test_instance.test_secondary",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "secondary"
},
"sensitive_values": {}
}
],
"address": "module.child",
"child_modules": [
{
"resources": [
{
"address": "module.child.module.grandchild.test_instance.test_alternate",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "secondary"
},
"sensitive_values": {}
},
{
"address": "module.child.module.grandchild.test_instance.test_main",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "main"
},
"sensitive_values": {}
}
],
"address": "module.child.module.grandchild"
}
]
},
{
"resources": [
{
"address": "module.sibling.test_instance.test_primary",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "primary"
},
"sensitive_values": {}
},
{
"address": "module.sibling.test_instance.test_secondary",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "secondary"
},
"sensitive_values": {}
}
],
"address": "module.sibling",
"child_modules": [
{
"resources": [
{
"address": "module.sibling.module.grandchild.test_instance.test_alternate",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "secondary"
},
"sensitive_values": {}
},
{
"address": "module.sibling.module.grandchild.test_instance.test_main",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0,
"values": {
"ami": "main"
},
"sensitive_values": {}
}
],
"address": "module.sibling.module.grandchild"
}
]
}
]
}
},
"resource_changes": [
{
"address": "module.child.module.grandchild.test_instance.test_alternate",
"module_address": "module.child.module.grandchild",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "secondary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.child.module.grandchild.test_instance.test_main",
"module_address": "module.child.module.grandchild",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "main"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.child.test_instance.test_primary",
"module_address": "module.child",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "primary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.child.test_instance.test_secondary",
"module_address": "module.child",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "secondary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.sibling.module.grandchild.test_instance.test_alternate",
"module_address": "module.sibling.module.grandchild",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "secondary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.sibling.module.grandchild.test_instance.test_main",
"module_address": "module.sibling.module.grandchild",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "main"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.sibling.test_instance.test_primary",
"module_address": "module.sibling",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "primary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "module.sibling.test_instance.test_secondary",
"module_address": "module.sibling",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "secondary"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "test_instance.test",
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "foo"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "test_instance.test_backup",
"mode": "managed",
"type": "test_instance",
"name": "test_backup",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "foo-backup"
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
}
],
"configuration": {
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"
}
}
},
"test.backup": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"alias": "backup",
"expressions": {
"region": {
"constant_value": "elsewhere"
}
}
}
},
"root_module": {
"resources": [
{
"address": "test_instance.test",
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "foo"
}
},
"schema_version": 0
},
{
"address": "test_instance.test_backup",
"mode": "managed",
"type": "test_instance",
"name": "test_backup",
"provider_config_key": "test.backup",
"expressions": {
"ami": {
"constant_value": "foo-backup"
}
},
"schema_version": 0
}
],
"module_calls": {
"child": {
"source": "./child",
"module": {
"resources": [
{
"address": "test_instance.test_primary",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "primary"
}
},
"schema_version": 0
},
{
"address": "test_instance.test_secondary",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_config_key": "test.backup",
"expressions": {
"ami": {
"constant_value": "secondary"
}
},
"schema_version": 0
}
],
"module_calls": {
"grandchild": {
"source": "./nested",
"module": {
"resources": [
{
"address": "test_instance.test_alternate",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_config_key": "test.backup",
"expressions": {
"ami": {
"constant_value": "secondary"
}
},
"schema_version": 0
},
{
"address": "test_instance.test_main",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "main"
}
},
"schema_version": 0
}
]
}
}
}
}
},
"sibling": {
"source": "./child",
"module": {
"resources": [
{
"address": "test_instance.test_primary",
"mode": "managed",
"type": "test_instance",
"name": "test_primary",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "primary"
}
},
"schema_version": 0
},
{
"address": "test_instance.test_secondary",
"mode": "managed",
"type": "test_instance",
"name": "test_secondary",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "secondary"
}
},
"schema_version": 0
}
],
"module_calls": {
"grandchild": {
"source": "./nested",
"module": {
"resources": [
{
"address": "test_instance.test_alternate",
"mode": "managed",
"type": "test_instance",
"name": "test_alternate",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "secondary"
}
},
"schema_version": 0
},
{
"address": "test_instance.test_main",
"mode": "managed",
"type": "test_instance",
"name": "test_main",
"provider_config_key": "test",
"expressions": {
"ami": {
"constant_value": "main"
}
},
"schema_version": 0
}
]
}
}
}
}
}
}
}
}
}

View File

@ -152,6 +152,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"version_constraint": ">= 1.2.3"
}
},

View File

@ -152,6 +152,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"

View File

@ -323,7 +323,7 @@ Because the configuration models are produced at a stage prior to expression eva
// the configuration tree, flattened into a single map for convenience since
// provider configurations are the one concept in Terraform that can span
// across module boundaries.
"provider_configs": {
"provider_config": {
// Keys in the provider_configs map are to be considered opaque by callers,
// and used just for lookups using the "provider_config_key" property in each
@ -333,6 +333,9 @@ Because the configuration models are produced at a stage prior to expression eva
// "name" is the name of the provider without any alias
"name": "aws",
// "full_name" is the fully-qualified provider name
"full_name": "registry.terraform.io/hashicorp/aws",
// "alias" is the alias set for a non-default configuration, or unset for
// a default configuration.
"alias": "foo",
@ -378,7 +381,9 @@ Because the configuration models are produced at a stage prior to expression eva
// "provider_config_key" is the key into "provider_configs" (shown
// above) for the provider configuration that this resource is
// associated with.
// associated with. If the provider configuration was passed into
// this module from the parent module, the key will point to the
// original provider config block.
"provider_config_key": "opaque_provider_ref_aws",
// "provisioners" is an optional field which describes any provisioners.
@ -440,7 +445,7 @@ Because the configuration models are produced at a stage prior to expression eva
// "module" is a representation of the configuration of the child module
// itself, using the same structure as the "root_module" object,
// recursively describing the full module tree.
"module": <module-configuration-representation>,
"module": <module-configuration-representation>
}
}
}