Merge pull request #30138 from hashicorp/alisdair/json-module-call-providers-mapping

jsonconfig: Improve provider configuration output
This commit is contained in:
Alisdair McDiarmid 2022-02-10 10:33:47 -05:00 committed by GitHub
commit d8018270a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 775 additions and 12 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

@ -22,7 +22,7 @@ import (
// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "1.0"
const FormatVersion = "1.1"
// Plan is the top-level representation of the json format of a plan. It includes
// the complete config and current state.

View File

@ -576,6 +576,9 @@ func TestShow_json_output(t *testing.T) {
}
json.Unmarshal([]byte(byteValue), &want)
// Disregard format version to reduce needless test fixture churn
want.FormatVersion = got.FormatVersion
if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}
@ -667,6 +670,9 @@ func TestShow_json_output_sensitive(t *testing.T) {
}
json.Unmarshal([]byte(byteValue), &want)
// Disregard format version to reduce needless test fixture churn
want.FormatVersion = got.FormatVersion
if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}

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>
}
}
}